2023-08-17  阅读(440)
原文作者:LifeIsForSharing 原文地址:https://solang.blog.csdn.net/article/details/105101316

Collections集合

Guava对JDK集合生态系统的扩展。这些是Guava中最成熟,最受欢迎的部分。

不可变集合,用于防御性编程,固定集合和提高效率。
新的集合类型,适用于JDK集合无法充分解决的用例:多重集合[multisets],多重映射[multimaps],表[tables],双向映射[bidirectional maps]等。
强大的集合工具,用于java.util.Collections中未提供的常见操作。
扩展工具,写一个Collection装饰器?实现Iterator迭代器?我们可以使其变得更容易。

1.不可变集合

1.1示例

    public static final ImmutableSet<String> COLOR_NAMES = ImmutableSet.of(
      "red",
      "orange",
      "yellow",
      "green",
      "blue",
      "purple");
    
    class Foo {
      final ImmutableSet<Bar> bars;
      Foo(Set<Bar> bars) {
        this.bars = ImmutableSet.copyOf(bars); // defensive copy!
      }
    }

1.2为什么使用

不可变对象具有许多优点,包括:

  • 可供不受信任的库安全使用。
  • 线程安全:可以被许多线程使用,而不会出现竞争状况。
  • 不需要支持变动,并且可以节省时间和空间。所有不可变的集合实现比其可变的同级具有更高的内存效率。(分析)
  • 可以作常量使用,它将保持不变。

制作对象的不变副本是一种很好的防御性编程技术。Guava提供每种标准Collection类型的简单易用的不可变版本,包括Guava自己的Collection变体。

JDK提供了Collections.unmodifiableXXX方法,但我们认为,这些方法是

  • 笨拙而冗长;在你想制作防御性副本的任何地方不能愉快使用
  • 不安全:只有不存在对原始集合的引用的情况下,返回的集合才是真正不变的
  • 低效:其数据结构仍然具有可变集合的所有开销,包括并发修改检查,哈希表中的额外空间等。

当你不希望修改集合或希望集合保持不变时,最好将其防御性地复制到不可变的集合中。

重要说明: 每个Guava不可变集合实现都拒绝null空值。我们对Google的内部代码库进行了详尽的研究,结果表明,集合中大约有5%的时间允许使用null元素,而其他95%的案例最好通过快速执行null失败来解决。如果需要使用null空值,考虑在允许空值的集合实现中使用Collections.unmodifiableList及其它用法。可以在此处找到更详细的建议。

1.3怎么用

ImmutableXXX集合可以通过几种方式创建:

  • 使用copyOf方法,例如ImmutableSet.copyOf(set)
  • 使用of方法,例如ImmutableSet.of("a", "b", "c")ImmutableMap.of("a", 1, "b", 2)
  • 例如,使用Builder构建器
    public static final ImmutableSet<Color> GOOGLE_COLORS =
       ImmutableSet.<Color>builder()
           .addAll(WEBSAFE_COLORS)
           .add(new Color(0, 191, 255))
           .build();

除有序集合外,元素 顺序从构建时开始保留 。例如,

    ImmutableSet.of("a", "b", "c", "a", "d", "b")

将按照"a", “b”, “c”, "d"的顺序遍历其元素。

1.3.1copyOf比想象的更智能

这很有用去记住ImmutableXXX.copyOf会在安全的情况下尝试避免复制数据——确切的细节未指定,但实现通常是“智能”的。例如:

    ImmutableSet<String> foobar = ImmutableSet.of("foo", "bar", "baz");
    thingamajig(foobar);
    
    void thingamajig(Collection<String> collection) {
       ImmutableList<String> defensiveCopy = ImmutableList.copyOf(collection);
       ...
    }

在此代码中,ImmutableList.copyOf(foobar)足够智能,只需返回foobar.asList(),这是ImmutableSet的恒定时间视图。

作为一般启发式方法,如果出现以下情况,则ImmutableXXX.copyOf(ImmutableCollection)会尝试避免线性时间复制:

  • 可以在恒定时间内使用底层数据结构。例如,ImmutableSet.copyOf(ImmutableList)不能在固定时间内完成。
  • 它不会导致内存泄漏——例如,如果你有ImmutableList<String> hugeList,并且执行了ImmutableList.copyOf(hugeList.subList(0, 10)),则将执行显式复制,以避免意外持有不需要的hugeList中的引用。
  • 它不会改变语义——因此ImmutableSet.copyOf(myImmutableSortedSet)将执行显式复制,因为ImmutableSet使用的hashCode()equals具有与ImmutableSortedSet基于比较器的行为不同的语义。

这有助于最大程度地降低良好的防御性编程风格的性能开销。

1.3.2asList

所有不可变集合都通过asList()提供一个ImmutableList视图,因此——例如——即使你将数据存储为ImmutableSortedSet,也可以使用sortedSet.asList().get(k)获得第k个最小的元素。

返回的ImmutableList通常——并非总是但通常——是恒定开销的视图,而不是显式副本。就是说,它通常比一般List列表更智能——例如,它将使用支持集合的高效contains包含方法。

1.4细节

接口 JDKorGuava? 不可变版本
Collection JDK ImmutableCollection
List JDK ImmutableList
Set JDK ImmutableSet
SortedSet/NavigableSet JDK ImmutableSortedSet
Map JDK ImmutableMap
SortedMap JDK ImmutableSortedMap
Multiset Guava ImmutableMultiset
SortedMultiset Guava ImmutableSortedMultiset
Multimap Guava ImmutableMultimap
ListMultimap Guava ImmutableListMultimap
SetMultimap Guava ImmutableSetMultimap
BiMap Guava ImmutableBiMap
ClassToInstanceMap Guava ImmutableClassToInstanceMap
Table Guava ImmutableTable

2.新集合类型

Guava引入了许多新的不在JDK中的集合类型,但是我们发现它们非常有用。所有这些都旨在与JDK集合框架愉快地共存,而不会在JDK集合抽象中产生麻烦。

通常,Guava集合实现非常精确地遵循JDK接口协定。

