【Java技术专题】「Guava开发指南」手把手教你如何进行使用Guava工具箱进行开发系统实战指南(不可变集合篇)

简介: 【Java技术专题】「Guava开发指南」手把手教你如何进行使用Guava工具箱进行开发系统实战指南(不可变集合篇)

为什么要使用不可变集合

不可变集合包括元组和冻结集合,其特点是不能被修改。元组有序、不可变、可包含不同类型元素,不能进行修改、删除操作,可通过索引访问元素。冻结集合是一种无序的集合类型,内部元素不能修改、添加或删除,因此常用于

不可变对象有很多优点

不可变对象有以下优点:

  1. 对于不可信的库调用,不可变对象是安全的;
  2. 在多线程环境下,由于不可变对象不会发生变化,因此不存在竞态条件问题;
  3. 不可变集合不需要考虑变化,因此可以节省时间和,所有不可变的集合都比可变集合有更好的内性,可以作为常量来安全使用。

Java原生的不可变集合

JDK提供的Collections.unmodifiableXXX方法可以将集合包装为不可变形式,但是我们认为这种方法并不理想。

  • 笨重而繁琐:不能适用于所有需要防御性拷贝的情况;
  • 不安全:必须确保没有人能够通过原始集合的引用进行修改,才能得到实际上不可变的集合;
  • 低效:包装成的集合仍然带有可变集合的负担,例如并发修改检查、散列表需要的额外空间等。

如果不需要修改某个集合,或者希望保持集合不变,将其防御性地拷贝到不可变集合是一个很好的实践。

重要提示:Guava不可变集合的实现不支持null值。通过对Google内部的代码库的详细研究,发现只有5%的情况需要在集合中允许null元素,而其余95%的场景都会快速失败处理null值。如果您需要在不可变集合中使用null,请使用JDK中的Collections.unmodifiableXXX方法

Java原生的不可变集合的案例

这段 Java 代码定义了一个名为 LUCKY_NUMBERS 的静态常量 Set(即不可变集合)。

java

复制代码

public static final Set<Integer> LUCKY_NUMBERS;
static {
 Set<Integer> set = new LinkedHashSet<Integer>();
 set.add(4);
 set.add(8);
 set.add(15);
 set.add(16);
 set.add(23);
 set.add(42);
 LUCKY_NUMBERS = Collections.unmodifiableSet(set);
}

使用 static 修饰符来表示这个 Set 常量是与类相关联,而不是与类的实例相关联的。static 代码块是一个静态初始化块,用于在类加载时初始化静态成员变量。在这里,用 LinkedHashSet 创建了一个 Set 对象,并将其设置为不可变集合。LinkedHashSet 可以保 持插入元素的顺序。

这个 Set 常量包含 6 个自然数,即 4,8,15,16,23 和 42。由于 Set 对象被设置为不可变集合,因此无法将新元素添加到集合中,以在代码运行时保持其中元素的不变性。

java

复制代码

public static final Set<Integer> LUCKY_NUMBERS
 = Collections.unmodifiableSet(
 new LinkedHashSet<Integer>(
 Arrays.asList(4, 8, 15, 16, 23, 42)));

Java原生定义了一个名为 LUCKY_NUMBERS 的静态常量 Set(即不可变集合)。这个 Set 常量包含 6 个自然数,即 4,8,15,16,23 和 42。与之前的示例代码不同的是,这里使用了更简单的方式实现了相同的功能——使用 Collections 类的 unmodifiableSet() 方法。在这里,使用 Arrays 类的 asList() 方法将一些整数转换成一个 List,然后再使用 LinkedHashSet 构造函数创建一个 LinkedHashSet。最后,将创建的 LinkedHashSet 转换为不可变 Set,并将其赋值为 LUCKY_NUMBERS 静态常量。由于 Set 对象被设置为不可变集合,因此无法将新元素添加到集合中,以在代码运行时保持其中元素的不变性。

java

复制代码

public static final ImmutableSet<Integer> LUCKY_NUMBERS
 = ImmutableSet.of(4, 8, 15, 16, 23, 42);

Guava定义了一个名为 LUCKY_NUMBERS 的静态常量集合,其元素为 4,8,15,16,23 和 42。与之前的示例不同的是,这里使用了 Guava 提供的 ImmutableSet,其是一个不可变集合。通过调用 of() 静态方法并传入要添加到集合中的元素,会返回一个不可变的 ImmutableSet 集合对象。ImmutableSet 允许我们创建一个在运行时不可更改的集合,从而使多线程代码的编写更加轻松,并且可以更好地保证代码安全性。由于 LUCKY_NUMBERS 集合使用了 ImmutableSet,因此在其对象的生命周期内,它的内容不会被修改,这有助于确保代码的稳定性和正确性。

