深入理解Java中的不可变对象

  不可变对象想必大部分朋友都不陌生,大家在平时写代码的过程中100%会使用到不可变对象,比如最常见的String对象、包装器对象等,那么到底为何Java语言要这么设计,真正意图和考虑点是什么?可能一些朋友没有细想过这些问题,今天我们就来聊聊跟不可变对象有关的话题。

  以下是本文目录大纲:

  一.什么是不可变对象

  二.深入理解不可变性

  三.如何创建不可变对象

  四.不可变对象真的"完全不可改变"吗?

  若有不正之处,希望谅解并欢迎批评指正。

  请尊重作者劳动成果,转载请标明原文链接:

  https://www.cnblogs.com/dolphin0520/p/10693891.html

一.什么是不可变对象

  下面是《Effective Java》这本书对于不可变对象的定义:

不可变对象(Immutable Object):对象一旦被创建后,对象所有的状态及属性在其生命周期内不会发生任何变化。

  从不可变对象的定义来看,其实比较简单,就是一个对象在创建后,不能对该对象进行任何更改。比如下面这段代码:

public class ImmutableObject {
private int value; public ImmutableObject(int value) {
this.value = value;
} public int getValue() {
return this.value;
}
}

  由于ImmutableObject不提供任何setter方法,并且成员变量value是基本数据类型,getter方法返回的是value的拷贝,所以一旦ImmutableObject实例被创建后,该实例的状态无法再进行更改,因此该类具备不可变性。

  再比如我们平时用的最多的String:

public class Test {

    public static void main(String[] args) {
String str = "I love java";
String str1 = str; System.out.println("after replace str:" + str.replace("java", "Java"));
System.out.println("after replace str1:" + str1);
}
}

  输出结果:

  

  从输出结果可以看出,在对str进行了字符串替换替换之后,str1指向的字符串对象仍然没有发生变化。

二.深入理解不可变性

  我们是否考虑过一个问题:假如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) {
this.age = age;
} public String getIdentityCardID() {
return identityCardID;
} public void setIdentityCardID(String identityCardID) {
this.identityCardID = identityCardID;
} @Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
} if (!(obj instanceof Person)) {
return false;
}
Person personObj = (Person) obj;
return this.age == personObj.getAge() && this.identityCardID.equals(personObj.getIdentityCardID());
} @Override
public int hashCode() {
return age * 37 + identityCardID.hashCode();
}
} public class Test { public static void main(String[] args) {
Person jack = new Person();
jack.setAge(10);
jack.setIdentityCardID("42118220090315234X"); Set<Person> personSet = new HashSet<Person>();
personSet.add(jack); jack.setAge(11); System.out.println(personSet.contains(jack)); }
}

 输出结果:

  

  所以在Java中,对于String、包装器这些类,我们经常会用他们来作为HashMap的key,试想一下如果这些类是可变的,将会发生什么?后果不可预知,这将会大大增加Java代码编写的难度。

三.如何创建不可变对象

  通常来说,创建不可变类原则有以下几条:

  1)所有成员变量必须是private

  2)最好同时用final修饰(非必须)

  3)不提供能够修改原有对象状态的方法

    • 最常见的方式是不提供setter方法

    • 如果提供修改方法,需要新创建一个对象,并在新创建的对象上进行修改

  4)通过构造器初始化所有成员变量,引用类型的成员变量必须进行深拷贝(deep copy)

  5)getter方法不能对外泄露this引用以及成员变量的引用

  6)最好不允许类被继承(非必须)

  JDK中提供了一系列方法方便我们创建不可变集合,如:

Collections.unmodifiableList(List<? extends T> list)

  另外,在Google的Guava包中也提供了一系列方法来创建不可变集合,如:

ImmutableList.copyOf(list)

  这2种方式虽然都能创建不可变list,但是两者是有区别的,JDK自带提供的方式实际上创建出来的不是真正意义上的不可变集合,看unmodifiableList方法的实现就知道了:

  可以看出,实际上UnmodifiableList是将入参list的引用复制了一份,同时将所有的修改方法抛出UnsupportedOperationException。因此如果在外部修改了入参list,实际上会影响到UnmodifiableList,而Guava包提供的ImmutableList是真正意义上的不可变集合,它实际上是对入参list进行了深拷贝。看下面这段测试代码的结果便一目了然:

public class Test {

    public static void main(String[] args) {
List<Integer> list = new ArrayList<Integer>();
list.add(1);
System.out.println(list); List unmodifiableList = Collections.unmodifiableList(list);
ImmutableList immutableList = ImmutableList.copyOf(list); list.add(2);
System.out.println(unmodifiableList);
System.out.println(immutableList); } }

  输出结果:

四.不可变对象真的"完全不可改变"吗?

  不可变对象虽然具备不可变性,但是不是"完全不可变"的,这里打上引号是因为通过反射的手段是可以改变不可变对象的状态的。

  大家看到这里可能有疑惑了,为什么既然能改变,为何还叫不可变对象?这里面大家不要误会不可变的本意,从不可变对象的意义分析能看出来对象的不可变性只是用来辅助帮助大家更简单地去编写代码,减少程序编写过程中出错的概率,这是不可变对象的初衷。如果真要靠通过反射来改变一个对象的状态,此时编写代码的人也应该会意识到此类在设计的时候就不希望其状态被更改,从而引起编写代码的人的注意。下面是通过反射方式改变不可变对象的例子:

public class Test {
public static void main(String[] args) throws Exception {
String s = "Hello World";
System.out.println("s = " + s); Field valueFieldOfString = String.class.getDeclaredField("value");
valueFieldOfString.setAccessible(true); char[] value = (char[]) valueFieldOfString.get(s);
value[5] = '_';
System.out.println("s = " + s);
} }

  输出结果:

参考文章:

http://ifeve.com/immutable-objects/

深入理解Java中的不可变对象的更多相关文章

  1. 为什么Java字符串是不可变对象?

    转自 http://developer.51cto.com/art/201503/468905.htm 本文主要来介绍一下Java中的不可变对象,以及Java中String类的不可变性,那么为什么Ja ...

  2. 深刻理解Java中final的作用(一):从final的作用剖析String被设计成不可变类的深层原因

    声明:本博客为原创博客,未经同意,不得转载!小伙伴们假设是在别的地方看到的话,建议还是来csdn上看吧(原文链接为http://blog.csdn.net/bettarwang/article/det ...

  3. Java中的不可变类理解

    一.Java中的不可变类 不可变类(Immutable Objects):当类的实例一经创建,其内容便不可改变,即无法修改其成员变量. 可变类(Mutable Objects):类的实例创建后,可以修 ...

  4. 深刻理解Java中形參与实參,引用与对象的关系

    声明:本博客为原创博客,未经同意.不得转载! 原文链接为http://blog.csdn.net/bettarwang/article/details/30989755 我们都知道.在Java中,除了 ...

  5. 为什么 String 在 Java 中是不可变的?

    我最喜欢的 Java 面试问题,很棘手,但同时也非常有用.一些面试者也常问这个问题,为什么 String 在 Java 中是 final 的.字符串在 Java 中是不可变的,因为 String 对象 ...

  6. 理解Java中的弱引用(Weak Reference)

    本篇文章尝试从What.Why.How这三个角度来探索Java中的弱引用,理解Java中弱引用的定义.基本使用场景和使用方法.由于个人水平有限,叙述中难免存在不准确或是不清晰的地方,希望大家可以指出, ...

  7. 深入理解Java中的IO

    深入理解Java中的IO 引言:     对程序语言的设计者来说,创建一个好的输入/输出(I/O)系统是一项艰难的任务 < Thinking in Java >   本文的目录视图如下: ...

  8. 理解Java中的ThreadLocal

    提到ThreadLocal,有些Android或者Java程序员可能有所陌生,可能会提出种种问题,它是做什么的,是不是和线程有关,怎么使用呢?等等问题,本文将总结一下我对ThreadLocal的理解和 ...

  9. 十分钟理解Java中的动态代理

    十分钟理解 Java 中的动态代理   一.概述 1. 什么是代理 我们大家都知道微商代理,简单地说就是代替厂家卖商品,厂家“委托”代理为其销售商品.关于微商代理,首先我们从他们那里买东西时通常不知道 ...

随机推荐

  1. 已实现乐观锁功能,FreeSql.DbContext 准备起航

    上回说到 FreeSql.DbContext 的规则,以及演示它的执行过程,可惜当时还不支持"乐观锁",对于更新数据来讲并不安全. FreeSql 核心库 v0.3.27 已提供乐 ...

  2. Kubernetes集群部署关键知识总结

    Kubernetes集群部署需要安装的组件东西很多,过程复杂,对服务器环境要求很苛刻,最好是能连外网的环境下安装,有些组件还需要连google服务器下载,这一点一般很难满足,因此最好是能提前下载好准备 ...

  3. OSPF 基础实验

    一.环境准备 1. 软件:GNS3 2. 路由:c7200 二.实验操作 实验要求: 1.掌握多区域的 OSPF 配置方法. 2.区别不同区域的路由. 3.掌握 OSPF 的路由汇总配置. 4.掌握  ...

  4. 学JAVA的第十九天,抽象类注意事项

    最近老师老是不讲新课,好繁!!! 还是来说抽象类吧. public abstract class A{ //因为下边有一个抽象方法,所以这就必须要是一个抽象类,要不然编译不能通过 public voi ...

  5. 结合Mybatis源码看设计模式——外观模式

    定义 提供了一个统一的接口,用来访问子系统中一群接口 适用场景 子系统复杂,增加外观模式提供简单调用接口 构建多层系统结构,用外观对象作为每层入口 详解 外观模式,主要理解外观.通俗一点可以认为这个模 ...

  6. Git 中 .gitignore 的配置语法

    一.前言 在日常的开发中,当我们需要将一个项目提交到 Git 时,并不是所有的文件都需要提交,比如一些自动生成的文件,类似于 .idea 文件.class 文件等,这时候就可以使用.gitignore ...

  7. Windows-删除Windows Server backup卷影副本

    现有环境中有一台Windows Server做过定期备份计划,时间太久未做清理操作,收到磁盘报警邮件后需要及时释放该空间,具体操作步骤如下: 当前备份计划信息如下: 清理步骤如下: 1.以管理身份运行 ...

  8. 10分钟搭建服务器集群——Windows7系统中nginx与IIS服务器搭建集群实现负载均衡

    分布式,集群,云计算机.大数据.负载均衡.高并发······当耳边响起这些词时,做为一个菜鸟程序猿无疑心中会激动一番(或许这是判断是否是一个标准阿猿的标准吧)! 首先自己从宏观把控一下,通过上网科普自 ...

  9. 转://IO的基础概念

    磁盘IO.网络IO 对磁盘的每个IO就是在磁盘与一些RAM单元之间相互传送一些相邻的扇区的内容.   磁盘IO延时(IO Latency):         也称为IO响应时间,是指内核对磁盘发出一个 ...

  10. JQuery 常用的那些东西

    CDN Google CDN Microsoft CDN CDNJS CDN jsDelivr CDN 选择器 jQuery 元素选择器和属性选择器允许您通过标签名.属性名或内容对 HTML 元素进行 ...