2.1Multiset多重集合

传统的Java习惯用法,例如计算一个单词在文档中出现了多少次是这样的:

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

这很笨拙,容易出错,并且不支持收集各种有用的统计信息,例如单词总数。我们可以做得更好。

Guava提供了一个新的集合类型Multiset,它支持添加多个元素。维基百科在数学中将多集合定义为:“集合概念的概括,其中元素成员可以多次出现…在多重集合中,与集合[set]相同和元组[tuples]相反,元素的顺序无关紧要:多重集合{a, a, b}和{a, b, a}是相等的。”

有两种主要的查看方式:

  • 就像ArrayList<E>一样,没有排序约束:排序无关紧要。
  • 这就像具有元素和计数的Map<E, Integer>

Guava的Multiset API结合了这两种查看Multiset的方式,如下所示:

  • 当被当成普通Collection时,Multiset的行为非常类似于无序ArrayList

    • 调用add(E)将添加给定元素一次。
    • Multiset的iterator()对每个元素的每次出现进行遍历。
    • Multiset的size()是所有元素的所有出现次数的总数。
  • 其它查询操作以及性能特征都与对Map<E, Integer>的期望一样。

    • count(Object)返回与该元素关联的计数。对于HashMultiset,计数为O(1),对于TreeMultiset,计数为O(log n),依此类推。
    • entrySet()返回Set<Multiset.Entry<E>>,其作用类似于MapentrySet
    • elementSet()返回多重集合的不同元素的Set<E>,就像MapkeySet()一样。
    • Multiset实现的内存消耗在不同元素的数量上是线性的。

值得注意的是,Multiset完全符合Collection接口的约定,除非在少数情况下JDK本身有先例——特别是TreeMultisetTreeSet一样,使用比较进行相等性而不是Object.equals。特别是,Multiset.addAll(Collection)每次出现时都会在Collection中添加每个元素一次,这比上面Map方法所需的for循环方便得多。

方法 描述
count(E) 计算已添加到此多集合的元素的出现次数。
elementSet() 以Set<E>的形式查看Multiset<E>的不同元素。
entrySet() 与Map.entrySet()类似,返回Set<Multiset.Entry<E>>,其中包含支持getElement()和getCount()的条目
add(E,int) 添加指定元素的指定出现次数。
remove(E,int) 删除指定元素的指定出现次数。
setCount(E,int) 将指定元素的出现次数设置为指定的非负值。
size() 返回Multiset中所有元素的出现总数。

2.1.1Multiset不是Map

请注意,Multiset<E>不是Map<E, Integer>,尽管它可能是Multiset实现的一部分。多重集合Multiset是真正的集合Collection类型,并且满足所有相关的合同义务。其它显著差异包括:

  • Multiset<E>的元素仅具有正计数。没有元素可以具有负计数,并且计数为0的值被认为不在多重集合中。它们不会出现在elementSet()entrySet()视图中。
  • multiset.size()返回集合的大小,该大小等于所有元素的计数之和。对于不同元素的数量,请使用elementSet().size()。(因此,例如,add(E)multiset.size()增加1。)
  • multiset.iterator()遍历每个元素的每次出现,因此迭代的长度等于multiset.size()
  • Multiset<E>支持添加元素,删除元素或直接设置元素次数。setCount(elem, 0)等效于删除所有出现的元素。
  • 不在多重集合中的元素的multiset.count(elem)始终返回0

2.1.2实现

Guava提供了许多Multiset的实现,这些实现大致对应于JDK map的实现。

Map 对应的Multiset 是否支持null元素
HashMap HashMultiset Yes
TreeMap TreeMultiset Yes
LinkedHashMap LinkedHashMultiset Yes
ConcurrentHashMap ConcurrentHashMultiset No
ImmutableMap ImmutableMultiset No

2.1.3SortedMultiset

SortedMultisetMultiset接口上的一种变体,它支持高效地提取指定区间内的子集。例如,你可以使用latencies.subMultiset(0, BoundType.CLOSED, 100, BoundType.OPEN).size()来确定你的网站在100ms延迟内的匹配数,然后将其与latencies.size()进行比较确定整体比例。

TreeMultiset实现SortedMultiset接口。在撰写本文时,仍在测试ImmutableSortedMultiset的GWT兼容性。

2.2Multimap多重映射

每个有经验的Java程序员都在某一点上实现了Map<K, List<V>>Map<K, Set<V>>,并处理了该结构的笨拙。例如,Map<K, Set<V>>是表示未标记有向图的典型方法。Guava的Multimap框架使处理键到多个值的映射变得容易。Multimap是将键与任意多个值相关联的一般方法。

有两种方法可以从概念上考虑Multimap:作为从单个键到单个值的映射的集合:

    a -> 1
    a -> 2
    a -> 4
    b -> 3
    c -> 5

或作为从唯一键到值的集合的映射:

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

通常,最好从第一个视图的角度考虑Multimap接口,但允许以另一种方式查看它,使用asMap()视图返回的Map<K, Collection<V>>。最重要的是,不存在映射到空集合的键:一个键要么映射至少一个值,要么根本不存在于Multimap中。

但是,很少直接使用Multimap接口。通常,将使用ListMultimapSetMultimap,它们分别将键映射到ListSet

2.2.1构造

创建Multimap的最直接的方法是使用MultimapBuilder,它允许你配置键和值的表示方式。例如:

    // creates a ListMultimap with tree keys and array list values
    ListMultimap<String, Integer> treeListMultimap =
        MultimapBuilder.treeKeys().arrayListValues().build();
    
    // creates a SetMultimap with hash keys and enum set values
    SetMultimap<Integer, MyEnum> hashEnumMultimap =
        MultimapBuilder.hashKeys().enumSetValues(MyEnum.class).build();

你也可以选择直接在实现类上使用create()方法,但是不建议使用MultimapBuilder

2.2.2修改

Multimap.get(key)返回与指定键关联的值的视图,即使当前没有。对于ListMultimap,它返回一个List,对于SetMultimap,它返回一个Set