怎么使用不可变集合

可用的方法来创建不可变集合有:

  1. 使用copyOf方法,例如ImmutableSet.copyOf(set);

java

复制代码

class Foo {
  Set<Bar> bars;
  Foo(Set<Bar> bars) {
    this.bars = ImmutableSet.copyOf(bars); // defensive copy!
  }
}
  1. 使用of方法,例如ImmutableSet.of("a","b","c")或ImmutableMap.of("a",1,"b",2);

java

复制代码

public static final ImmutableSet<String> COLOR_NAMES = ImmutableSet.of(
  "red",
  "orange",
  "yellow",
  "green",
  "blue",
  "purple");

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

java

复制代码

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

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

  1. 使用构建器工具,例如使用ImmutableList.builder()返回一个builder对象,之后可使用该对象来添加或删除元素,并使用它们来创建不可变的集合。

java

复制代码

public static final ImmutableSet<Color> GOOGLE_COLORS =
  ImmutableSet.<Color>builder()
  .addAll(WEBSAFE_COLORS)
  .add(new Color(0, 191, 255))
  .build();

比想象中更智能的copyOf

值得注意的是,ImmutableXXX.copyOf() 方法在安全的情况下会尝试避免进行拷贝。实现细节不详,但通常是非常智能的,例如:

java

复制代码

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(),这是一个常量时间复杂度的 List 视图,其存储的元素是不可变的。

ImmutableXXX.copyOf(ImmutableCollection) 是一种探索性实现,旨在避免对于一些可能的情况进行线性时间复杂度的拷贝操作,例如:

  • 可以在常量时间内使用底层数据结构,但是ImmutableSet.copyOf(ImmutableList)就不能在常量时间内完成。
  • 如使用ImmutableList.copyOf(hugeList.sublist(0,10)),可以避免不必要地持有 hugeList 的引用,从而防止内存泄漏问题的发生。
  • 如果我们想使用 ImmutableSet.copyOf(myImmutableSortedSet) 操作来获取不改变语义的 ImmutableSet,需要注意不同语义对 hashCode() 和 equals 的影响。为避免此类问题,需要显式地拷贝一份新的 ImmutableSet。

为了减少防御性编程风格带来的性能开销,在合适的情况下我们可以尽量避免进行线性拷贝操作。这样可以最大限度地提高代码的性能表现。

asList视图

所有不可变集合都提供 asList() 方法,可以方便地将集合元素以列表形式进行读取。例如,可以使用 sortedSet.asList().get(k) 在 ImmutableSortedSet 中读取第k个最小元素。

注意,asList() 返回的 ImmutableList 视图实现通常是稳定的且开销较小,而非简单地将元素拷贝进列表中。因此,asList() 返回的列表视图通常比一般列表的平均性能更好。此外,在底层集合的支持下,asList() 总是使用高效的 contains() 方法。

可变集合和不可变集合的关系

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

不可变的Map

定义了一个名为 ENGLISH_TO_INT 的静态不可变 Map,用于将字符串类型的英文单词转换为对应的整数值。

java

复制代码

public static final Map<String, Integer> ENGLISH_TO_INT;
static {
 Map<String, Integer> map
 = new LinkedHashMap<String, Integer>();
 map.put("four", 4);
 map.put("eight", 8);
 map.put("fifteen", 15);
 map.put("sixteen", 16);
 map.put("twenty-three", 23);
 map.put("forty-two", 42); 
 ENGLISH_TO_INT = Collections.unmodifiableMap(map);
}

使用 LinkedHashMap 存储字符串-整数的映射关系,其中字符串代表英文单词,整数为对应的数字。在静态代码块中,程序使用 Collections.unmodifiableMap() 将这个 Map 转换为不可修改的 Map,并将其赋值给 ENGLISH_TO_INT 常量。这个过程将会确保 ENGLISH_TO_INT 对象在其生命周期内不会被修改,从而防止了可能的错误或安全问题。

java

复制代码

public static final ImmutableMap<String, Integer>
 ENGLISH_TO_INT = ImmutableMap
 .with("four", 4)
 .with("eight", 8)
 .with("fifteen", 15)
 .with("sixteen", 16)
 .with("twenty-three", 23)
 .with("forty-two", 42)
 .build();

