Guava: Set + Function = Map?

前端 未结 6 1130
眼角桃花
眼角桃花 2021-02-05 01:40

Is there an idiomatic way to take a Set and a Function, and get a Map live view? (i.e. the Map

6条回答
  •  醉酒成梦
    2021-02-05 02:44

    Creating a Map from a Set and a Function

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

    Where to put this code?

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

    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 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 funk;
    
        public SetBackedMap(
            final Set keys, Function 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
    

    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 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 funk;
        private final Set keys;
    
        public MutableSetBackedMap(final Set keys,
            final Function 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}
    

提交回复
热议问题