修改会写入底层Multimap。例如,

    Set<Person> aliceChildren = childrenMultimap.get(alice);
    aliceChildren.clear();
    aliceChildren.add(bob);
    aliceChildren.add(carol);

写入底层多重映射。

修改多重映射(更直接)的其它方法包括:

方法签名 描述 等价
put(K,V) 将键与值添加一个关联。 multimap.get(key).add(value)
putAll(K,Iterable) 从键到每个值依次添加关联。 Iterables.addAll(multimap.get(key),values)
remove(K,V) 从键key到值value中删除一个关联,并且如果多重映射集合更改了,则返回true。 multimap.get(key).remove(value)
removeAll(K) 删除并返回与指定键关联的所有值。返回的集合可能可以修改,也可能无法修改,但是对其进行修改不会影响多重映射。(返回适当的集合类型。) multimap.get(key).clear()
replaceValues(K,Iterable) 清除与键key关联的所有值,并将键key设置为与每个值values关联。返回先前与该键关联的值。 multimap.get(key).clear();Iterables.addAll(multimap.get(key),values)

2.2.3视图

Multimap还支持许多强大的视图。

  • asMap将任何Multimap<K, V>视为Map<K, Collection<V>>。返回的map映射支持remove,并且对返回的集合所做的更改均会写入,但是该map映射不支持putputAll。至关重要的是,当你要在不存在的键上使用null而不是一个新的,可写的空集合时,可以使用asMap().get(key)。(你可以并且应该将asMap.get(key)强制转换为适当的集合类型——SetMultimapSetListMultimapList——但类型系统这里不允许ListMultimap返回Map<K, List<V>>。)
  • entriesMultimap中的所有条目视为Collection<Map.Entry<K, V>>。(对于SetMultimap,这是一个Set。)
  • keySetMultimap中的不同键视为一个Set
  • keysMultimap的键视为一个Multiset,其多重性等于与该键关联的值的数量。元素可以从Multiset中删除,但不能添加;更改将写入。
  • values()Multimap中的所有值视为“扁平”的Collection<V>,全部当成一个集合。这类似于Iterables.concat(multimap.asMap().values()),但返回完整的Collection

2.2.4Multimap不是Map

Multimap<K, V>不是Map<K, Collection<V>>,尽管这样的map映射可能在Multimap实现中使用。显著差异包括:

  • Multimap.get(key)始终返回一个非null,可能为空的集合。这并不意味着多重映射会花费与该键关联的任何内存,而是返回的集合是一个视图,该视图允许你根据需要添加与键的关联。
  • 如果你更喜欢类似Map的行为——不在多重映射中的键返回null,请使用asMap()视图获取Map<K, Collection<V>>。(或者,要从ListMultimap获取Map<K, List<V>>,使用静态Multimaps.asMap()方法。SetMultimapSortedSetMultimap也存在类似的方法。)
  • 当且仅当与指定键有相关联的任何元素时,Multimap.containsKey(key)才为true。特别的,如果键k先前与一个或多个值相关联,此后已从多重映射移除,则Multimap.containsKey(k)将返回false。
  • Multimap.entries()返回Multimap中所有键的所有条目。如果要所有键->集合的条目,请使用asMap().entrySet()
  • Multimap.size()返回整个多重映射中的条目数,而不是不同键的数。使用Multimap.keySet().size()来获取不同键的数量。

2.2.5实现

Multimap提供了多种实现。你可以在大多数使用Map<K, Collection<V>>的地方使用它。

实现 键的行为类似… 值的行为类似…
ArrayListMultimap HashMap ArrayList
HashMultimap HashMap HashSet
LinkedListMultimap* LinkedHashMap``* LinkedList``*
LinkedHashMultimap** LinkedHashMap LinkedHashSet
TreeMultimap TreeMap TreeSet
ImmutableListMultimap ImmutableMap ImmutableList
ImmutableSetMultimap ImmutableMap ImmutableSet

除了不可变的实现之外,这些实现中的每一个都支持null的键和值。

* LinkedListMultimap.entries()保留了 非不同 键值之间的迭代顺序。有关详细信息,请参见链接。

** LinkedHashMultimap保留条目的插入顺序,键的插入顺序以及与任何一个键关联的值的集合的插入顺序。

请注意,并非所有实现都实际上与列出的实现一样作为Map<K, Collection<V>>来实现的!(特别是,一些Multimap实现使用自定义哈希表来最大程度地减少开销。)

如果需要更多自定义,请使用Multimaps.newMultimap(Map, Supplier)list列表set集合版本,以使用自定义集合、列表或集合实现来支持多重映射。

2.3BiMap双向映射

将值映射回键的传统方法是维护两个单独的映射,并使两个映射保持同步,但这容易出错,并且在映射中已经存在值时会造成极大的混乱。例如:

    Map<String, Integer> nameToId = Maps.newHashMap();
    Map<Integer, String> idToName = Maps.newHashMap();
    
    nameToId.put("Bob", 42);
    idToName.put(42, "Bob");
    // what happens if "Bob" or 42 are already present?
    // weird bugs can arise if we forget to keep these in sync...

BiMap<K, V>Map<K, V>

  • 允许使用inverse()查看“反转”的BiMap<V, K>
  • 确保值是唯一的,使values()成为Set

如果你尝试将键映射到已经存在的值,则BiMap.put(key, value)将抛出IllegalArgumentException。如果要删除具有指定值的任何现有条目,改用BiMap.forcePut(key, value)

    BiMap<String, Integer> userId = HashBiMap.create();
    ...
    
    String userForId = userId.inverse().get(id);

2.3.1实现

键-值映射实现 值-键映射实现 对应的BiMap
HashMap HashMap HashBiMap
ImmutableMap ImmutableMap ImmutableBiMap
EnumMap EnumMap EnumBiMap
EnumMap HashMap EnumHashBiMap

注意: BiMap工具,例如,synchronizedBiMap位于Maps中。