这段代码使用Guava定义了一个名为 ENGLISH_TO_INT 的静态不可变 Map,用于将字符串类型的英文单词转换为对应的整数值。这个代码的实现方式是使用 Guava 开源库中的 ImmutableMap 实现。静态常量 ENGLISH_TO_INT 的值是通过使用 with() 方法连续地向 ImmutableMap 对象中添加字符串-整数的映射关系,然后调用 build() 方法建立不可变的 Map 对象实现的。

与第一个代码示例相比,这个实现方式更加简洁且语法更加直观。此外,Guava 中的 ImmutableMap 实现具有更好的性能和可读性,并能够避免并发修改和不必要的内存使用,从而更容易实现代码的可靠性和稳定性。

ImmutableMap.of

java

复制代码

static final ImmutableMap<Integer, String> MAP
 = ImmutableMap.of(1, "one", 2, "two");

这段 Java 代码定义了一个名为 MAP 的静态不可变 Map 对象,它的键是整数类型的 1 和 2,分别对应的值是字符串类型的 "one" 和 "two"。

ImmutableMap.of() 方法是 Guava 开源库中用于创建不可变 Map 对象的快捷方式。这个方法支持传递 1 到 5 个键值对作为参数,从而只需一行代码就能创建一个简单的不可变 Map 对象。由于这个 Map 对象是不可变的,因此它的键值对不能被增加、删除或修改。这种不可变性使得这个 Map 对象更加稳定和安全,并且在多线程环境中更加可靠。

总的来说,这种代码编写风格更加简洁和直观,同时使用了 Guava 提供的不可变集合工具类库,从而可以提高代码的可读性和稳定性。同时这种实现方式还可以避免程序的并发修改和不必要的内存使用,具有更好的性能表现。

相关文章
|
10天前
|
运维 Java
Java版HIS系统 云HIS系统 云HIS源码 结构简洁、代码规范易阅读
云HIS系统分为两个大的系统,一个是基层卫生健康云综合管理系统,另一个是基层卫生健康云业务系统。基层卫生健康云综合管理系统由运营商、开发商和监管机构使用,用来进行运营管理、运维管理和综合监管。基层卫生健康云业务系统由基层医院使用,用来支撑医院各类业务运转。
36 5
|
1天前
|
Java 索引
Java String应用与开发
Java String应用与开发
8 0
|
1天前
|
安全 Java 开发者
构建高效微服务架构:后端开发的新范式Java中的多线程并发编程实践
【4月更文挑战第29天】在数字化转型的浪潮中,微服务架构已成为软件开发的一大趋势。它通过解耦复杂系统、提升可伸缩性和促进敏捷开发来满足现代企业不断变化的业务需求。本文将深入探讨微服务的核心概念、设计原则以及如何利用最新的后端技术栈构建和部署高效的微服务架构。我们将分析微服务带来的挑战,包括服务治理、数据一致性和网络延迟问题,并讨论相应的解决方案。通过实际案例分析和最佳实践的分享,旨在为后端开发者提供一套实施微服务的全面指导。 【4月更文挑战第29天】在现代软件开发中,多线程技术是提高程序性能和响应能力的重要手段。本文通过介绍Java语言的多线程机制,探讨了如何有效地实现线程同步和通信,以及如
|
2天前
|
监控 Java BI
java基于云计算的SaaS医院his信息系统源码 HIS云平台源码
基于云计算技术的B/S架构的HIS系统源码,SaaS模式Java版云HIS系统,融合B/S版电子病历系统,支持电子病历四级,HIS与电子病历系统均拥有自主知识产权。
21 5
|
3天前
|
Java Shell API
Java 模块化编程:概念、优势与实战指南
【4月更文挑战第27天】Java 模块化编程是 Java 9 中引入的一项重大特性,通过 Java Platform Module System (JPMS) 实现。模块化旨在解决 Java 应用的封装性、可维护性和性能问题
10 0
|
6天前
|
设计模式 存储 前端开发
Java从入门到精通:2.2.1学习Java Web开发,了解Servlet和JSP技术,掌握MVC设计模式
Java从入门到精通:2.2.1学习Java Web开发,了解Servlet和JSP技术,掌握MVC设计模式
|
5天前
|
数据采集 存储 Java
高德地图爬虫实践:Java多线程并发处理策略
高德地图爬虫实践:Java多线程并发处理策略
|
3天前
|
设计模式 安全 Java
【JAVA】Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式
【JAVA】Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式
|
1天前
|
消息中间件 监控 安全
【JAVAEE学习】探究Java中多线程的使用和重点及考点
【JAVAEE学习】探究Java中多线程的使用和重点及考点
|
3天前
|
安全 Java
【JAVA】线程的run()和start()有什么区别?
【JAVA】线程的run()和start()有什么区别?