Guava(3) Collection Types

2.New Collection Types

guava定义了一些JDK中没有的,但是比较常用的集合结构。

Multiset,Multimap,BiMap,Table,ClassToInstanceMap,RangeSet,RangeMap。

2.1Multiset

一个带有计数、元素可重复、无序的set。

假设如下场景,对一篇文章中出现的单词进行计数。

List<String> words = Lists.newArrayList("Potter", "Potter", "Potter");
Map<String, Integer> counts = new HashMap<String, Integer>();
for (String word : words) {
    Integer count = counts.get(word);
    if (count == null) {
        counts.put(word, 1);
    } else {
        counts.put(word, count + 1);
    }
}

使用Multiset就可以简化实现。

List<String> words = Lists.newArrayList("Potter", "Potter", "Potter");
Multiset<String> countSet = HashMultiset.create();
for (String word : words) {
    countSet.add(word);
}

即使是使用Map.merge简化,Multiset依然更加简洁。

List<String> words = Lists.newArrayList("Potter", "Potter", "Potter");
// map
Map<String, Integer> counts = new HashMap<String, Integer>();
for (String word : words) {
    counts.merge(word, 1, Integer::sum);
}
// multiset
Multiset<String> countSet = HashMultiset.create();
countSet.addAll(words);

实际上Multiset具有两面性:

  • 一个无序的ArrayList

add()添加元素,可迭代操作每个元素,size()获取的是所有出现次数的总数。

  • 一个带有计数的集合Map<E, Integer>

count(object)获取出现次数,entrySet()类似于Map的entrySet,elementSet()类似于Map的keySet()。

一个更具体的例子如下所示。

Multiset<String> bookStore = HashMultiset.create();
bookStore.add("Potter");
bookStore.add("Potter");
bookStore.add("Potter");
System.out.println(bookStore.contains("Potter")); // true
System.out.println(bookStore.count("Potter")); // 3

bookStore.remove("Potter");
System.out.println(bookStore.contains("Potter")); // true
System.out.println(bookStore.count("Potter")); // 2
bookStore.setCount("Potter", 5);
System.out.println(String.join(",", bookStore)); // Potter...[5]

for (Multiset.Entry<String> bookEntry : bookStore.entrySet()) {
    System.out.println(bookEntry.getElement()); // Potter
    System.out.println(bookEntry.getCount()); // 5
}

System.out.println(bookStore.elementSet()); // [Potter]

虽然Multiset具有Map的特征,但是它不是一个Map,它继承的是Collection接口。

@GwtCompatible
public interface Multiset<E> extends Collection<E> {
  ...
}

除此之外,还有如下区别:

  • Multiset的元素count只能大于0,count为0的元素不会出现在elementSet()和entrySet()中。

  • Multiset的size()指的是所有元素的count的和,如果要查看distinct数量,使用elementSet().size()。

  • Multiset的iterator()迭代的是所有出现,也就是iterator的长度是size()。

  • Multiset支持直接设置元素的count,setCount(elem, 0)代表清空该元素的所有出现。

  • Multiset的count()对不存在的元素返回值为0。

通过SortedMultiset可以提取Multiset指定范围内的元素。

SortedMultiset<String> words = TreeMultiset.create();
words.add("a");
words.add("b");
words.add("c");
words.add("d");
SortedMultiset<String> queryWords = words.subMultiset("b", BoundType.CLOSED, "d", BoundType.OPEN);
System.out.println(queryWords); // b c

guava提供了一些Multiset的实现,大致与JDK中的map实现对应。

Map Corresponding Multiset Supports null elements
HashMap HashMultiset Yes
TreeMap TreeMultiset Yes
LinkedHashMap LinkedHashMultiset Yes
ConcurrentHashMap ConcurrentHashMultiset No
ImmutableMap ImmutableMultiset No

2.2Multimap

使用Multimap可以更简单地处理如下数据结构。

Map<K, List<V>>
Map<K, Set<V>>

这种数据结构可能需要以下两种表现形式,Multimap通过asMap()可以支持这两种视图。

// 1-1
a -> 1
a -> 2
a -> 4
b -> 3
c -> 5
// 1-N
a -> [1, 2, 4]
b -> [3]
c -> [5]

构建一个Multimap如下,tree map和hash map的简单区别就是元素的有序和无序。