2.4Table

    Table<Vertex, Vertex, Double> weightedGraph = HashBasedTable.create();
    weightedGraph.put(v1, v2, 4);
    weightedGraph.put(v1, v3, 20);
    weightedGraph.put(v2, v3, 5);
    
    weightedGraph.row(v1); // returns a Map mapping v2 to 4, v3 to 20
    weightedGraph.column(v3); // returns a Map mapping v1 to 20, v2 to 5

通常,当你尝试一次在多个键上建立索引时,会遇到诸如Map<FirstName, Map<LastName, Person>>之类的东西,使用起来很丑陋且笨拙。Guava提供了一个新的集合类型Table,它支持任何“行”类型和“列”类型的用例。Table表支持多种视图,使你可以从任何角度使用数据,包括:

  • rowMap(),它将Table<R, C, V>视为Map<R, Map<C, V>>。同样,rowKeySet()返回Set<R>
  • row(r)返回一个非null的Map<C, V>>。写入Map将写入底层表Table
  • 提供了类似的列方法:columnMap()columnKeySet()column(c)。(基于列的访问效率比基于行的访问效率低)
  • cellSet()Table.Cell<R, C, V>的集合形式返回Table的视图。Cell单元格非常类似于Map.Entry,但是区分行键和列键。

提供了几种Table表格实现,包括:

  • HashBasedTable,本质上由HashMap<R, HashMap<C, V>>支持。
  • TreeBasedTable,本质上由TreeMap<R, TreeMap<C, V>>支持。
  • ImmutableTable
  • ArrayTable,它要求在构造时指定行和列的完整范围,但由二维数组支持,以在表密集时提高速度和内存效率。ArrayTable的工作方式与其它实现有所不同。有关详细信息,请查阅Javadoc。

2.5ClassToInstanceMap

有时,你的映射键并非全都属于同一类型:它们的键是类型,并且你想将它们映射到该类型的值。Guava为此提供了ClassToInstanceMap

除了扩展Map接口之外,ClassToInstanceMap还提供了方法 T getInstance(Class)T putInstance(Class, T),这消除了需要在强制类型安全时进行不愉快的转换。

ClassToInstanceMap只有一个类型参数,通常命名为B,代表由map映射管理的类型的上限。例如:

    ClassToInstanceMap<Number> numberDefaults = MutableClassToInstanceMap.create();
    numberDefaults.putInstance(Integer.class, Integer.valueOf(0));

从技术上讲,ClassToInstanceMap<B>实现Map<Class<? extends B>, B>——换句话说,从B的子类到B的实例的映射。这可能会使ClassToInstanceMap中涉及的泛型类型引起混乱,但是请记住,B始终是map映射中类型的上限——通常,B只是Object

Guava提供了一些有用的实现,分别命名为MutableClassToInstanceMapImmutableClassToInstanceMap

重要说明: 与任何其它Map<Class, Object>一样,ClassToInstanceMap可能包含原始类型的条目,并且原始类型及其对应的包装器类型可以映射到不同的值。

2.6RangeSet

RangeSet描述了一组不相连的非空区间。将区间添加到可变的RangeSet时,所有相连的区间都会合并在一起,而空区间将被忽略。例如:

    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.canonical(DiscreteDomain)预处理区间,例如DiscreteDomain.integers()

注意: GWT或JDK 1.5及更早版本都不支持RangeSetRangeSet需要充分利用JDK 1.6中的NavigableMap功能。

2.6.1视图

RangeSet实现支持非常广泛的视图,包括:

  • complement()RangeSet的补集视图。complement也是一个RangeSet,因为它包含不相连的非空区间。
  • subRangeSet(Range<C>):返回RangeSet与指定Range的交集的视图。这归纳了传统排序集合的headSetsubSettailSet视图。
  • asRanges():将RangeSet查看为Set<Range<C>>,可以对其进行迭代。
  • asSet(DiscreteDomain) (仅ImmutableRangeSet支持):将RangeSet<C>查看为ImmutableSortedSet<C>,查看区间中的元素而不是区间本身。(如果DiscreteDomainRangeSet都没有上边界或都没有下边界,则不支持此操作。)

2.6.2查询

除了对其视图进行操作外,RangeSet还直接支持多种查询操作,其中最突出的是:

  • contains(C)RangeSet上最基本的操作,查询RangeSet中的任何区间是否包含指定的元素。
  • rangeContaining(C):返回包含指定元素的Range;如果没有,则返回null
  • encloses(Range<C>):足够直接地,测试RangeSet中的任何Range是否包含指定区间。
  • span():返回encloses包含此RangeSet中每个区间的最小Range

2.7RangeMap

RangeMap是一种集合类型,描述了从不相交的非空区间到值的映射。与RangeSet不同,RangeMap永远不会“合并”相邻的映射,即使相邻的区间映射到相同的值。例如:

    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), "foo");
    // {[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"}

2.7.1视图

RangeMap提供两种视图:

  • asMapOfRanges():将RangeMap视作Map<Range<K>, V>。例如,这可以用于迭代RangeMap
  • subRangeMap(Range<K>)会将RangeMap与指定Range的交集作为RangeMap视图。这归纳了传统的headMapsubMaptailMap操作。

3.集合工具

任何具有JDK集合框架经验的程序员都知道并喜欢java.util.Collections中提供的工具。Guava沿这些思路提供了更多工具:适用于所有集合的静态方法。这些是Guava最受欢迎和最成熟的部分。

与特定接口对应的方法以相对直观的方式进行分组:

集合接口 JDKorGuava? 对应的Guava工具类
Collection JDK Collections2
List JDK Lists
Set JDK Sets
SortedSet JDK Sets
Map JDK Maps
SortedMap JDK Maps
Queue JDK Queues
Multiset Guava Multisets
Multimap Guava Multimaps
BiMap Guava Maps
Table Guava Tables

寻找转换,过滤器之类的东西?这些东西在functional函数式语法下的函数式编程文章中。

3.1静态构造函数

在JDK 7之前,构造新的泛型集合需要令人不愉快的重复代码:

    List<TypeThatsTooLongForItsOwnGood> list = new ArrayList<TypeThatsTooLongForItsOwnGood>();

