【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 提供的不可变集合工具类库,从而可以提高代码的可读性和稳定性。同时这种实现方式还可以避免程序的并发修改和不必要的内存使用,具有更好的性能表现。

相关文章
|
3天前
|
SQL Java 数据库连接
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率。本文介绍了连接池的工作原理、优势及实现方法,并提供了HikariCP的示例代码。
13 3
|
2天前
|
SQL Java 程序员
倍增 Java 程序员的开发效率
应用计算困境:Java 作为主流开发语言,在数据处理方面存在复杂度高的问题,而 SQL 虽然简洁但受限于数据库架构。SPL(Structured Process Language)是一种纯 Java 开发的数据处理语言,结合了 Java 的架构灵活性和 SQL 的简洁性。SPL 提供简洁的语法、完善的计算能力、高效的 IDE、大数据支持、与 Java 应用无缝集成以及开放性和热切换特性,能够大幅提升开发效率和性能。
|
3天前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
11 2
|
3天前
|
Java 数据库连接 数据库
深入探讨Java连接池技术如何通过复用数据库连接、减少连接建立和断开的开销,从而显著提升系统性能
在Java应用开发中,数据库操作常成为性能瓶颈。本文通过问题解答形式,深入探讨Java连接池技术如何通过复用数据库连接、减少连接建立和断开的开销,从而显著提升系统性能。文章介绍了连接池的优势、选择和使用方法,以及优化配置的技巧。
8 1
WK
|
2天前
|
开发框架 移动开发 Java
C++和Java哪个更适合开发移动应用
本文对比了C++和Java在移动应用开发中的优劣,从市场需求、学习难度、开发效率、跨平台性和应用领域等方面进行了详细分析。Java在Android开发中占据优势,而C++则适合对性能要求较高的场景。选择应根据具体需求和个人偏好综合考虑。
WK
9 0
|
7天前
|
监控 安全 Java
在 Java 中使用线程池监控以及动态调整线程池时需要注意什么?
【10月更文挑战第22天】在进行线程池的监控和动态调整时,要综合考虑多方面的因素,谨慎操作,以确保线程池能够高效、稳定地运行,满足业务的需求。
77 38
|
4天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
8天前
|
Java 调度
[Java]线程生命周期与线程通信
本文详细探讨了线程生命周期与线程通信。文章首先分析了线程的五个基本状态及其转换过程,结合JDK1.8版本的特点进行了深入讲解。接着,通过多个实例介绍了线程通信的几种实现方式,包括使用`volatile`关键字、`Object`类的`wait()`和`notify()`方法、`CountDownLatch`、`ReentrantLock`结合`Condition`以及`LockSupport`等工具。全文旨在帮助读者理解线程管理的核心概念和技术细节。
24 1
[Java]线程生命周期与线程通信
|
6天前
|
安全 Java
在 Java 中使用实现 Runnable 接口的方式创建线程
【10月更文挑战第22天】通过以上内容的介绍,相信你已经对在 Java 中如何使用实现 Runnable 接口的方式创建线程有了更深入的了解。在实际应用中,需要根据具体的需求和场景,合理选择线程创建方式,并注意线程安全、同步、通信等相关问题,以确保程序的正确性和稳定性。
|
5天前
|
缓存 Java 调度
Java中的多线程编程:从基础到实践
【10月更文挑战第24天】 本文旨在为读者提供一个关于Java多线程编程的全面指南。我们将从多线程的基本概念开始,逐步深入到Java中实现多线程的方法,包括继承Thread类、实现Runnable接口以及使用Executor框架。此外,我们还将探讨多线程编程中的常见问题和最佳实践,帮助读者在实际项目中更好地应用多线程技术。
11 3