// tree map
ListMultimap<String, Integer> treeListMultimap = MultimapBuilder.treeKeys().arrayListValues().build();
treeListMultimap.put("euclid", 4); // [4]
List<Integer> valueList = Lists.newArrayList(1, 2, 3, 2);
treeListMultimap.putAll("euclid", valueList); // [4, 1, 2, 3, 2]
List<Integer> list = treeListMultimap.get("euclid");
list.add(5); // [4, 1, 2, 3, 2, 5]
list.remove(0); // [1, 2, 3, 2, 5]
treeListMultimap.remove("euclid", 2); // [1, 3, 2, 5]
List<Integer> replaceList = Lists.newArrayList(8, 9, 10);
treeListMultimap.replaceValues("euclid", replaceList); // [7, 8, 9]
treeListMultimap.removeAll("euclid"); 
List<Integer> emptyList = treeListMultimap.get("euclid"); // []
boolean exists = treeListMultimap.containsKey("euclid"); //false
// hash map
SetMultimap<Integer, MyEnum> hashEnumMultimap =
        MultimapBuilder.hashKeys().enumSetValues(MyEnum.class).build();
hashEnumMultimap.put(0, MyEnum.DONE);

与Multiset一样,如果集合必须包含至少一个元素,否则视为不存在于Multimap中。

同样的,Multimap虽然具备Map的一些特征,比如entries、keySet、keys、values等,但是它也不是一个Map,它并没有继承Map接口。

@GwtCompatible
public interface Multimap<K, V> {} 

更具体的差异描述如下:

Multimap.containsKey仅在集合对象中至少包含一个元素的情况下为true,当value为null或者空集合时视为false;

Multimap.entries的返回值是1-1的键值对,如下所示;

ListMultimap<String, Integer> treeListMultimap = MultimapBuilder.treeKeys().arrayListValues().build();
treeListMultimap.put("euclid", 1);
treeListMultimap.put("euclid", 2);
treeListMultimap.put("tulip", 3);
for (Map.Entry<String, Integer> entry : treeListMultimap.entries()) {
    System.out.println(entry.getKey());
    System.out.println(entry.getValue());
} 
// euclid 1 euclid 2 tulip 3

Multimap.size的返回值是entries的数量,并非keySet().size。

2.3BiMap

key和value相互索引,都是唯一的。

Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("euclid", 1);
BiMap<String, Integer> map = HashBiMap.create(hashMap);

BiMap<String, Integer> map = HashBiMap.create();
map.put("euclid", 1);
map.inverse().get(1); // euclid
map.forcePut("euclid", 2);
map.inverse().get(1); // null

常用的其他实现如下所示:

Key-Value Map Impl Value-Key Map Impl Corresponding BiMap
HashMap HashMap HashBiMap
ImmutableMap ImmutableMap ImmutableBiMap
EnumMap EnumMap EnumBiMap
EnumMap HashMap EnumHashBiMap

EnumBiMap与EnumHashBiMap的区别是,EnumBiMap要求key和value都是Enum。

EnumMap<MyEnum, String> enumStringEnumMap = new EnumMap<>(MyEnum.class);
enumStringEnumMap.put(MyEnum.TODO, "待办");
enumStringEnumMap.put(MyEnum.DONE, "完成");
EnumHashBiMap<MyEnum, String> enumBiMap = EnumHashBiMap.create(enumStringEnumMap);

EnumMap<MyEnum, Foo> enumStringEnumMap = new EnumMap<>(MyEnum.class);
enumStringEnumMap.put(MyEnum.TODO, Foo.EUCLID);
enumStringEnumMap.put(MyEnum.DONE, Foo.POISSION);
EnumBiMap<MyEnum, Foo> enumBiMap = EnumBiMap.create(enumStringEnumMap);

2.4Table

需要使用两个主键索引元素时,可以替换掉Map实现,比较简洁。

// hash map
Map<Integer, Map<Integer, String>> map = new HashMap<>();
// hash table
Table<Integer, Integer, String> table = HashBasedTable.create();
table.put(1, 1, "1行1列");
table.put(1, 2, "1行2列");
table.put(2, 1, "2行1列");
table.put(2, 2, "2行2列");
Map<Integer, String> row = table.row(1);
Map<Integer, Map<Integer, String>> rowMap = table.rowMap();
Map<Integer, String> column = table.column(1);
for (Table.Cell<Integer, Integer, String> cell : table.cellSet()) {
    cell.getRowKey();
    cell.getColumnKey();
    cell.getValue();
}
// tree table
Table<Integer, Integer, String> table = TreeBasedTable.create();
// immutable table
Table<Integer, Integer, String> table = ImmutableTable.<Integer, Integer, String>builder()
        .put(1, 1, "1行1列")
        .build();