我认为我们都可以同意这是令人不愉快的。 Guava提供了使用泛型来推断右侧类型的静态方法:

    List<TypeThatsTooLongForItsOwnGood> list = Lists.newArrayList();
    Map<KeyType, LongishValueType> map = Maps.newLinkedHashMap();

可以肯定的是,JDK 7中的菱形运算符使此工作不再那么麻烦:

    List<TypeThatsTooLongForItsOwnGood> list = new ArrayList<>();

但是Guava的发展远不止于此。使用工厂方法模式,我们可以非常方便地使用其起始元素初始化集合。

    Set<Type> copySet = Sets.newHashSet(elements);
    List<String> theseElements = Lists.newArrayList("alpha", "beta", "gamma");

此外,借助命名工厂方法的功能(Effective Java项1),我们可以提高将集合初始化大小的可读性:

    List<Type> exactly100 = Lists.newArrayListWithCapacity(100);
    List<Type> approx100 = Lists.newArrayListWithExpectedSize(100);
    Set<Type> approx100Set = Sets.newHashSetWithExpectedSize(100);

下面列出了提供的确切静态工厂方法及其相应的工具类。

注意: Guava引入的新集合类型不会暴露原始构造函数,也不会在工具类中包含初始化程序。相反,它们直接暴露静态工厂方法,例如:

    Multiset<String> multiset = HashMultiset.create();

3.2Iterables

可能情况下,Guava都倾向于提供接受Iterable而不是Collection的工具。在Google,遇到实际上没有存储在主存储器中的“集合”并不少见,而是从数据库或另一个数据中心收集的,并且如果不实际获取所有元素就无法支持size()之类的操作。

因此,可以在Iterables中找到你可能希望看到的所有集合都支持的许多操作。此外,大多数Iterators方法在Iterators中都有一个对应的版本,可以接受原始的Iterator。

Iterables类中的绝大多数操作都是懒惰的:它们仅在绝对必要时才推进支持迭代。本身返回Iterables的方法将返回延迟计算的视图,而不是在内存中显式构造一个集合。

从Guava 12开始,FluentIterable类对Iterables进行了补充,该类包装了Iterable,并为其中的许多操作提供了“流利”的语法。

以下是一些最常用的工具的选择,尽管在Guava functional idioms中讨论了Iterables中许多更“函数式”的方法。

3.2.1常规方法

方法 描述 参见
concat(Iterable) 返回几个iterable的串联的惰性视图。 concat(Iterable...)
frequency(Iterable,Object) 返回对象的出现次数。 CompareCollections.frequency(Collection,Object);seeMultiset
partition(Iterable,int) 返回分区为指定大小的块的不可修改的iterable视图 Lists.partition(List,int),paddedPartition(Iterable,int)
getFirst(Iterable,Tdefault) 返回iterable的第一个元素,如果为空,则返回默认值。 CompareIterable.iterator().next(),FluentIterable.first()
getLast(Iterable) 返回iterable的最后一个元素,如果为空则快速失败,并显示NoSuchElementException。 getLast(Iterable,Tdefault),FluentIterable.last()
elementsEqual(Iterable,Iterable) 如果iterable具有相同顺序的相同元素,则返回true。 CompareList.equals(Object)
unmodifiableIterable(Iterable) 返回不可修改的iterable视图。 CompareCollections.unmodifiableCollection(Collection)
limit(Iterable,int) 返回最多返回指定数量的元素的Iterable。 FluentIterable.limit(int)
getOnlyElement(Iterable) 返回Iterable中的唯一元素。如果iterable为空或包含多个元素,则会快速失败。 getOnlyElement(Iterable,Tdefault)
    Iterable<Integer> concatenated = Iterables.concat(
      Ints.asList(1, 2, 3),
      Ints.asList(4, 5, 6));
    // concatenated has elements 1, 2, 3, 4, 5, 6
    
    String lastAdded = Iterables.getLast(myLinkedHashSet);
    
    String theElement = Iterables.getOnlyElement(thisSetIsDefinitelyASingleton);
      // if this set isn't a singleton, something is wrong!

3.2.2与Collection类似

通常,集合在其它集合上天然支持这些操作,但对iterables不提供支持。

当输入实际上是Collection时,这些操作中的每一个都委托给相应的Collection接口方法。例如,如果将Iterables.size传递给Collection,它将调用Collection.size方法,而不是遍历迭代器。

方法 类似的Collection方法 等价的FluentIterable
addAll(CollectionaddTo,IterabletoAdd) Collection.addAll(Collection)
contains(Iterable,Object) Collection.contains(Object) FluentIterable.contains(Object)
removeAll(IterableremoveFrom,CollectiontoRemove) Collection.removeAll(Collection)
retainAll(IterableremoveFrom,CollectiontoRetain) Collection.retainAll(Collection)
size(Iterable) Collection.size() FluentIterable.size()
toArray(Iterable,Class) Collection.toArray(T[]) FluentIterable.toArray(Class)
isEmpty(Iterable) Collection.isEmpty() FluentIterable.isEmpty()
get(Iterable,int) List.get(int) FluentIterable.get(int)
toString(Iterable) Collection.toString() FluentIterable.toString()

3.2.3FluentIterable

除了上面介绍的方法以及在函数式语法的functional中,FluentIterable还提供了一些方便的方法来复制到不可变的集合中:

结果类型 方法
ImmutableList toImmutableList()
ImmutableSet toImmutableSet()
ImmutableSortedSet toImmutableSortedSet(Comparator)

3.2.4Lists

除了静态构造函数方法和函数编程方法外,Lists还为List对象提供了许多有价值的工具方法。

方法 描述
partition(List,int) 返回基础列表的视图,该视图分为指定大小的块。
reverse(List) 返回指定列表的反向视图。注意:如果列表是不可变的,请考虑使用ImmutableList.reverse()。
    List<Integer> countUp = Ints.asList(1, 2, 3, 4, 5);
    List<Integer> countDown = Lists.reverse(theList); // {5, 4, 3, 2, 1}
    
    List<List<Integer>> parts = Lists.partition(countUp, 2); // {{1, 2}, {3, 4}, {5}}

