views:

280

answers:

5

Is there an idiomatic way to take a Set<K> and a Function<K,V>, and get a Map<K,V> live view? (i.e. the Map is backed by the Set and Function combo, and if e.g. an element is added to the Set, then the corresponding entry also exists in the Map).

(see e.g. Collections2.filter for more discussion on live views)


What if a live view is not needed? Is there something 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;
}
+1  A: 

It's not in yet, but is being tracked here:

http://code.google.com/p/guava-libraries/issues/detail?id=56

Dave L.
And it has been for about two years, so don't expect it to happen any time soon...
seanizer
+1  A: 

I don't know if this is what you mean by live view.Any way here is my try.

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 not complete and the overridden functions are not tested but I hope it convey's the idea.

UPDATE:

I made some small change's to seanizer's answer so that the changes made in map will reflect in the set also.

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);
    }


}

Interface SetFunctionMap:

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 
Emil
@seanizer: By manually do you mean the function put(key,value) ? But this function doesn't work.it throws unsupported exception.So the only way of mapping the key is using the function.
Emil
@Emil I misunderstood you. wait a few minutes, I am working on a better answer.
seanizer
@Emil have a look at my own updated answer for a similar version
seanizer
+8  A: 

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<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);

Side note: If guava were my library, I'd make them accessible 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 have implemented this as a one-way view:

  • Changes to the set are reflected in the map, but not vice-versa (and you can't change the map anyway, the put(key, value) method isn't implemented).
  • The entrySet() iterator uses the set iterator internally, so it will also inherit the internal iterator's handling of ConcurrentModificationException.
  • Both put(k,v) and entrySet().iterator().remove() will throw UnsupportedOperationException.
  • Values are cached in a 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<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 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<K, V> extends Map<K, V>{
    boolean removeKey(final K key);
    boolean addKey(final K key);
}

Map 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}
seanizer
+1 good implementation.
Emil
I would just change the `final Function<K, V> funk` for a `final Function<? super K, V> funk`, but that's a nice implementation indeed :)
Colin Hebert
@Colin true, but then it would have to be `final Function<? super K, ? extends V> funk` to be even more precise :-). I updated my code with this version.
seanizer
@seanizer:I think it's good if can override the toString ,so that you can print the map directly.
Emil
@Emil obviously, but `hashCode()`, `equals()` and `toString()` are already provided by `AbstractMap`.
seanizer
@seanizer:Actually what i meant is it's good if we can print the map like this {1:1, 2:10 ,4:100...} instead of [1,2,4..] which is current output for toString
Emil
@Emil that's what AbstractMap does. Here's the output of `map.toString()` using the map from the example above: `{1=1, 2=10, 4=100, 8=1000, 16=10000, 32=100000, 64=1000000, 128=10000000, 256=100000000}`
seanizer
how do you get the string you are talking about?
seanizer
@seanizer: Sorry ,It's my mistake.I was foolishly printing the the set instead of the map.
Emil
@emil thought so :-)
seanizer
@seanizer: Sorry to disturb you again.I made some changes in your code to reflect changes in map to the set.Please check it.
Emil
@emil I don't think that's a good idea, it would break this map's contract.
seanizer
@seanizer: Sorry,I'm not much aware about these stuff's.Can you explain what is the map's contract ?
Emil
@Emil I misunderstood your code. Here's an updated version
seanizer
@seanizer:"a variation of Emil's variation of my solution",I liked that one.
Emil
+4  A: 

For the non live view 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 said in the comments, I have to extends another class so I just implemented Map, that's why there is so much code.

There is a totally useless (or not ?) feature that allows you to change the converter on the fly.

Colin Hebert
A: 

what about Maps.uniqueIndex()

Mairbek
That's backwards. The OP wants the value to be computed, not the key.
Mark Peters