写点什么

深入理解 Java 中的不可变对象,这可能是目前最全的

发布于: 10 小时前

[](


)二.深入理解不可变性


=========================================================================


我们是否考虑过一个问题:假如 Java 中的 String、包装器类设计成可变的 ok 么?如果 String 对象可变了,会带来哪些问题?


我们这一节主要来聊聊不可变对象存在的意义。

[](

)1)让并发编程变得更简单


说到并发编程,可能很多朋友都会觉得最苦恼的事情就是如何处理共享资源的互斥访问,可能稍不留神,就会导致代码上线后出现莫名其妙的问题,并且大部分并发问题都不是太容易进行定位和复现。所以即使是非常有经验的程序员,在进行并发编程时,也会非常的小心,内心如履薄冰。


大多数情况下,对于资源互斥访问的场景,都是采用加锁的方式来实现对资源的串行访问,来保证并发安全,如 synchronize 关键字,Lock 锁等。但是这种方案最大的一个难点在于:在进行加锁和解锁时需要非常地慎重。如果加锁或者解锁时机稍有一点偏差,就可能会引发重大问题,然而这个问题 Java 编译器无法发现,在进行单元测试、集成测试时可能也发现不了,甚至程序上线后也能正常运行,但是可能突然在某一天,它就莫名其妙地出现了。


既然采用串行方式来访问共享资源这么容易出现问题,那么有没有其他办法来解决呢?


事实上,引起线程安全问题的根本原因在于:多个线程需要同时访问同一个共享资源。


假如没有共享资源,那么多线程安全问题就自然解决了,Java 中提供的 ThreadLocal 机制就是采取的这种思想。


然而大多数时候,线程间是需要使用共享资源互通信息的,如果共享资源在创建之后就完全不再变更,如同一个常量,而多个线程间并发读取该共享资源是不会存在线上安全问题的,因为所有线程无论何时读取该共享资源,总是能获取到一致的、完整的资源状态。


不可变对象就是这样一种在创建之后就不再变更的对象,这种特性使得它们天生支持线程安全,让并发编程变得更简单。


我们来看一个例子,这个例子来源于:[http://ifeve.com/immutable-objects/](


)



public class SynchronizedRGB {
private int red; // 颜色对应的红色值
private int green; // 颜色对应的绿色值
private int blue; // 颜色对应的蓝色值
private String name; // 颜色名称

private void check(int red, int green, int blue) {
if (red < 0 || red > 255 || green < 0 || green > 255
|| blue < 0 || blue > 255) {
throw new IllegalArgumentException();
}
}

public SynchronizedRGB(int red, int green, int blue, String name) {
check(red, green, blue);
this.red = red;
this.green = green;
this.blue = blue;
this.name = name;
}

public void set(int red, int green, int blue, String name) {
check(red, green, blue);
synchronized (this) {
this.red = red;
this.green = green;
this.blue = blue;
this.name = name;
}
}

public synchronized int getRGB() {
return ((red << 16) | (green << 8) | blue);
}

public synchronized String getName() {
return name;
}
}
复制代码


例如一个有个线程 1 执行了以下代码:



SynchronizedRGB color = new SynchronizedRGB(0, 0, 0, "Pitch Black");
int myColorInt = color.getRGB(); // Statement1
String myColorName = color.getName(); // Statement2
复制代码


然后有另外一个线程 2 在 Statement 1 之后、Statement 2 之前调用了 color.set 方法:



color.set(0, 255, 0, "Green");
复制代码


那么在线程 1 中变量 myColorInt 的值和 myColorName 的值就会不匹配。为了避免出现这样的结果,必须要像下面这样把这两条语句绑定到一块执行:



synchronized (color) {
int myColorInt = color.getRGB();
String myColorName = color.getName();
}
复制代码


假如 SynchronizedRGB 是不可变类,那么就不会出现这个问题,比如将 SynchronizedRGB 改成下面这种实现方式:



public class ImmutableRGB {
private int red;
private int green;
private int blue;
private String name;

private void check(int red, int green, int blue) {
if (red < 0 || red > 255 || green < 0 || green > 255
|| blue < 0 || blue > 255) {
throw new IllegalArgumentException();
}
}

public ImmutableRGB(int red, int green, int blue, String name) {
check(red, green, blue);
this.red = red;
this.green = green;
this.blue = blue;
this.name = name;
}

public ImmutableRGB set(int red, int green, int blue, String name) {
return new ImmutableRGB(red, green, blue, name);
}

public int getRGB() {
return ((red << 16) | (green << 8) | blue);
}

public String getName() {
return name;
}
}
复制代码


由于 set 方法并没有改变原来的对象,而是新创建了一个对象,所以无论线程 1 或者线程 2 怎么调用 set 方法,都不会出现并发访问导致的数据不一致的问题。

[](

)2)消除副作用


很多时候一些很严重的 bug 是由于一个很小的副作用引起的,并且由于副作用通常不容易被察觉,所以很难在编写代码以及代码 review 过程中发现,并且即使发现了也可能会花费很大的精力才能定位出来。


举个简单的例子:



class Person {
private int age; // 年龄
private String identityCardID; // 身份证号码

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getIdentityCardID() {
return identityCardID;
}

public void setIdentityCardID(String identityCardID) {
this.identityCardID = identityCardID;
}
}


public class Test {

public static void main(String[] args) {
Person jack = new Person();
jack.setAge(101);
jack.setIdentityCardID("42118220090315234X");

System.out.println(validAge(jack));
    
    // 后续使用可能没有察觉到jack的age被修改了
    // 为后续埋下了不容易察觉的问题

}

public static boolean validAge(Person person) {
if (person.getAge() >= 100) {
person.setAge(100); // 此处产生了副作用
return false;
}
return true;
}

}
复制代码


validAge 函数本身只是对 age 大小进行判断,但是在这个函数里面有一个副作用,就是对参数 person 指向的对象进行了修改,导致在外部的 jack 指向的对象也发生了变化。


如果 Person 对象是不可变的,在 validAge 函数中是无法对参数 person 进行修改的,从而避免了 validAge 出现副作用,减少了出错的概率。

[](

)3)减少容器使用过程出错的概率


我们在使用 HashSet 时,如果 HashSet 中元素对象的状态可变,就会出现元素丢失的情况,比如下面这个例子:



class Person {
private int age; // 年龄
private String identityCardID; // 身份证号码

public int getAge() {
return age;
}

public void setAge(int age) {

### Java核心架构进阶知识点
**[CodeChina开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频】](https://ali1024.coding.net/public/P7/Java/git)**
面试成功其实都是必然发生的事情,因为在此之前我做足了充分的准备工作,不单单是纯粹的刷题,更多的还会去刷一些Java核心架构进阶知识点,比如:JVM、高并发、多线程、缓存、Spring相关、分布式、微服务、RPC、网络、设计模式、MQ、Redis、MySQL、设计模式、负载均衡、算法、数据结构、kafka、ZK、集群等。而这些也全被整理浓缩到了一份pdf——《Java核心架构进阶知识点整理》,全部都是精华中的精华,本着共赢的心态,好东西自然也是要分享的
![image](https://static001.geekbang.org/infoq/4d/4de32e8635448def2338e95ab79c11b4.png)
![image](https://static001.geekbang.org/infoq/b5/b5e2fba58e99eaf4c6b8600de276bd8a.png)
![image](https://static001.geekbang.org/infoq/ee/ee367477ca30221aeff18471e67d4bbe.png)
内容颇多,篇幅却有限,这就不在过多的介绍了,大家可根据以上截图自行脑补

复制代码


用户头像

VX:vip204888 领取资料 2021.07.29 加入

还未添加个人简介

评论

发布
暂无评论
深入理解Java中的不可变对象,这可能是目前最全的