3.2.5静态工厂方法

Lists提供了以下静态工厂方法:

实现 工厂
ArrayList basic,withelements,fromIterable,withexactcapacity,withexpectedsize,fromIterator
LinkedList basic,fromIterable

3.3Sets

Sets工具类包括许多好用的方法。

3.3.1集合理论运算

我们提供了一些标准的集合理论运算,作为参数集的视图来实现。这些返回一个SetView,可以使用:

  • 直接作为Set,因为它实现了Set接口
  • 通过使用copyInto(Set)将其复制到另一个可变集合中
  • 通过使用immutableCopy()制作不可变的副本
方法
union(Set,Set)
intersection(Set,Set)
difference(Set,Set)
symmetricDifference(Set,Set)

例如:

    Set<String> wordsWithPrimeLength = ImmutableSet.of("one", "two", "three", "six", "seven", "eight");
    Set<String> primes = ImmutableSet.of("two", "three", "five", "seven");
    
    SetView<String> intersection = Sets.intersection(primes, wordsWithPrimeLength); // contains "two", "three", "seven"
    // I can use intersection as a Set directly, but copying it can be more efficient if I use it a lot.
    return intersection.immutableCopy();

3.3.2其它Set工具

方法 描述 参见
cartesianProduct(List) 返回每个可能的列表,可以通过从每个集合中选择一个元素来获得。 cartesianProduct(Set...)
powerSet(Set) 返回指定集合的子集。
    Set<String> animals = ImmutableSet.of("gerbil", "hamster");
    Set<String> fruits = ImmutableSet.of("apple", "orange", "banana");
    
    Set<List<String>> product = Sets.cartesianProduct(animals, fruits);
    // {{"gerbil", "apple"}, {"gerbil", "orange"}, {"gerbil", "banana"},
    //  {"hamster", "apple"}, {"hamster", "orange"}, {"hamster", "banana"}}
    
    Set<Set<String>> animalSets = Sets.powerSet(animals);
    // {{}, {"gerbil"}, {"hamster"}, {"gerbil", "hamster"}}

3.3.3静态工厂方法

Sets提供以下静态工厂方法:

实现 工厂方法
HashSet basic,withelements,fromIterable,withexpectedsize,fromIterator
LinkedHashSet basic,fromIterable,withexpectedsize
TreeSet basic,withComparator,fromIterable

3.4Maps

Maps有许多很酷的工具,值得单独解释。

3.4.1uniqueIndex

Maps.uniqueIndex(Iterable, Function)解决了这样的常见情况:一堆对象每个都有一些独特的属性,并且希望能够基于该属性查找那些对象。

假设我们有一堆字符串,我们知道它们具有唯一的长度,并且我们希望能够查找具有特定长度的字符串。

    ImmutableMap<Integer, String> stringsByIndex = Maps.uniqueIndex(strings, new Function<String, Integer> () {
        public Integer apply(String string) {
          return string.length();
        }
      });

如果索引不是唯一的,请参见下面的Multimaps.index

3.4.2difference

Maps.difference(Map, Map)允许你比较两个map之间的所有差异。它返回一个MapDifference对象,该对象将维恩图分解为:

方法 描述
entriesInCommon() 这两个映射中的条目均具有匹配的键和值。
entriesDiffering() 条目具有相同的键,但值不同。此映射中的值的类型为MapDifference.ValueDifference,可让你查看左侧和右侧的值。
entriesOnlyOnLeft() 返回其键在左侧但不在右侧映射中的条目。
entriesOnlyOnRight() 返回其键在右侧但不在左侧映射中的条目。
    Map<String, Integer> left = ImmutableMap.of("a", 1, "b", 2, "c", 3);
    Map<String, Integer> right = ImmutableMap.of("b", 2, "c", 4, "d", 5);
    MapDifference<String, Integer> diff = Maps.difference(left, right);
    
    diff.entriesInCommon(); // {"b" => 2}
    diff.entriesDiffering(); // {"c" => (3, 4)}
    diff.entriesOnlyOnLeft(); // {"a" => 1}
    diff.entriesOnlyOnRight(); // {"d" => 5}

3.4.3BiMap工具

BiMap上的Guava工具位于Maps类中,因为BiMap也是Map

BiMap工具 相应的Map工具
synchronizedBiMap(BiMap) Collections.synchronizedMap(Map)
unmodifiableBiMap(BiMap) Collections.unmodifiableMap(Map)

3.4.3.1静态工厂方法

Maps提供以下静态工厂方法。

实现 工厂方法
HashMap basic,fromMap,withexpectedsize
LinkedHashMap basic,fromMap
TreeMap basic,fromComparator,fromSortedMap
EnumMap fromClass,fromMap
ConcurrentMap basic
IdentityHashMap basic

3.5Multisets

标准Collection操作(例如containsAll)会忽略多重集合中元素的数量,而只关心元素是否在多重集合中。Multisets提供了许多操作,这些操作考虑了多重集合中元素的重复性。

方法 说明 和Collection方法的区别
containsOccurrences(Multisetsup,Multisetsub) 如果所有o的sub.count(o)<=super.count(o)则返回true。 Collection.containsAll忽略计数,仅测试是否包含元素。
removeOccurrences(MultisetremoveFrom,MultisettoRemove) 对于toRemove中元素的每次出现,都删除removeFrom中的一个出现。 Collection.removeAll删除所有元素的所有出现,即使出现在toRemove中也是如此。
retainOccurrences(MultisetremoveFrom,MultisettoRetain) 保证所有o的removeFrom.count(o)<=toRetain.count(o). Collection.retainAll保留所有元素的所有出现,即使出现在toRetain中也是如此。
intersection(Multiset,Multiset) 返回两个多重集合交集的视图;一种非破坏性的方法来retainOccurrences保留出现次数。 没有类似物。
    Multiset<String> multiset1 = HashMultiset.create();
    multiset1.add("a", 2);
    
    Multiset<String> multiset2 = HashMultiset.create();
    multiset2.add("a", 5);
    
    multiset1.containsAll(multiset2); // returns true: all unique elements are contained,
      // even though multiset1.count("a") == 2 < multiset2.count("a") == 5
    Multisets.containsOccurrences(multiset1, multiset2); // returns false
    
    multiset2.removeOccurrences(multiset1); // multiset2 now contains 3 occurrences of "a"
    
    multiset2.removeAll(multiset1); // removes all occurrences of "a" from multiset2, even though multiset1.count("a") == 2
    multiset2.isEmpty(); // returns true

