Guava的使用指南之Collections

Guava Collections可以帮助我们写出更简短精炼、可读性强的代码。

看看Guava Collections为我们做了哪些很酷的事情:

  • Immutable Collections:还在使用Collections.unmodifiableXXX()? Immutable Collections这才是真正的不可修改的集合
  • Multiset:看看如何把重复的元素放入一个集合
  • Multimaps:需要在一个key对应多个value的时候,自己写一个实现比较繁琐,让Multimaps来帮帮
  • BiMap:java.util.Map只能保证key的不重复,BiMap保证value也不重复
  • MapMaker:超级强大的Map构造类
  • Ordering class:大家知道用Comparator作为比较器来对集合排序,但是对于多关键字排序Ordering class可以简化很多的代码
  • 其他特性

当然,如果没有Guava Collections你也可以用Java Collections Framework完成上面的功能。但是Guava Collections提供的这些API经过精心设计,而且还有2500个单元测试来保障它的质量。所以我们没必要重新发明轮子。接下来我们来详细看看Guava Collections的一些具体功能。

Immutable Collections:真正的不可修改的集合

大家都用过Collections.unmodifiableXXX()来做一个不可修改的集合。例如你要构造存储常量的Set,你可以这样做:

1
2
Set<String> set = new HashSet<String>(Arrays.asList(new String[]{"RED","GREEN"}));
Set<String> unmodifiableSet = Collections.unmodifiableSet(set);

这看上去似乎不错,因为每次调unmodifiableSet.add()都会抛出一个UnsupportedOperationException。感觉安全了?慢!如果有人在原来的set上add或者remove元素会怎么样?结果unmodifiableSet也是被add或者remove元素了。而且构造这样一个简单的set写了两句长的代码。下面看看ImmutableSet是怎么来做地更安全和简洁:

1
ImmutableSet<String> immutableSet = ImmutableSet.of("RED","GREEN");

就这样一句就够了,而且试图调add方法的时候,它一样会抛出UnsupportedOperationException。重要的是代码的可读性增强了不少,非常直观的展现了代码的用意。如果像之前这个代码保护一个set怎么做呢?你可以:

1
ImmutableSet<String> immutableSet = ImmutableSet.copyOf(set);

从构造的方式来说,ImmutableSet集合还提供了Builder模式来构造一个集合:

1
2
Builder<String> builder = ImmutableSet.builder();
ImmutableSet<String> immutableSet = builder.add("RED").addAll(set).build();

在这个例子里面Builder不但能加入单个元素还能加入既有的集合。

除此之外,Guava Collections还提供了各种Immutable集合的实现:ImmutableList, ImmutableMap, ImmutableSortedSet, ImmutableSortedMap。

Multiset:把重复的元素放入集合

你可能会说这和Set接口的契约冲突,因为Set接口的javaDoc里面规定不能放入重复元素。事实上,Multiset并没有实现java.util.Set接口,它更像是一个Bag。普通的Set就像这样:[car,ship,bike],而Multiset会是这样:[carx2,shipx6,bikex3]。

譬如一个List里面有各种字符,然后你要统计每个字符串在List里面出现的次数:

1
2
3
4
5
6
7
Map<String, Integer> map = new HashMap<String, Integer>();
for(String word:wordList){
  Integer count = map.get(word);
  map.put(word, (count==null)?1:count+1);
}
//count word "the"
Integer count = map.get("the");

如果用Multiset就可以这样:

1
2
3
4
HashMultiset<String> multiSet = HashMultiset.create();
multiSet.addAll(wordList);
//count word "the"
Integer count = multiSet.count("the");

这样连循环都不用了,而且Multiset用的方法叫count,显然比在Map里面调用get有更好的可读性。Multiset还提供了setCount这样设定元素重复次数的方法,虽然你可以通过使用Map来实现类似的功能,但是程序的可读性比Multiset差了很多。

常用实现Multiset接口的类有: * HashMultiset:元素存放于HashMap * LinkedHashMultiset:元素存放于LinkedHashMap,即元素的排列顺序由第一次放入的顺序决定 * TreeMultiset:元素被排序存放于TreeMap * EnumMultiset:元素必须是enum类型 * ImmutableMultiset:不可修改的Mutiset

