美文网首页Java
Guava常用数据结构

Guava常用数据结构

作者: 王勇1024 | 来源:发表于2020-11-10 20:45 被阅读0次

    Guava是一种基于开源的Java库,其中包含谷歌正在由他们很多项目使用的很多核心库。这个库是为了方便编码,并减少编码错误。这个库提供用于集合,缓存,支持原语,并发性,常见注解,字符串处理,I/O和验证的实用方法。

    Guava 的好处:

    • 标准化 - Guava库是由谷歌托管。
    • 高效 - 可靠,快速和有效的扩展JAVA标准库
    • 优化 -Guava库经过高度的优化。
    • 函数式编程 -增加JAVA功能和处理能力。
    • 实用程序 - 提供了经常需要在应用程序开发的许多实用程序类。
    • 验证 -提供标准的故障安全验证机制。
    • 最佳实践 - 强调最佳的做法。

    今天我们就一起来学习一下Guava吧。

    不可变集合

    范例

    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。

    asList视图

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

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

    关联可变集合和不可变集合

    可变集合接口 属于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

    新集合类型

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

    Multiset

    Guava 提供了一个新集合类型 Multiset,它可以多次添加相等的元素。Multiset继承自 JDK 中的 Collection 接口,而不是 Set 接口,所以包含重复元素并没有违反原有的接口契约。
    可以用两种方式看待 Multiset:

    • 没有元素顺序限制的 ArrayList
    • 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>,和 Map 的 entrySet 类似。
    • elementSet() 返回所有不重复元素的 Set,和 Map 的 keySet()类似。
    • 所有 Multiset 实现的内存消耗随着不重复元素的个数线性增长。

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

    • Multiset 中的元素计数只能是正数。任何元素的计数都不能为负,也不能是 0。elementSet()和 entrySe
      t()视图中也不会有这样的元素。
    • 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

    Multimap

    每个有经验的 Java 程序员都在某处实现过 Map<K, List>或 Map<K, Set>,并且要忍受这个结构的笨拙。例如,Map<K, Set>通常用来表示非标定有向图。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>,让你可以按另一种方式看待 Multimap。重要的是,不会有任何键映射到空集合:一个键要么至少到一个值,要么根本就不在Multimap 中。

    很少会直接使用 Multimap 接口,更多时候你会用 ListMultimapSetMultimap接口,它们分别把键映射到 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>形式的视图。。返回的 Map 支持 remove 操作,并且会反映到底层的 Multimap,但它不支持 put 或 putAll 操作。
    • entries 用 Collection<Map.Entry<K, V>>返回 Multimap 中所有”键-单个值映射”——包括重复
      键。(对 SetMultimap,返回的是 Set)
    • keySet 用 Set 表示 Multimap 中所有不同的键。
    • keys 用 Multiset 表示 Multimap 中的所有键,每个键重复出现的次数等于它映射的值的个数。可以从这个
      Multiset 中移除元素,但不能做添加操作;移除操作会反映到底层的 Multimap。
    • values() 用一个”扁平”的Collection包含 Multimap 中的所有值。

    Multimap 的各种实现

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

    实现 键行为类似 值行为类似
    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()保留了所有键和值的迭代顺序。
    • LinkedHashMultimap 保留了映射项的插入顺序,包括键插入的顺序,以及键映射的所有值的插入顺序。

    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()返回”行”的集合S
      et。

    • 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 的内存利用率。

    相关文章

      网友评论

        本文标题:Guava常用数据结构

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