Guava:Set<K> + Function<K, V> = Map<K, V>?

33

有没有一种惯用的方式可以获取Set<K>Function<K,V>,并获得Map<K,V>实时视图?(即MapSetFunction组合支持,例如如果向Set添加元素,则相应的条目也存在于Map中)。

(有关实时视图的更多讨论,请参见Collections2.filter


如果不需要实时视图,有没有比这更好的选择呢?
请注意,我已经按照要求进行了翻译。
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;
}

“Live view” 是什么意思? - jjnguy
3
啊,听起来这可能非常有用。 - jjnguy
1
稍微改进一下就是使用Maps.newHashMapWithExpectedSize(keys.size())。 - Dave L.
还有一个Maps.transformEntries(),它是实时的,但不是正确的签名。 - skaffman
这个有什么用途? - ColinD
1
Guava现在提供了Maps.asMap函数... - Louis Wasserman
6个回答

29

从集合和函数创建地图

这里有两个类,每个类都可以完成相应的工作。第一个只显示了集合的地图视图,而第二个可以通过特殊接口将值写回到集合中。

调用语法:

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

这段代码应该放在哪里?

附注:如果Guava是我的库,我会通过Maps类来使它们可访问:

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

不可变版本:

我把这个实现成了一个单向视图:

  • 对集合的更改会反映在映射中,但反过来则不行(而且你无法更改映射,因为put(key, value)方法未实现)。
  • entrySet()迭代器在内部使用集合迭代器,因此它也会继承内部迭代器对ConcurrentModificationException的处理方式。
  • put(k,v)entrySet().iterator().remove()都会抛出UnsupportedOperationException异常。
  • 值存储在一个WeakHashMap中,并没有进行特殊的并发处理,即任何级别都没有同步。这对大多数情况足够好,但如果您的函数很耗时,您可能需要添加一些锁定。

代码:

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

}

测试:

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

输出:

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

可变版本:

尽管我认为将其设计为单向的是个好主意,但这里有一个供Emil使用的双向视图版本(它是我的解决方案的Emil变体的变体 :-))。它需要扩展地图接口,我将其称为ComputingMap以明确表明这是一个不应该调用put(key, value)的地图。

地图接口:

public interface ComputingMap<K, V> extends Map<K, V>{
    boolean removeKey(final K key);
    boolean addKey(final K key);
}

Map 实现:

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

}

测试:

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

输出:

{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}

@EmilµśŠńäČ’╝īõĮåµś»hashCode()ŃĆüequals()ÕÆītoString()ÕĘ▓ń╗Åńö▒AbstractMapµÅÉõŠøŃĆé - Sean Patrick Floyd
@Emil 这就是 AbstractMap 的作用。这是使用上面示例中的 map 执行 map.toString() 的输出结果:{1=1, 2=10, 4=100, 8=1000, 16=10000, 32=100000, 64=1000000, 128=10000000, 256=100000000} - Sean Patrick Floyd
@Sean Patrick Floyd:Olivier Cailloux(请参见他下面的答案:https://dev59.com/IW865IYBdhLWcg3wU9CI#6834655)指出Map.Entry实现缺少适当的equals和hashCode实现。 - Christian Semrau
我认为地图也可以支持常规的删除方法(通过实现EntryIterator.remove,或者更好地使用真正的实现)。它将执行与removeKey相同的操作。 - Paŭlo Ebermann
@PaŭloEbermann 这些天我会写得不同,没错 :-) - Sean Patrick Floyd
显示剩余10条评论

22

注意。Sean Patrick Floyd的回答虽然非常有用,但有一个缺陷。一个很简单的问题,但我调试了一段时间,所以不要掉入同样的陷阱:MapEntry类需要实现equals和hashcode方法。这是我的实现(直接从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());
}

这个回复最好作为相关答案的评论,但我理解似乎没有权限发表评论(或者没有找到该如何发表评论!)。


1
+1:确实,按照Map.Entry的契约,这两种方法是必需的。(而且您目前缺乏StackOverflow“声望”来发布评论。通过编写好的答案,您可以增加“声望”并解锁此网站的许多功能。) - Christian Semrau
我花了3年时间才看到这个答案 :-) 当然,作为一个经验法则:任何在SO上看到的代码都不应被视为完整的生产就绪解决方案,即使它有时包含有价值的想法。 - Sean Patrick Floyd

14

1
已经有大约两年了,所以不要指望它很快会发生... - Sean Patrick Floyd
1
不是很快,但看起来它最终会出现在 Guava 14 中(在 @Beta 版本中)。 - Ted M. Young

5

对于非实时视图,代码存在于lambdaJ中,使用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);

我尝试了自己的实现:http://ideone.com/Kkpcn 正如评论中所说,我必须扩展另一个类,因此我只实现了Map,这就是为什么有这么多代码的原因。
还有一个完全没有用处(或者不是?)的功能,允许您随时更改转换器。

2

3
那是相反的。楼主希望计算数值而不是键名。 - Mark Peters

1

我不知道你所说的实时视图是否是这个意思。无论如何,这是我的尝试。

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

这个实现并不完整,覆盖的函数也没有经过测试,但我希望它能传达出想法。

更新:

我对seanizer答案进行了一些小改动,以便在映射中进行的更改也会反映在集合中。

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:

public interface SetFunctionMap<K,V> extends Map<K, V>{
     public boolean putKey(K key);
     public boolean removeKey(K key);
}

测试代码:

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

}
}

输出:

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 

@seanizer:您的手动操作是否指的是put(key,value)函数?但是这个函数不起作用,它会抛出不支持的异常。因此,映射键的唯一方法是使用该函数。 - Emil
我误解了你。请等几分钟,我正在努力提供更好的答案。 - Sean Patrick Floyd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接