在日常开发中我们通常有需要对 List 容器进行分组的情况,比如对下面的list数据根据name字段来进行分组:

[
    {
        "date":"2018-01-31",
        "name":"wuzhong",
        "socre":0.8
    },
    {
        "date":"2018-01-30",
        "name":"wuzhong",
        "socre":0.9
    },
    {
        "date":"2018-01-31",
        "name":"wuzhong2",
        "socre":0.8
    }
]

通常我们的做法可能很自然的想到 Map<String,List> 的结构,比如代码如下:

Map<String,List<Item>> map = new HashMap<>();
for (Item item : list){
  List<Item> tmp = map.get(item.getName());
  if (null == tmp){
      tmp = new ArrayList<>();
      map.put(item.getName(),tmp);
  }
  tmp.add(item);
}

很简单, 但是代码量有点多,特别是需要判断List为null并初始化。

再用guava实现上述的功能:

Multimap<String,Item> multiMap = ArrayListMultimap.create();
for (Item item : list){
multiMap.put(item.getName(),item);
}
代码量直接减少了一半...

怎么实现的
我们直接跟着 ArrayListMultimap 的源码进去,发现其父类和我们最初的设计一样,也是用了 Map<K, Collection> 作为数据的容器,但是多了一个 totalSize 的字段。

abstract class AbstractMapBasedMultimap<K, V> extends AbstractMultimap<K, V>
    implements Serializable {
  private transient Map<K, Collection<V>> map;
  private transient int totalSize;

接着我们继续去看put方法的具体实现。

public boolean put(@Nullable K key, @Nullable V value) {
  Collection<V> collection = map.get(key);
  if (collection == null) {
    collection = createCollection(key);
    if (collection.add(value)) {
      totalSize++;
      map.put(key, collection);
      return true;
    } else {
      throw new AssertionError("New Collection violated the Collection spec");
    }
  } else if (collection.add(value)) {
    totalSize++;
    return true;
  } else {
    return false;
  }
}

它主要做了2件事:

初始化容器,并将元素添加到容器里
维护 totalSize
这样我们再调用 multimap.size()的方法直接就返回了,不需要再次遍历和统计的过程。

疑问
multimap 里 public List get(@Nullable K key) 这个方法返回的是个List容器,如果我们直接对他操作,是不是也会影响totalsize呢?

Collection<Item> wuzhong2 = multiMap.get("wuzhong2");
wuzhong2.clear();
System.out.println(multiMap.size());     //输出2
System.out.println(multiMap.keySet());   //输出 wuzhong

结果是显而易见的,对guava返回的容器进行的操作的确是会影响它的宿主对象的。

具体的源码可以看下 com.google.common.collect.AbstractMapBasedMultimap.WrappedList ,他用了代理模式,底层还是用了一个 Collection 容器。

private class WrappedCollection extends AbstractCollection<V> {
    final K key;
    Collection<V> delegate;
    final WrappedCollection ancestor;
    final Collection<V> ancestorDelegate;
    
    public void clear() {
      int oldSize = size(); // calls refreshIfEmpty
      if (oldSize == 0) {
        return;
      }
      delegate.clear();
      totalSize -= oldSize;    //维护实时的totalsize
      removeIfEmpty(); //      //维护keyset,及时删除
    }
    

Multimap的实现类

  Multimap提供了丰富的实现,所以你可以用它来替代程序里的Map<K, Collection>,具体的实现如下:

|content1|content2|content3|
  |Implementation | Keys 的行为类似 |    Values的行为类似
|-------|-------|-------|
|  ArrayListMultimap | HashMap |   ArrayList
|  HashMultimap | HashMap |    HashSet
|  LinkedListMultimap | LinkedHashMap* | LinkedList*
|  LinkedHashMultimap | LinkedHashMap | LinkedHashSet
|  TreeMultimap | TreeMap | TreeSet
|  ImmutableListMultimap | ImmutableMap | ImmutableList
 | ImmutableSetMultimap | ImmutableMap | ImmutableSet  

  以上这些实现,除了immutable的实现都支持null的键和值。

  1、LinkedListMultimap.entries()能维持迭代时的顺序。

  2、LinkedHashMultimap维持插入的顺序,以及键的插入顺序。
总结
multimap 整体上是对java底层api的二次封装,很好的处理了各种细节,比如子容器的判空处理,totalsize的计算效率, keys 的维护等 。 在接口的易用性上也非常贴合开发者。

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议

弱小和无知不是生存的障碍,傲慢才是。