Guava: Set <K> + Function <K, V> = Map <K, V>?
Is there an idiomatic way to take Set<K> and Function<K,V> and get Map<K,V> live view? (i.e. Map supported by a combination of Set and Function , and if, for example, an element is added to Set , then the corresponding record also exists in Map ).
(see, for example, Collections2.filter for a more detailed discussion of live views)
What if real-time viewing is not needed? Is there anything better than this:
public static <K,V> Map<K,V> newMapFrom(Set<K> keys, Function<? super K,V> f) { Map<K,V> map = Maps.newHashMap(); for (K k : keys) { map.put(k, f.apply(k)); } return map; } Create a map from a set and function
Here are two classes, each of which must do the job. The first one simply shows the view of the dialing map, and the second can write the values โโback to the dialing via a special interface.
Call Syntax:
Map<K,V> immutable = new SetBackedMap<K,V>(Set<K> keys, Function<K,V> func); Map<K,V> mutable = new MutableSetBackedMap<K,V>(Set<K> keys, Function<K,V> func); Where to place this code?
Side note: if guava was my library, I would make them available through the Maps class:
Map<K,V> immutable = Maps.immutableComputingMap(Set<K> keys, Function<K,V> func); Map<K,V> mutable = Maps.mutableComputingMap(Set<K> keys, Function<K,V> func); Immutable Version:
I implemented this as a one-way view:
- Changes in the set are reflected in the map, but not vice versa (and you cannot change the map in any case, the
put(key, value)method is not implemented). - The
entrySet()iterator uses the set iterator internally, so it will also inherit the internal iterator handlingConcurrentModificationException. - Both
put(k,v)andentrySet().iterator().remove()will throw anUnsupportedOperationException. - Values โโare cached in
WeakHashMap, without special concurrency processing, i.e. synchronization to any level. This will be done in most cases, but if your function is expensive, you can add some lock.
The code:
public class SetBackedMap<K, V> extends AbstractMap<K, V>{ private class MapEntry implements Entry<K, V>{ private final K key; public MapEntry(final K key){ this.key = key; } @Override public K getKey(){ return this.key; } @Override public V getValue(){ V value = SetBackedMap.this.cache.get(this.key); if(value == null){ value = SetBackedMap.this.funk.apply(this.key); SetBackedMap.this.cache.put(this.key, value); } return value; } @Override public V setValue(final V value){ throw new UnsupportedOperationException(); } } private class EntrySet extends AbstractSet<Entry<K, V>>{ public class EntryIterator implements Iterator<Entry<K, V>>{ private final Iterator<K> inner; public EntryIterator(){ this.inner = EntrySet.this.keys.iterator(); } @Override public boolean hasNext(){ return this.inner.hasNext(); } @Override public Map.Entry<K, V> next(){ final K key = this.inner.next(); return new MapEntry(key); } @Override public void remove(){ throw new UnsupportedOperationException(); } } private final Set<K> keys; public EntrySet(final Set<K> keys){ this.keys = keys; } @Override public Iterator<Map.Entry<K, V>> iterator(){ return new EntryIterator(); } @Override public int size(){ return this.keys.size(); } } private final WeakHashMap<K, V> cache; private final Set<Entry<K, V>> entries; private final Function<? super K, ? extends V> funk; public SetBackedMap( final Set<K> keys, Function<? super K, ? extends V> funk){ this.funk = funk; this.cache = new WeakHashMap<K, V>(); this.entries = new EntrySet(keys); } @Override public Set<Map.Entry<K, V>> entrySet(){ return this.entries; } } Test:
final Map<Integer, String> map = new SetBackedMap<Integer, String>( new TreeSet<Integer>(Arrays.asList( 1, 2, 4, 8, 16, 32, 64, 128, 256)), new Function<Integer, String>(){ @Override public String apply(final Integer from){ return Integer.toBinaryString(from.intValue()); } }); for(final Map.Entry<Integer, String> entry : map.entrySet()){ System.out.println( "Key: " + entry.getKey() + ", value: " + entry.getValue()); } Output:
Key: 1, value: 1 Key: 2, value: 10 Key: 4, value: 100 Key: 8, value: 1000 Key: 16, value: 10000 Key: 32, value: 100000 Key: 64, value: 1000000 Key: 128, value: 10000000 Key: 256, value: 100000000 Mutable Version:
While I think itโs a good idea to do it one way, here is a version for Emil that provides a two-way presentation (this is a variation of Emilโs variation of my solution :-)). This requires an extended map interface, which I will call ComputingMap , to show that it is a map where it makes no sense to call put(key, value) .
Card Interface:
public interface ComputingMap<K, V> extends Map<K, V>{ boolean removeKey(final K key); boolean addKey(final K key); } Card Implementation:
public class MutableSetBackedMap<K, V> extends AbstractMap<K, V> implements ComputingMap<K, V>{ public class MapEntry implements Entry<K, V>{ private final K key; public MapEntry(final K key){ this.key = key; } @Override public K getKey(){ return this.key; } @Override public V getValue(){ V value = MutableSetBackedMap.this.cache.get(this.key); if(value == null){ value = MutableSetBackedMap.this.funk.apply(this.key); MutableSetBackedMap.this.cache.put(this.key, value); } return value; } @Override public V setValue(final V value){ throw new UnsupportedOperationException(); } } public class EntrySet extends AbstractSet<Entry<K, V>>{ public class EntryIterator implements Iterator<Entry<K, V>>{ private final Iterator<K> inner; public EntryIterator(){ this.inner = MutableSetBackedMap.this.keys.iterator(); } @Override public boolean hasNext(){ return this.inner.hasNext(); } @Override public Map.Entry<K, V> next(){ final K key = this.inner.next(); return new MapEntry(key); } @Override public void remove(){ throw new UnsupportedOperationException(); } } public EntrySet(){ } @Override public Iterator<Map.Entry<K, V>> iterator(){ return new EntryIterator(); } @Override public int size(){ return MutableSetBackedMap.this.keys.size(); } } private final WeakHashMap<K, V> cache; private final Set<Entry<K, V>> entries; private final Function<? super K, ? extends V> funk; private final Set<K> keys; public MutableSetBackedMap(final Set<K> keys, final Function<? super K, ? extends V> funk){ this.keys = keys; this.funk = funk; this.cache = new WeakHashMap<K, V>(); this.entries = new EntrySet(); } @Override public boolean addKey(final K key){ return this.keys.add(key); } @Override public boolean removeKey(final K key){ return this.keys.remove(key); } @Override public Set<Map.Entry<K, V>> entrySet(){ return this.entries; } } Test:
public static void main(final String[] args){ final ComputingMap<Integer, String> map = new MutableSetBackedMap<Integer, String>( new TreeSet<Integer>(Arrays.asList( 1, 2, 4, 8, 16, 32, 64, 128, 256)), new Function<Integer, String>(){ @Override public String apply(final Integer from){ return Integer.toBinaryString(from.intValue()); } }); System.out.println(map); map.addKey(3); map.addKey(217); map.removeKey(8); System.out.println(map); } Output:
{1=1, 2=10, 4=100, 8=1000, 16=10000, 32=100000, 64=1000000, 128=10000000, 256=100000000} {1=1, 2=10, 3=11, 4=100, 16=10000, 32=100000, 64=1000000, 128=10000000, 217=11011001, 256=100000000} Attention. Sean Patrick Floyd answers, although very helpful, has a flaw. Simple, but it took me a while to debug, so don't fall into the same trap: the MapEntry class requires equals and hashcode values. Here is mine (a simple copy from javadoc).
@Override public boolean equals(Object obj) { if (!(obj instanceof Entry)) { return false; } Entry<?, ?> e2 = (Entry<?, ?>) obj; return (getKey() == null ? e2.getKey() == null : getKey().equals(e2.getKey())) && (getValue() == null ? e2.getValue() == null : getValue().equals(e2.getValue())); } @Override public int hashCode() { return (getKey() == null ? 0 : getKey().hashCode()) ^ (getValue() == null ? 0 : getValue().hashCode()); } This answer would be better as a comment on the corresponding answer, but AFAIU I have no right to leave a comment (or did not find how!).
In Guava 14, Maps.asMap now Maps.asMap for viewing Set and Maps.toMap for an unaltered copy.
You can see most of the discussion of the issues raised here: https://github.com/google/guava/issues/56
For inanimate viewing, the code exists in lambdaJ with Lambda.map(Set, Converter) .
Set<K> setKs = new Set<K>(); Converter<K, V> converterKv = new Converter<K,V>{ @Override public V convert(K from){ return null; //Not useful here but you can do whatever you want } } Map<K, V> mapKvs = Lambda.map(setKs, converterKv); I tried my own implementation: http://ideone.com/Kkpcn As stated in the comments, I need to extend another class, so I just implemented Map , so there is so much code.
There is an absolutely useless function (or not?) That allows you to change the converter on the fly.
I donโt know, this is what you mean by live performance. Any way is my attempt.
public class GuavaTst { public static void main(String[] args) { final Function<String, String> functionToLower = new Function<String, String>() { public String apply (String input) { return input.toLowerCase(); } }; final Set<String> set=new HashSet<String>(); set.add("Hello"); set.add("BYE"); set.add("gOOd"); Map<String, String> testMap = newLiveMap(set,functionToLower); System.out.println("Map :- "+testMap); System.out.println("Set :- "+set); set.add("WoRld"); System.out.println("Map :- "+testMap); System.out.println("Set :- "+set); testMap.put("OMG",""); System.out.println("Map :- "+testMap); System.out.println("Set :- "+set); } static <K,V> Map<K,V> newLiveMap(final Set<K> backEnd,final Function<K,V> fun) { return new HashMap<K,V>(){ @Override public void clear() { backEnd.clear(); } @Override public boolean containsKey(Object key) { return backEnd.contains(key); } @Override public boolean isEmpty() { return backEnd.isEmpty(); } @Override public V put(K key, V value) { backEnd.add(key); return null; } @Override public boolean containsValue(Object value) { for(K s:backEnd) if(fun.apply(s).equals(value)) return true; return false; } @Override public V remove(Object key) { backEnd.remove(key); return null; } @Override public int size() { return backEnd.size(); } @Override public V get(Object key) { return fun.apply((K)key); } @Override public String toString() { StringBuilder b=new StringBuilder(); Iterator<K> itr=backEnd.iterator(); b.append("{"); if(itr.hasNext()) { K key=itr.next(); b.append(key); b.append(":"); b.append(this.get(key)); while(itr.hasNext()) { key=itr.next(); b.append(", "); b.append(key); b.append(":"); b.append(this.get(key)); } } b.append("}"); return b.toString(); } }; } } The implementation is incomplete, and overridden functions are not tested, but I hope that it conveys the idea.
UPDATE:
I made a small change to the seanizer so that the changes made on the map are reflected in also install.
public class SetBackedMap<K, V> extends AbstractMap<K, V> implements SetFunctionMap<K, V>{ public class MapEntry implements Entry<K, V>{ private final K key; public MapEntry(final K key){ this.key = key; } @Override public K getKey(){ return this.key; } @Override public V getValue(){ V value = SetBackedMap.this.cache.get(this.key); if(value == null){ value = SetBackedMap.this.funk.apply(this.key); SetBackedMap.this.cache.put(this.key, value); } return value; } @Override public V setValue(final V value){ throw new UnsupportedOperationException(); } } public class EntrySet extends AbstractSet<Entry<K, V>>{ public class EntryIterator implements Iterator<Entry<K, V>>{ private final Iterator<K> inner; public EntryIterator(){ this.inner = EntrySet.this.keys.iterator(); } @Override public boolean hasNext(){ return this.inner.hasNext(); } @Override public Map.Entry<K, V> next(){ final K key = this.inner.next(); return new MapEntry(key); } @Override public void remove(){ throw new UnsupportedOperationException(); } } private final Set<K> keys; public EntrySet(final Set<K> keys){ this.keys = keys; } @Override public boolean add(Entry<K, V> e) { return keys.add(e.getKey()); } @Override public Iterator<Map.Entry<K, V>> iterator(){ return new EntryIterator(); } @Override public int size(){ return this.keys.size(); } @Override public boolean remove(Object o) { return keys.remove(o); } } private final WeakHashMap<K, V> cache; private final Set<Entry<K, V>> entries; private final Function<K, V> funk; public SetBackedMap(final Set<K> keys, final Function<K, V> funk){ this.funk = funk; this.cache = new WeakHashMap<K, V>(); this.entries = new EntrySet(keys); } @Override public Set<Map.Entry<K, V>> entrySet(){ return this.entries; } public boolean putKey(K key){ return entries.add(new MapEntry(key)); } @Override public boolean removeKey(K key) { cache.remove(key); return entries.remove(key); } } SetFunctionMap Interface:
public interface SetFunctionMap<K,V> extends Map<K, V>{ public boolean putKey(K key); public boolean removeKey(K key); } Test code:
public class SetBackedMapTst { public static void main(String[] args) { Set<Integer> set=new TreeSet<Integer>(Arrays.asList( 1, 2, 4, 8, 16)); final SetFunctionMap<Integer, String> map = new SetBackedMap<Integer, String>(set, new Function<Integer, String>(){ @Override public String apply(final Integer from){ return Integer.toBinaryString(from.intValue()); } }); set.add(222); System.out.println("Map: "+map); System.out.println("Set: "+set); map.putKey(112); System.out.println("Map: "+map); System.out.println("Set: "+set); map.removeKey(112); System.out.println("Map: "+map); System.out.println("Set: "+set); } } Output:
Map: {1=1, 2=10, 4=100, 8=1000, 16=10000, 222=11011110}//change to set reflected in map Set: [1, 2, 4, 8, 16, 222] Map: {1=1, 2=10, 4=100, 8=1000, 16=10000, 112=1110000, 222=11011110} Set: [1, 2, 4, 8, 16, 112, 222]//change to map reflected in set Map: {1=1, 2=10, 4=100, 8=1000, 16=10000, 222=11011110} Set: [1, 2, 4, 8, 16, 222]//change to map reflected in set