美文网首页Java 杂谈
Guava集合[Collections]

Guava集合[Collections]

作者: 小鸡在路上 | 来源:发表于2019-03-30 17:40 被阅读4次

    2.1-不可变集合

    范例

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

    为什么要使用不可变集合

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

    • 当对象被不可信的库调用时,不可变形式是安全的;
    • 不可变对象被多个线程调用时,不存在竞态条件问题
    • 不可变集合不需要考虑变化,因此可以节省时间和空间。所有不可变的集合都比它们的可变形式有更好的内存利用率(分析和测试细节);
    • 不可变对象因为有固定不变,可以作为常量来安全使用。

    创建对象的不可变拷贝是一项很好的防御性编程技巧。Guava为所有JDK标准集合类型和Guava新集合类型都提供了简单易用的不可变版本。
    JDK也提供了Collections.unmodifiableXXX方法把集合包装为不可变形式,但我们认为不够好:

    • 笨重而且累赘:不能舒适地用在所有想做防御性拷贝的场景;
    • 不安全:要保证没人通过原集合的引用进行修改,返回的集合才是事实上不可变的;
    • 低效:包装过的集合仍然保有可变集合的开销,比如并发修改的检查、散列表的额外空间,等等。

    如果你没有修改某个集合的需求,或者希望某个集合保持不变时,把它防御性地拷贝到不可变集合是个很好的实践。

    重要提示:所有Guava不可变集合的实现都不接受null值。我们对Google内部的代码库做过详细研究,发现只有5%的情况需要在集合中允许null元素,剩下的95%场景都是遇到null值就快速失败。如果你需要在不可变集合中使用null,请使用JDK中的Collections.unmodifiableXXX方法。更多细节建议请参考“使用和避免null”

    怎么使用不可变集合

    不可变集合可以用如下多种方式创建:

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

    此外,对有序不可变集合来说,排序是在构造集合的时候完成的,如:

    ImmutableSortedSet.of("a", "b", "c", "a", "d", "b");
    

    会在构造时就把元素排序为a, b, c, d。

    比想象中更智能的copyOf

    请注意,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的常量时间复杂度的List视图。
    作为一种探索,ImmutableXXX.copyOf(ImmutableCollection)会试图对如下情况避免线性时间拷贝:

    • 在常量时间内使用底层数据结构是可能的——例如,ImmutableSet.copyOf(ImmutableList)就不能在常量时间内完成。
    • 不会造成内存泄露——例如,你有个很大的不可变集合ImmutableList<String>
      hugeList, ImmutableList.copyOf(hugeList.subList(0, 10))就会显式地拷贝,以免不必要地持有hugeList的引用。
    • 不改变语义——所以ImmutableSet.copyOf(myImmutableSortedSet)会显式地拷贝,因为和基于比较器的ImmutableSortedSet相比,ImmutableSet对hashCode()和equals有不同语义。

    在可能的情况下避免线性拷贝,可以最大限度地减少防御性编程风格所带来的性能开销。

    asList视图

    所有不可变集合都有一个asList()方法提供ImmutableList视图,来帮助你用列表形式方便地读取集合元素。例如,你可以使用sortedSet.asList().get(k)从ImmutableSortedSet中读取第k个最小元素。

    asList()返回的ImmutableList通常是——并不总是——开销稳定的视图实现,而不是简单地把元素拷贝进List。也就是说,asList返回的列表视图通常比一般的列表平均性能更好,比如,在底层集合支持的情况下,它总是使用高效的contains方法。

    细节:关联可变集合和不可变集合

    接口 JDK or Guava version
    可变集合接口 属于****JDK****还是****Guava 不可变版本
    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.2-新集合类型

    Guava引入了很多JDK没有的、但我们发现明显有用的新集合类型。这些新类型是为了和JDK集合框架共存,而没有往JDK集合抽象中硬塞其他概念。作为一般规则,Guava集合非常精准地遵循了JDK接口契约。

    Multiset

    统计一个词在文档中出现了多少次,传统的做法是这样的:

      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,它可以多次添加相等的元素。维基百科从数学角度这样定义Multiset:”集合[set]概念的延伸,它的元素可以重复出现…与集合[set]相同而与元组[tuple]相反的是,Multiset元素的顺序是无关紧要的:Multiset {a, a, b}和{a, b, a}是相等的”。——译者注:这里所说的集合[set]是数学上的概念,Multiset继承自JDK中的Collection接口,而不是Set接口,所以包含重复元素并没有违反原有的接口契约。

    可以用两种方式看待Multiset:

    • 没有元素顺序限制的ArrayList<E>
    • Map<E, Integer>,键为元素,值为计数

    Guava的Multiset API也结合考虑了这两种方式:
    当把Multiset看成普通的Collection时,它表现得就像无序的ArrayList:

    • add(E)添加单个给定元素
    • iterator()返回一个迭代器,包含Multiset的所有元素(包括重复的元素)
    • size()返回所有元素的总个数(包括重复的元素)

    当把Multiset看作Map<E, Integer>时,它也提供了符合性能期望的查询操作:

    • count(Object)返回给定元素的计数。HashMultiset.count的复杂度为O(1),TreeMultiset.count的复杂度为O(log n)。
    • entrySet()返回Set<Multiset.Entry<E>>,和Map的entrySet类似。
    • elementSet()返回所有不重复元素的Set<E>,和Map的keySet()类似。
    • 所有Multiset实现的内存消耗随着不重复元素的个数线性增长。

    值得注意的是,除了极少数情况,Multiset和JDK中原有的Collection接口契约完全一致——具体来说,TreeMultiset在判断元素是否相等时,与TreeSet一样用compare,而不是Object.equals。另外特别注意,Multiset.addAll(Collection)可以添加Collection中的所有元素并进行计数,这比用for循环往Map添加元素和计数方便多了。

    方法 描述
    count(E) 给定元素在Multiset中的计数
    elementSet() Multiset中不重复元素的集合,类型为Set<E>
    entrySet() 和Map的entrySet类似,返回Set<Multiset.Entry<E>>,其中包含的Entry支持getElement()和getCount()方法
    add(E, int) 增加给定元素在Multiset中的计数
    remove(E, int) 减少给定元素在Multiset中的计数
    setCount(E, int) 设置给定元素在Multiset中的计数,不可以为负数
    size() 返回集合元素的总个数(包括重复的元素)

    Multiset不是Map

    请注意,Multiset<E>不是Map<E, Integer>,虽然Map可能是某些Multiset实现的一部分。准确来说Multiset是一种Collection类型,并履行了Collection接口相关的契约。关于Multiset和Map的显著区别还包括:

    • Multiset中的元素计数只能是正数。任何元素的计数都不能为负,也不能是0。elementSet()和entrySet()视图中也不会有这样的元素。
    • multiset.size()返回集合的大小,等同于所有元素计数的总和。对于不重复元素的个数,应使用elementSet().size()方法。(因此,add(E)把multiset.size()增加1)
    • multiset.iterator()会迭代重复元素,因此迭代长度等于multiset.size()。
    • Multiset支持直接增加、减少或设置元素的计数。setCount(elem, 0)等同于移除所有elem。
    • 对multiset 中没有的元素,multiset.count(elem)始终返回0。

    Multiset的各种实现

    Guava提供了多种Multiset的实现,大致对应JDK中Map的各种实现:

    Map 对应的****Multiset 是否支持****null****元素
    HashMap HashMultiset
    TreeMap TreeMultiset 是(如果comparator支持的话)
    LinkedHashMap LinkedHashMultiset
    ConcurrentHashMap ConcurrentHashMultiset
    ImmutableMap ImmutableMultiset

    SortedMultiset

    SortedMultiset是Multiset 接口的变种,它支持高效地获取指定范围的子集。比方说,你可以用 latencies.subMultiset(0,BoundType.CLOSED, 100, BoundType.OPEN).size()来统计你的站点中延迟在100毫秒以内的访问,然后把这个值和latencies.size()相比,以获取这个延迟水平在总体访问中的比例。

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

    Multimap

    每个有经验的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中。

    很少会直接使用Multimap接口,更多时候你会用ListMultimap或SetMultimap接口,它们分别把键映射到List或Set。

    修改Multimap

    Multimap.get(key)以集合形式返回键所对应的值视图,即使没有任何对应的值,也会返回空集合。ListMultimap.get(key)返回List,SetMultimap.get(key)返回Set。

    对值视图集合进行的修改最终都会反映到底层的Multimap。例如:

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

    其他(更直接地)修改Multimap的方法有:

    方法签名 描述 等价于
    put(K, V) 添加键到单个值的映射 multimap.get(key).add(value)
    putAll(K, Iterable<V>) 依次添加键到多个值的映射 Iterables.addAll(multimap.get(key), values)
    remove(K, V) 移除键到值的映射;如果有这样的键值并成功移除,返回true。 multimap.get(key).remove(value)
    removeAll(K) 清除键对应的所有值,返回的集合包含所有之前映射到K的值,但修改这个集合就不会影响Multimap了。 multimap.get(key).clear()
    replaceValues(K, Iterable<V>) 清除键对应的所有值,并重新把key关联到Iterable中的每个元素。返回的集合包含所有之前映射到K的值。 multimap.get(key).clear(); Iterables.addAll(multimap.get(key), values)

    Multimap的视图

    Multimap还支持若干强大的视图:

    • asMap为Multimap<K, V>提供Map<K,Collection<V>>形式的视图。返回的Map支持remove操作,并且会反映到底层的Multimap,但它不支持put或putAll操作。更重要的是,如果你想为Multimap中没有的键返回null,而不是一个新的、可写的空集合,你就可以使用asMap().get(key)。(你可以并且应当把asMap.get(key)返回的结果转化为适当的集合类型——如SetMultimap.asMap.get(key)的结果转为Set,ListMultimap.asMap.get(key)的结果转为List——Java类型系统不允许ListMultimap直接为asMap.get(key)返回List——译者注:也可以用Multimaps中的asMap静态方法帮你完成类型转换
    • entries用Collection<Map.Entry<K, V>>返回Multimap中所有”键-单个值映射”——包括重复键。(对SetMultimap,返回的是Set)
    • keySet用Set表示Multimap中所有不同的键。
    • keys用Multiset表示Multimap中的所有键,每个键重复出现的次数等于它映射的值的个数。可以从这个Multiset中移除元素,但不能做添加操作;移除操作会反映到底层的Multimap。
    • values()用一个”扁平”的Collection<V>包含Multimap中的所有值。这有一点类似于Iterables.concat(multimap.asMap().values()),但它直接返回了单个Collection,而不像multimap.asMap().values()那样是按键区分开的Collection。

    Multimap不是Map

    Multimap<K, V>不是Map<K,Collection<V>>,虽然某些Multimap实现中可能使用了map。它们之间的显著区别包括:

    • Multimap.get(key)总是返回非null、但是可能空的集合。这并不意味着Multimap为相应的键花费内存创建了集合,而只是提供一个集合视图方便你为键增加映射值——译者注:如果有这样的键,返回的集合只是包装了Multimap中已有的集合;如果没有这样的键,返回的空集合也只是持有Multimap引用的栈对象,让你可以用来操作底层的Multimap。因此,返回的集合不会占据太多内存,数据实际上还是存放在Multimap中。
    • 如果你更喜欢像Map那样,为Multimap中没有的键返回null,请使用asMap()视图获取一个Map<K, Collection<V>>。(或者用静态方法Multimaps.asMap()为ListMultimap返回一个Map<K, List<V>>。对于SetMultimap和SortedSetMultimap,也有类似的静态方法存在)
    • 当且仅当有值映射到键时,Multimap.containsKey(key)才会返回true。尤其需要注意的是,如果键k之前映射过一个或多个值,但它们都被移除后,Multimap.containsKey(key)会返回false。
    • Multimap.entries()返回Multimap中所有”键-单个值映射”——包括重复键。如果你想要得到所有”键-值集合映射”,请使用asMap().entrySet()。
    • Multimap.size()返回所有”键-单个值映射”的个数,而非不同键的个数。要得到不同键的个数,请改用Multimap.keySet().size()。

    Multimap的各种实现

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

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

    除了两个不可变形式的实现,其他所有实现都支持null键和null值

    *LinkedListMultimap.entries()保留了所有键和值的迭代顺序。详情见doc链接。

    **LinkedHashMultimap保留了映射项的插入顺序,包括键插入的顺序,以及键映射的所有值的插入顺序。

    请注意,并非所有的Multimap都和上面列出的一样,使用Map<K, Collection<V>>来实现(特别是,一些Multimap实现用了自定义的hashTable,以最小化开销)

    如果你想要更大的定制化,请用Multimaps.newMultimap(Map, Supplier<Collection>)listset版本,使用自定义的Collection、List或Set实现Multimap。

    BiMap

    传统上,实现键值对的双向映射需要维护两个单独的map,并保持它们间的同步。但这种方式很容易出错,而且对于值已经在map中的情况,会变得非常混乱。例如:

      Map<String, Integer> nameToId = Maps.newHashMap(); 
    
      Map<Integer, String> idToName = Maps.newHashMap(); 
    
        
    
      nameToId.put("Bob", 42); 
    
      idToName.put(42, "Bob"); 
    
      //如果"Bob"和42已经在map中了,会发生什么? 
    
      //如果我们忘了同步两个map,会有诡异的bug发生... 
    

    BiMap<K, V>是特殊的Map:

    • 可以用 inverse()反转BiMap<K, V>的键值映射
    • 保证值是唯一的,因此 values()返回Set而不是普通的Collection

    在BiMap中,如果你想把键映射到已经存在的值,会抛出IllegalArgumentException异常。如果对特定值,你想要强制替换它的键,请使用 BiMap.forcePut(key, value)

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

    BiMap的各种实现

    键****–****值实现 值****–****键实现 对应的****BiMap****实现
    HashMap HashMap HashBiMap
    ImmutableMap ImmutableMap ImmutableBiMap
    EnumMap EnumMap EnumBiMap
    EnumMap HashMap EnumHashBiMap

    注:Maps类中还有一些诸如synchronizedBiMap的BiMap工具方法.

    Table

      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():用Map<R, Map<C, V>>表现Table<R, C, V>。同样的, rowKeySet()返回”行”的集合Set<R>。
    • row(r) :用Map<C, V>返回给定”行”的所有列,对这个map进行的写操作也将写入Table中。
    • 类似的列访问方法:columnMap()columnKeySet()column(c)。(基于列的访问会比基于的行访问稍微低效点)
    • cellSet():用元素类型为Table.Cell<R, C, V>的Set表现Table<R, C, V>。Cell类似于Map.Entry,但它是用行和列两个键区分的。

    Table有如下几种实现:

    • HashBasedTable:本质上用HashMap<R, HashMap<C, V>>实现;
    • TreeBasedTable:本质上用TreeMap<R, TreeMap<C,V>>实现;
    • ImmutableTable:本质上用ImmutableMap<R, ImmutableMap<C, V>>实现;注:ImmutableTable对稀疏或密集的数据集都有优化。
    • ArrayTable:要求在构造时就指定行和列的大小,本质上由一个二维数组实现,以提升访问速度和密集Table的内存利用率。ArrayTable与其他Table的工作原理有点不同,请参见Javadoc了解详情。

    ClassToInstanceMap

    ClassToInstanceMap是一种特殊的Map:它的键是类型,而值是符合键所指类型的对象。

    为了扩展Map接口,ClassToInstanceMap额外声明了两个方法:T getInstance(Class<T>)T putInstance(Class<T>, T),从而避免强制类型转换,同时保证了类型安全。

    ClassToInstanceMap有唯一的泛型参数,通常称为B,代表Map支持的所有类型的上界。例如:

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

    从技术上讲,ClassToInstanceMap<B>实现了Map<Class<? extends B>, B>——或者换句话说,是一个映射B的子类型到对应实例的Map。这让ClassToInstanceMap包含的泛型声明有点令人困惑,但请记住B始终是Map所支持类型的上界——通常B就是Object。

    对于ClassToInstanceMap,Guava提供了两种有用的实现:MutableClassToInstanceMapImmutableClassToInstanceMap

    RangeSet

    RangeSet描述了一组不相连的、非空的区间。当把一个区间添加到可变的RangeSet时,所有相连的区间会被合并,空区间会被忽略。例如:

      RangeSet<Integer> rangeSet = TreeRangeSet.create(); 
    
      rangeSet.add(Range.closed(1, 10)); // {[1,10]} 
    
      rangeSet.add(Range.closedOpen(11, 15));//不相连区间:{[1,10], [11,15)} 
    
      rangeSet.add(Range.closedOpen(15, 20)); //相连区间; {[1,10], [11,20)} 
    
      rangeSet.add(Range.openClosed(0, 0)); //空区间; {[1,10], [11,20)} 
    
      rangeSet.remove(Range.open(5, 10)); //分割[1, 10]; {[1,5], [10,10], [11,20)} 
    

    请注意,要合并Range.closed(1, 10)和Range.closedOpen(11, 15)这样的区间,你需要首先用Range.canonical(DiscreteDomain)对区间进行预处理,例如DiscreteDomain.integers()。

    注:RangeSet不支持GWT,也不支持JDK5和更早版本;因为,RangeSet需要充分利用JDK6中NavigableMap的特性。

    RangeSet的视图

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

    • complement():返回RangeSet的补集视图。complement也是RangeSet类型,包含了不相连的、非空的区间。
    • subRangeSet(Range<C>):返回RangeSet与给定Range的交集视图。这扩展了传统排序集合中的headSet、subSet和tailSet操作。
    • asRanges():用Set<Range<C>>表现RangeSet,这样可以遍历其中的Range。
    • asSet(DiscreteDomain<C>)(仅ImmutableRangeSet支持):用ImmutableSortedSet<C>表现RangeSet,以区间中所有元素的形式而不是区间本身的形式查看。(这个操作不支持DiscreteDomain 和RangeSet都没有上边界,或都没有下边界的情况)

    RangeSet的查询方法

    为了方便操作,RangeSet直接提供了若干查询方法,其中最突出的有:

    • contains(C):RangeSet最基本的操作,判断RangeSet中是否有任何区间包含给定元素。
    • rangeContaining(C):返回包含给定元素的区间;若没有这样的区间,则返回null。
    • encloses(Range<C>):简单明了,判断RangeSet中是否有任何区间包括给定区间。
    • span():返回包括RangeSet中所有区间的最小区间。

    RangeMap

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

    RangeMap的视图

    RangeMap提供两个视图:

    • asMapOfRanges():用Map<Range<K>, V>表现RangeMap。这可以用来遍历RangeMap。
    • subRangeMap(Range<K>):用RangeMap类型返回RangeMap与给定Range的交集视图。这扩展了传统的headMap、subMap和tailMap操作。

    2.4-集合扩展工具类

    简介

    有时候你需要实现自己的集合扩展。也许你想要在元素被添加到列表时增加特定的行为,或者你想实现一个Iterable,其底层实际上是遍历数据库查询的结果集。Guava为你,也为我们自己提供了若干工具方法,以便让类似的工作变得更简单。(毕竟,我们自己也要用这些工具扩展集合框架。)

    Forwarding装饰器

    针对所有类型的集合接口,Guava都提供了Forwarding抽象类以简化装饰者模式的使用。

    Forwarding抽象类定义了一个抽象方法:delegate(),你可以覆盖这个方法来返回被装饰对象。所有其他方法都会直接委托给delegate()。例如说:ForwardingList.get(int)实际上执行了delegate().get(int)。

    通过创建ForwardingXXX的子类并实现delegate()方法,可以选择性地覆盖子类的方法来增加装饰功能,而不需要自己委托每个方法——译者注:因为所有方法都默认委托给delegate()返回的对象,你可以只覆盖需要装饰的方法。

    此外,很多集合方法都对应一个”标准方法[standardxxx]”实现,可以用来恢复被装饰对象的默认行为,以提供相同的优点。比如在扩展AbstractList或JDK中的其他骨架类时,可以使用类似standardAddAll这样的方法。

    让我们看看这个例子。假定你想装饰一个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); // 用add(int, E)实现 
    
      } 
    
      @Override public boolean addAll(Collection<? extends E> c) { 
    
      return standardAddAll(c); // 用add实现 
    
      } 
    
      } 
    

    记住,默认情况下,所有方法都直接转发到被代理对象,因此覆盖ForwardingMap.put并不会改变ForwardingMap.putAll的行为。小心覆盖所有需要改变行为的方法,并且确保装饰后的集合满足接口契约。

    通常来说,类似于AbstractList的抽象集合骨架类,其大多数方法在Forwarding装饰器中都有对应的”标准方法”实现。

    对提供特定视图的接口,Forwarding装饰器也为这些视图提供了相应的”标准方法”实现。例如,ForwardingMap提供StandardKeySet、StandardValues和StandardEntrySet类,它们在可以的情况下都会把自己的方法委托给被装饰的Map,把不能委托的声明为抽象方法。

    PeekingIterator

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

    Iterators提供一个<tt>Iterators.peekingIterator(Iterator)</tt>方法,来把Iterator包装为<tt>PeekingIterator</tt>,这是Iterator的子类,它能让你事先窥视[<tt>peek()</tt>]到下一次调用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)) { 
    
      //跳过重复的元素 
    
      iter.next(); 
    
      } 
    
      result.add(current); 
    
      } 
    

    传统的实现方式需要记录上一个元素,并在特定情况下后退,但这很难处理且容易出错。相较而言,PeekingIterator在理解和使用上就比较直接了。

    AbstractIterator

    实现你自己的Iterator?AbstractIterator让生活更轻松。

    用一个例子来解释AbstractIterator最简单。比方说,我们要包装一个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(); 
    
      } 
    
      }; 
    
      } 
    

    你实现了<tt>computeNext()</tt>方法,来计算下一个值。如果循环结束了也没有找到下一个值,请返回endOfData()表明已经到达迭代的末尾。

    注意:AbstractIterator继承了UnmodifiableIterator,所以禁止实现remove()方法。如果你需要支持remove()的迭代器,就不应该继承AbstractIterator。

    AbstractSequentialIterator

    有一些迭代器用其他方式表示会更简单。AbstractSequentialIterator 就提供了表示迭代的另一种方式。

      Iterator<Integer> powersOfTwo = new AbstractSequentialIterator<Integer>(1) { // 注意初始值1! 
    
      protected Integer computeNext(Integer previous) { 
    
      return (previous == 1 << 30) ? null : previous * 2; 
    
      } 
    
      }; 
    

    我们在这儿实现了computeNext(T)方法,它能接受前一个值作为参数。

    注意,你必须额外传入一个初始值,或者传入null让迭代立即结束。因为computeNext(T)假定null值意味着迭代的末尾——AbstractSequentialIterator不能用来实现可能返回null的迭代器。

    原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: [Google Guava] 2.4-集合扩展工具类

    相关文章

      网友评论

        本文标题:Guava集合[Collections]

        本文链接:https://www.haomeiwen.com/subject/npdsbqtx.html