// array table
Table<Integer, Integer, String> arrayTable = ArrayTable.create(table);

其中ArrayTable比较特殊,先看一下HashBasedTable的实现。

@GwtCompatible(serializable = true)
public class HashBasedTable<R, C, V> extends StandardTable<R, C, V> {
  ...
  public static <R, C, V> HashBasedTable<R, C, V> create() {
    return new HashBasedTable<>(new LinkedHashMap<R, Map<C, V>>(), new Factory<C, V>(0));
  }

  HashBasedTable(Map<R, Map<C, V>> backingMap, Factory<C, V> factory) {
    super(backingMap, factory);
  }  
  ...    
}

@GwtCompatible
class StandardTable<R, C, V> extends AbstractTable<R, C, V> implements Serializable {
  @GwtTransient final Map<R, Map<C, V>> backingMap;
  @GwtTransient final Supplier<? extends Map<C, V>> factory;

  StandardTable(Map<R, Map<C, V>> backingMap, Supplier<? extends Map<C, V>> factory) {
    this.backingMap = backingMap;
    this.factory = factory;
  }
  ...
} 

可以看到HashBasedTable最终的结构依然是一个Map,包括TreeBasedTable、ImmutableTable也是类似的实现,接下来看一下但是ArrayTable。

@Beta
@GwtCompatible(emulated = true)
public final class ArrayTable<R, C, V> extends AbstractTable<R, C, V> implements Serializable {
  ...
  public static <R, C, V> ArrayTable<R, C, V> create(Table<R, C, V> table) {
    return (table instanceof ArrayTable<?, ?, ?>)
        ? new ArrayTable<R, C, V>((ArrayTable<R, C, V>) table)
        : new ArrayTable<R, C, V>(table);
  }

  private ArrayTable(Table<R, C, V> table) {
    this(table.rowKeySet(), table.columnKeySet());
    putAll(table);
  }   

  private final ImmutableList<R> rowList;
  private final ImmutableList<C> columnList;

  private final ImmutableMap<R, Integer> rowKeyToIndex;
  private final ImmutableMap<C, Integer> columnKeyToIndex;
  private final V[][] array;  

  private ArrayTable(Iterable<? extends R> rowKeys, Iterable<? extends C> columnKeys) {
    this.rowList = ImmutableList.copyOf(rowKeys);
    this.columnList = ImmutableList.copyOf(columnKeys);
    checkArgument(rowList.isEmpty() == columnList.isEmpty());

    rowKeyToIndex = Maps.indexMap(rowList);
    columnKeyToIndex = Maps.indexMap(columnList);

    @SuppressWarnings("unchecked")
    V[][] tmpArray = (V[][]) new Object[rowList.size()][columnList.size()];
    array = tmpArray;

    eraseAll();
  }  

  @Override
  public void putAll(Table<? extends R, ? extends C, ? extends V> table) {
    super.putAll(table);
  }  

  @CanIgnoreReturnValue
  @Override
  public V put(R rowKey, C columnKey, @Nullable V value) {
    checkNotNull(rowKey);
    checkNotNull(columnKey);
    Integer rowIndex = rowKeyToIndex.get(rowKey);
    checkArgument(rowIndex != null, "Row %s not in %s", rowKey, rowList);
    Integer columnIndex = columnKeyToIndex.get(columnKey);
    checkArgument(columnIndex != null, "Column %s not in %s", columnKey, columnList);
    return set(rowIndex, columnIndex, value);
  }

  @CanIgnoreReturnValue
  public V set(int rowIndex, int columnIndex, @Nullable V value) {
    // In GWT array access never throws IndexOutOfBoundsException.
    checkElementIndex(rowIndex, rowList.size());
    checkElementIndex(columnIndex, columnList.size());
    V oldValue = array[rowIndex][columnIndex];
    array[rowIndex][columnIndex] = value;
    return oldValue;
  }     
}

@GwtCompatible
abstract class AbstractTable<R, C, V> implements Table<R, C, V> {
  ...
  @Override
  public void putAll(Table<? extends R, ? extends C, ? extends V> table) {
    for (Table.Cell<? extends R, ? extends C, ? extends V> cell : table.cellSet()) {
      put(cell.getRowKey(), cell.getColumnKey(), cell.getValue());
    }
  }
  ...
}  

