Is there an idiomatic way to take a Set
and a Function
, and get a Map
live view? (i.e. the Map
Here are two classes that should each do the job. The first just shows a map view of the set, while the second can write values back to the set through a special interface.
Call Syntax:
Map immutable = new SetBackedMap(Set keys, Function func);
Map mutable = new MutableSetBackedMap(Set keys, Function func);
Side note: If guava were my library, I'd make them accessible through the Maps class:
Map immutable = Maps.immutableComputingMap(Set keys, Function func);
Map mutable = Maps.mutableComputingMap(Set keys, Function func);
I have implemented this as a one-way view:
put(key, value)
method isn't implemented).entrySet()
iterator uses the
set iterator internally, so it will
also inherit the internal iterator's
handling of
ConcurrentModificationException
.put(k,v)
and
entrySet().iterator().remove()
will
throw
UnsupportedOperationException
.WeakHashMap
,
with no special concurrency handling, i.e. there is no synchronization at
any level. This will do for most cases, but if your function is expensive, you might want to add some locking.Code:
public class SetBackedMap extends AbstractMap{
private class MapEntry implements Entry{
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>{
public class EntryIterator implements Iterator>{
private final Iterator inner;
public EntryIterator(){
this.inner = EntrySet.this.keys.iterator();
}
@Override
public boolean hasNext(){
return this.inner.hasNext();
}
@Override
public Map.Entry next(){
final K key = this.inner.next();
return new MapEntry(key);
}
@Override
public void remove(){
throw new UnsupportedOperationException();
}
}
private final Set keys;
public EntrySet(final Set keys){
this.keys = keys;
}
@Override
public Iterator> iterator(){
return new EntryIterator();
}
@Override
public int size(){
return this.keys.size();
}
}
private final WeakHashMap cache;
private final Set> entries;
private final Function super K, ? extends V> funk;
public SetBackedMap(
final Set keys, Function super K, ? extends V> funk){
this.funk = funk;
this.cache = new WeakHashMap();
this.entries = new EntrySet(keys);
}
@Override
public Set> entrySet(){
return this.entries;
}
}
Test:
final Map map =
new SetBackedMap(
new TreeSet(Arrays.asList(
1, 2, 4, 8, 16, 32, 64, 128, 256)),
new Function(){
@Override
public String apply(final Integer from){
return Integer.toBinaryString(from.intValue());
}
});
for(final Map.Entry 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
While I think it's a good idea to make this one-way, here's a version for Emil that provides a two-way view (it's a variation of Emil's variation of my solution :-)). It requires an extended map interface that I'll call ComputingMap
to make clear that this is a map where it doesn't make sense to call put(key, value)
.
Map interface:
public interface ComputingMap extends Map{
boolean removeKey(final K key);
boolean addKey(final K key);
}
Map implementation:
public class MutableSetBackedMap extends AbstractMap implements
ComputingMap{
public class MapEntry implements Entry{
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>{
public class EntryIterator implements Iterator>{
private final Iterator inner;
public EntryIterator(){
this.inner = MutableSetBackedMap.this.keys.iterator();
}
@Override
public boolean hasNext(){
return this.inner.hasNext();
}
@Override
public Map.Entry next(){
final K key = this.inner.next();
return new MapEntry(key);
}
@Override
public void remove(){
throw new UnsupportedOperationException();
}
}
public EntrySet(){
}
@Override
public Iterator> iterator(){
return new EntryIterator();
}
@Override
public int size(){
return MutableSetBackedMap.this.keys.size();
}
}
private final WeakHashMap cache;
private final Set> entries;
private final Function super K, ? extends V> funk;
private final Set keys;
public MutableSetBackedMap(final Set keys,
final Function super K, ? extends V> funk){
this.keys = keys;
this.funk = funk;
this.cache = new WeakHashMap();
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> entrySet(){
return this.entries;
}
}
Test:
public static void main(final String[] args){
final ComputingMap map =
new MutableSetBackedMap(
new TreeSet(Arrays.asList(
1, 2, 4, 8, 16, 32, 64, 128, 256)),
new Function(){
@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}