Multisets中的其它工具包括:

方法 描述
copyHighestCountFirst(Multiset) 返回按频率降序遍历元素的多重集合的不变副本。
unmodifiableMultiset(Multiset) 返回多重集合的不可修改视图。
unmodifiableSortedMultiset(SortedMultiset) 返回排序后的多重集合的不可修改视图。
    Multiset<String> multiset = HashMultiset.create();
    multiset.add("a", 3);
    multiset.add("b", 5);
    multiset.add("c", 1);
    
    ImmutableMultiset<String> highestCountFirst = Multisets.copyHighestCountFirst(multiset);
    
    // highestCountFirst, like its entrySet and elementSet, iterates over the elements in order {"b", "a", "c"}

3.6Multimaps

Multimaps提供了一些常规工具操作,值得单独解释。

3.6.1index

当你希望能够查找具有某些共同特定属性(不一定是唯一属性)的所有对象时,Maps.uniqueIndexMultimaps.index(Iterable, Function)的表亲回答了这种情况。

假设我们要根据字符串的长度对其进行分组。

    ImmutableSet<String> digits = ImmutableSet.of(
        "zero", "one", "two", "three", "four",
        "five", "six", "seven", "eight", "nine");
    Function<String, Integer> lengthFunction = new Function<String, Integer>() {
      public Integer apply(String string) {
        return string.length();
      }
    };
    ImmutableListMultimap<Integer, String> digitsByLength = Multimaps.index(digits, lengthFunction);
    /*
     * digitsByLength maps:
     *  3 => {"one", "two", "six"}
     *  4 => {"zero", "four", "five", "nine"}
     *  5 => {"three", "seven", "eight"}
     */

3.6.2invertFrom

由于Multimap可以将多个键映射到一个值,而一个键则映射到多个值,因此反转Multimap很有用。Guava提供了invertFrom(Multimap toInvert, Multimap dest),可让你执行此操作,而无需为你选择实现。

注意: 如果使用的是ImmutableMultimap,考虑使用ImmutableMultimap.inverse()

    ArrayListMultimap<String, Integer> multimap = ArrayListMultimap.create();
    multimap.putAll("b", Ints.asList(2, 4, 6));
    multimap.putAll("a", Ints.asList(4, 2, 1));
    multimap.putAll("c", Ints.asList(2, 5, 3));
    
    TreeMultimap<Integer, String> inverse = Multimaps.invertFrom(multimap, TreeMultimap.<String, Integer> create());
    // note that we choose the implementation, so if we use a TreeMultimap, we get results in order
    /*
     * inverse maps:
     *  1 => {"a"}
     *  2 => {"a", "b", "c"}
     *  3 => {"c"}
     *  4 => {"a", "b"}
     *  5 => {"c"}
     *  6 => {"b"}
     */

3.6.3forMap

是否需要在Map上使用Multimap方法?forMap(Map)Map视为SetMultimap。例如,与Multimaps.invertFrom结合使用时,此功能特别有用。

    Map<String, Integer> map = ImmutableMap.of("a", 1, "b", 1, "c", 2);
    SetMultimap<String, Integer> multimap = Multimaps.forMap(map);
    // multimap maps ["a" => {1}, "b" => {1}, "c" => {2}]
    Multimap<Integer, String> inverse = Multimaps.invertFrom(multimap, HashMultimap.<Integer, String> create());
    // inverse maps [1 => {"a", "b"}, 2 => {"c"}]

3.6.4包装器

Multimaps提供了传统的包装方法,以及用于根据你选择的MapCollection实现获取自定义Multimap实现的工具。

Multimap类型 不可变包装 同步包装 自定义包装
Multimap unmodifiableMultimap synchronizedMultimap newMultimap
ListMultimap unmodifiableListMultimap synchronizedListMultimap newListMultimap
SetMultimap unmodifiableSetMultimap synchronizedSetMultimap newSetMultimap
SortedSetMultimap unmodifiableSortedSetMultimap synchronizedSortedSetMultimap newSortedSetMultimap

自定义Multimap实现可让你指定应在返回的Multimap中使用特定实现。注意事项包括:

  • 多重映射假定对map和工厂返回的列表拥有完全所有权。这些对象不应手动更新,提供时应为空,并且不应使用软引用、弱引用或虚引用。
  • 修改Multimap后, 无法保证 Map的内容是什么样的。
  • 当任何并发操作更新多重映射时,即使map和工厂生成的实例都是多重映射,也不是线程安全的。但是,并发读操作将正常运行。如有必要,请使用synchronized同步包装器解决此问题。
  • 如果map、工厂、工厂生成的列表以及多重集合的内容都可序列化,则多重集合可序列化。
  • Multimap.get(key)返回的集合与Supplier供应商返回的集合类型不同,虽然供应商返回RandomAccess列表,但Multimap.get(key)返回的列表也将是随机访问。

请注意,自定义的Multimap方法期望使用Supplier参数来生成新的新集合。这是一个写ListMultimap的示例,该列表支持由TreeMap映射到LinkedList

    ListMultimap<String, Integer> myMultimap = Multimaps.newListMultimap(
      Maps.<String, Collection<Integer>>newTreeMap(),
      new Supplier<LinkedList<Integer>>() {
        public LinkedList<Integer> get() {
          return Lists.newLinkedList();
        }
      });

3.7Tables

Tables类提供了一些方便的工具。

3.7.1customTable