看到这里你可能已经发现Guava Collections都是以create或是of这样的静态方法来构造对象。这是因为这些集合类大多有多个参数的私有构造方法,由于参数数目很多,客户代码程序员使用起来就很不方便。而且以这种方式可以返回原类型的子类型对象。另外,对于创建范型对象来讲,这种方式更加简洁。

Muitimap:在Map的value里面放多个元素

Muitimap就是一个key对应多个value的数据结构。看上去它很像java.util.Map的结构,但是Muitimap不是Map,没有实现Map的接口。设想你对Map调了2次参数key一样的put方法,结果就是第2次的value覆盖了第一次的value。但是对Muitimap来说这个key同时对应了2个value。所以Map看上去是:{k1=v1,k2=v2,…},而Muitimap是:{k1=[v1,v2,v3],k2=[v7,v8],…}。

举个记名投票的例子。所有选票都放在一个List里面,List的每个元素包括投票人和选举人的名字。我们可以这样写:

1
2
3
4
5
6
7
8
9
10
//Key is cadidate name, its value is his voters
HashMap<String, HashSet<String>> hMap = new HashMap<String, HashSet<String>>();
for(Ticket ticket:tickets){
  HashSet<String> set = hMap.get(ticket.getCandidate());
  if(set==null){
      set = new HashSet<String>();
      hMap.put(ticket.getCandidate(), set);
  }
  set.add(ticket.getVoter);
}

我们再来看看Muitimap能做些什么:

1
2
3
4
HashMultimap<String, String> map = HashMultimap.create();
for(Ticket ticket:tickets){
  map.put(ticket.getCandidate(), ticket.getVoter());
}

就这么简单!

Muitimap接口的主要实现类有: * HashMultimap:key放在HashMap, 而value放在HashSet, 即一个key对应的value不可重复 * ArrayListMultimap:key放在HashMap,而value放在ArrayList,即一个key对应的value有顺序可重复 * LinkedHashMultimap:key放在LinkedHashMap,而value放在LinkedHashSet,即一个key对应的value有顺序不可重复 * TreeMultimap:key放在TreeMap,而value放在TreeSet,即一个key对应的value有排列顺序 * ImmutableMultimap:不可修改的Multimap

BiMap:双向Map

BiMap实现了java.util.Map接口。它的特点是它的value和它key一样也是不可重复的,换句话说它的key和value是等价的。如果你往BiMap的value里面放了重复的元素,就会得到IllegalArgumentException.

举个例子,你可能经常会碰到在Map里面根据value值来反推它的key值的逻辑:

1
2
3
4
5
for(Map.Entry<User, Address> entry:map.entreSet()){
  if(entry.getValue().equals(anAddress)){
      return entry.getKey();
  }
}

如果把User和Address都放在BiMap,那么一句代码就得到结果了:

1
return biMap.inverse().get(anAddress);

这里的inverse方法就是把BiMap的key集合value集合对调,因此biMap==biMap.inverse().inverse()。

BiMap的常用实现有: * HashBiMap:key集合与value集合都有HashMap实现 * EnumBiMap:key与value都必须是enum类型 * ImmutableBiMap:不可修改的BiMap

MapMaker:超级强大的Map构造工具

MapMaker是用来构造ConcurrentMap的工具类。为什么可以把MapMaker叫做超级强大?看了下面的例子你就知道了。首先,它可以用来构造ConcurrentHashMap:

1
2
//ConcurrentHashMap with concurrency level 8
ConcurrentMap<String, Object> map1 = new MapMaker().concurrencyLevel(8).makeMap();

或者构造用各种不同reference作为key和value的Map:

1
2
//ConcurrentMap with soft reference key and weak reference value
ConcurrentMap<String, Object> map2 = new MapMaker().softKeys().weakValues().makeMap();

或者构造有自动移除时间过期项的Map:

1
2
//Automatically removed entries from map after 30 seconds since they are created
ConcurrentMap<String, Object> map3 = new MapMaker().expireAfterWrite(30, TimeUnit.SECONDS).makeMap();

或者构造有最大限制数目的Map:

1
2
3
//Map size grows close to the 100, the map vill evict
//entries that are less likely to be used again
ConcurrentMap<String, Object> map4 = new MapMaker().maximumSize(100).makeMap();

或者提供当Map里面不包含所get的项,而需要自动加入到Map的功能。这个功能当Map作为缓存的时候很有用:

1
2
3
4
5
6
//Create an Object to the map, when get() is missing in map
ConcurrentMap<String, Object> map5 = new MapMaker().makeComputingMap(new Function<>(String, Object){
  public Object apply(String key){
      return createObject(key);
  }
});

这些还不是最强大的特性,最厉害的是MapMaker可以提供拥有以上所有特性的Map:

1
2
3
4
5
6
7
8
9
10
11
12
13
//Put all features together!
ConcurrentMap<String, Object> mapAll = new MapMaker()
.concurrencyLevel(8)
.softKeys()
.weakValues()
.expireAfterWrite(30, TimeUnit.SECONDS)
.makeComputingMap(
  new Function<String, Object>(){
      public Object apply(String key){
          return createObject(key);
      }
  }
);

Ordering class:灵活的多字段排序比较器

需对集合排序或者求最大值最小值,首推java.util.Collections类,但关键是要提供Comparator接口的实现。假设有个待排序的List, 而Foo里面有两个排序关键字int a, int b和int c:

1
2
3
4
5
6
7
8
Collections.sort(list, new Comparator<Foo>(){
  @Override
  public int compare(Foo f1, Foo f2){
      int resultA = f1.a-f2.a;
      int resultB = f1.b-f2.b;
      return resultA==0?(resultB==0?fl.c-f2.c:resultB):resultA;
  }
})

这看上去有点眼晕,如果用一串if-else也好不到哪里去。看看ComparisonChain能做到什么:

1
2
3
4
5
6
7
Collections.sort*(list, new Comparator<Foo>(){
  @Override
  return ComparisonChain.start()
      .compare(f1.a, f2.a)
      .compare(f1.b, f2.b)
      .compare(f1.c, f2.c).result();
});

如果排序关键字要用自定义比较器,compare方法也有接受Comparator的重载版本。譬如Foo里面每个排序关键字都已经有了各自的Comparator,那么利用ComparisonChain可以:

1
2
3
4
5
6
7
Collections.sort(list, new Comparator<Foo>(){
  @Override
  return ComparisonChain.start()
      .compare(f1.a, f2.a, comparatorA)
      .compare(f1.b, f2.b, comparatorB)
      .compare(f1.c, f2.c, comparatorC).result();
});

Ordring类还提供了一个组合Comparator对象的方法。而且Ordring本身实现了Comparator接口所以它能直接作为Comparator使用:

1
2
3
Ordering<Foo> ordering = Ordering.compound(
  Arrays.asList(ComparatorA, comparatorB, comparatorC));
Collections.sort(list, ordering);

其他特性

过滤器:利用Collection2.filter()方法过滤集合中不符合条件的元素。譬如过滤一个List里面小于10的元素:

1
2
3
4
5
6
Collection<Integer> filterCollection = Collections2.filter(list, new Predicate<Integer>(){
  @Override
  public boolean apply(Integer input){
      return input >= 10;
  }
});

当然,你可以自己写一个循环来实现这个功能,但是这样不能保证之后小于10的元素不被放入集合。filter的强大之处在于返回的filterCollection仍然有排斥小于10的元素的特性,如果调filterCollection.add(9)就会得到一个IllegalArgumentException.

转换器:利用Collections.transform方法来转换集合中的元素。譬如把一个Set里面所有元素都转化成带格式的String来产生新的Collection:

1
2
3
4
5
6
Collection<String> formatCollection = Collection2.transform(set, new Function<Integer, String>(){
  @Override
  public String apply(Integer input){
      return new DecimalFormat("#,###").format(input);
  }
})