可以看到ArrayTable的最终的结构是一个二维数组,这也是ArrayTable要求在初始化时就指定完整数据的原因。

ArrayTable使用二维数组是为了在处理大量数据时提高运算效率。

另外在类的注解上可以看到@Beta的注解,以及该类中多处标注了todo字样,是一个还不稳定的实现。

2.5ClassToInstanceMap

有时候会需要这样一个Map,它的key就是value的类型,ClassToInstanceMap就是这样的Map。

// mutable
ClassToInstanceMap<Number> numberDefaults = MutableClassToInstanceMap.create();
numberDefaults.putInstance(Integer.class, 1);
numberDefaults.putInstance(Double.class, 1d);
numberDefaults.putInstance(BigDecimal.class, new BigDecimal("0.00001"));
Number n1 = numberDefaults.get(Integer.class);
Double n2 = numberDefaults.getInstance(Double.class);
BigDecimal n3 = numberDefaults.getInstance(BigDecimal.class);
// immutable
ClassToInstanceMap<Number> map = ImmutableClassToInstanceMap.<Number>builder()
        .put(Integer.class, 1)
        .build();

2.6RangeSet

比较方便的处理连续或不连续的区间,直接看例子。

RangeSet<Integer> rangeSet = TreeRangeSet.create();
rangeSet.add(Range.closed(1, 10)); // {[1, 10]}
rangeSet.add(Range.closedOpen(11, 15)); // disconnected range: {[1, 10], [11, 15)}
rangeSet.add(Range.closedOpen(15, 20)); // connected range; {[1, 10], [11, 20)}
rangeSet.add(Range.openClosed(0, 0)); // empty range; {[1, 10], [11, 20)}
rangeSet.remove(Range.open(5, 10)); // splits [1, 10]; {[1, 5], [10, 10], [11, 20)}

注意到Range.closed(1, 10)和Range.closedOpen(11, 15)并没有被合并为Range.closedOpen(1, 15)。

这里需要通过canonical方法先处理成连续区间后才会自动合并。

Range.closed(1, 10).canonical(DiscreteDomain.integers()); // [1, 11)

处理区间,必然要处理排序、合并、切割,这就要求操作对象是可比较的,也就是必须实现Comparable接口。

@Beta
@DoNotMock("Use ImmutableRangeSet or TreeRangeSet")
@GwtIncompatible
public interface RangeSet<C extends Comparable> {}

一些常用方法的例子。

RangeSet<Integer> c = rangeSet.complement(); // 补集
RangeSet<Integer> s = rangeSet.subRangeSet(Range.closed(6, 15)); // 交集
Set<Range<Integer>> ranges = rangeSet.asRanges();
rangeSet.span(); // 能包含set中所有range的最小range
rangeSet.encloses(Range.closed(8, 11)); // 是否包含指定集
rangeSet.contains(1); // 是否包含指定元素
rangeSet.rangeContaining(1); // 包含指定元素的集
// 不可变限定 asSet
ImmutableRangeSet<Integer> rangeSet = ImmutableRangeSet
    .<Integer>builder().add(Range.closed(1, 10)).build();
ImmutableSortedSet<Integer> set = rangeSet.asSet(DiscreteDomain.integers());

2.7RangeMap

以Range为key的Map,一些常用方法。

RangeMap<Integer, String> rangeMap = TreeRangeMap.create();
rangeMap.put(Range.closed(1, 10), "foo"); // {[1, 10] => "foo"}
rangeMap.put(Range.open(3, 6), "bar"); // {[1, 3] => "foo", (3, 6) => "bar", [6, 10] => "foo"}
rangeMap.put(Range.open(10, 20), "euclid"); // {[1, 3] => "foo", (3, 6) => "bar", [6, 10] => "foo", (10, 20) => "foo"}
rangeMap.remove(Range.closed(5, 11)); // {[1, 3] => "foo", (3, 5) => "bar", (11, 20) => "foo"}
rangeMap.get(2);
rangeMap.putCoalescing(Range.closed(20, 30), "euclid");
rangeMap.subRangeMap(Range.closed(22, 25));
Map<Range<Integer>, String> map = rangeMap.asMapOfRanges();

可以通过元素查找到值。

put和putCoalescing的区别在于,后者会查询value相同的Range,尝试将Range连接。

subRangeMap通过Range切割出部分RangeMap。