Multimaps.newXXXMultimap(Map, Supplier)工具相比,Tables.newCustomTable(Map, Supplier)允许你使用所需的任何行或列映射来指定Table实现。

    // use LinkedHashMaps instead of HashMaps
    Table<String, Character, Integer> table = Tables.newCustomTable(
      Maps.<String, Map<Character, Integer>>newLinkedHashMap(),
      new Supplier<Map<Character, Integer>> () {
        public Map<Character, Integer> get() {
          return Maps.newLinkedHashMap();
        }
      });

3.7.2transpose

使用transpose(Table)方法可以将Table<R, C, V>视为Table<C, R, V>

3.7.3包装器

这些是你熟悉和喜爱的不可修改的包装器。但是,请考虑在大多数情况下改用ImmutableTable

4.扩展工具

4.1简介

有时你需要编写自己的集合扩展。也许你希望在将元素添加到列表中时添加特殊的行为,或者你想要编写实际上由数据库查询支持的Iterable。Guava提供了许多工具,可以使你和我们都更轻松地完成这些任务。(毕竟,我们自己负责扩展集合框架。)

4.2Forwarding Decorators

对于所有各种集合接口,Guava提供了Forwarding抽象类以简化使用装饰器模式decorator pattern

Forwarding类定义了一个抽象方法delegate(),你应该重写该抽象方法以返回装饰的对象。其他每个方法都直接委托给委托:因此,例如,ForwardingList.get(int)可以简单地实现为delegate().get(int)

通过将ForwardingXXX子类化并实现delegate()方法,你可以仅覆盖目标类中的选定方法,从而添加修饰的功能,而不必亲自委派每个方法。

此外,许多方法都有standardMethod实现,可用于恢复预期的行为,并提供与以下方法相同的好处:扩展AbstractList或在JDK中其他框架类。

让我们做一个例子。假设你要装饰一个List,以便它记录添加到其中的所有元素。当然,无论使用哪种方法添加元素——add(int, E)add(E)addAll(Collection),我们都希望记录——所以我们必须重写所有这些方法。

    class AddLoggingList<E> extends ForwardingList<E> {
      final List<E> delegate; // backing list
      @Override protected List<E> delegate() {
        return delegate;
      }
      @Override public void add(int index, E elem) {
        log(index, elem);
        super.add(index, elem);
      }
      @Override public boolean add(E elem) {
        return standardAdd(elem); // implements in terms of add(int, E)
      }
      @Override public boolean addAll(Collection<? extends E> c) {
        return standardAddAll(c); // implements in terms of add
      }
    }

请记住,默认情况下,所有方法都直接转发给委托,因此,覆盖ForwardingMap.put不会更改ForwardingMap.putAll的行为。小心重写必须更改其行为的每种方法,并确保修饰后的集合满足其约定。

通常,由抽象集合框架(如AbstractList)提供的大多数方法也将作为Forwarding实现中的standard标准实现。

提供特殊视图的接口有时会提供这些视图的Standard标准实现。例如,ForwardingMap提供了StandardKeySetStandardValuesStandardEntrySet类,每一个类都尽可能将其方法委托给经过修饰的map,否则,它们将留下不能作为抽象被委托的方法。

接口 Forwarding装饰器
Collection ForwardingCollection
List ForwardingList
Set ForwardingSet
SortedSet ForwardingSortedSet
Map ForwardingMap
SortedMap ForwardingSortedMap
ConcurrentMap ForwardingConcurrentMap
Map.Entry ForwardingMapEntry
Queue ForwardingQueue
Iterator ForwardingIterator
ListIterator ForwardingListIterator
Multiset ForwardingMultiset
Multimap ForwardingMultimap
ListMultimap ForwardingListMultimap
SetMultimap ForwardingSetMultimap

4.3PeekingIterator

有时,普通的Iterator接口还不够。

Iterators迭代器支持Iterators.peekingIterator(Iterator)方法,该方法包装一个Iterator并返回PeekingIterator,它是Iterator的子类型,可让你peek()窥视下一次调用next()将返回的元素。

注意: Iterators.peekingIterator返回的PeekingIterator不支持peek()之后调用remove()

让我们举个例子:在删除连续重复元素的同时复制列表List

    List<E> result = Lists.newArrayList();
    PeekingIterator<E> iter = Iterators.peekingIterator(source.iterator());
    while (iter.hasNext()) {
      E current = iter.next();
      while (iter.hasNext() && iter.peek().equals(current)) {
        // skip this duplicate element
        iter.next();
      }
      result.add(current);
    }

传统的方式是记录先前的元素,并在某些条件下回退,但这是一项棘手且容易出错的业务。PeekingIterator相对容易理解和使用。

4.4AbstractIterator

实现自己的IteratorAbstractIterator可以使你的生活更轻松。

举个例子最容易解释。假设我们要包装一个iterator迭代器,以跳过空值。

    public static Iterator<String> skipNulls(final Iterator<String> in) {
      return new AbstractIterator<String>() {
        protected String computeNext() {
          while (in.hasNext()) {
            String s = in.next();
            if (s != null) {
              return s;
            }
          }
          return endOfData();
        }
      };
    }

你实现一个方法computeNext(),该方法仅计算下一个值。完成序列后,只需返回endOfData()即可标记迭代结束。

注意: AbstractIterator扩展了UnmodifiableIterator,它禁止实现remove()。如果需要支持remove()的迭代器,则不应扩展AbstractIterator

4.4.1AbstractSequentialIterator

一些迭代器更容易以其他方式表示。AbstractSequentialIterator提供了另一种表示迭代的方式。

    Iterator<Integer> powersOfTwo = new AbstractSequentialIterator<Integer>(1) { // note the initial value!
      protected Integer computeNext(Integer previous) {
        return (previous == 1 << 30) ? null : previous * 2;
      }
    };

在这里,我们实现了computeNext(T)方法,该方法接受previous的值作为参数。

请注意,你还必须另外传递一个初始值;如果迭代器应立即结束,则应传递null
请注意,computeNext假设null值表示迭代结束——AbstractSequentialIterator不能用于实现可能返回null的迭代器。

本文参考:
Immutable collections
New collection types
Powerful collection utilities
Extension utilities
guava-tests-collect

阅读全文
  • 点赞