一、Java中的不可变类

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

可变类(Mutable Objects):类的实例创建后,可以修改其内容。

Java 中八个基本类型的包装类和 String 类都属于不可变类,而其他的大多数类都属于可变类。

二、与引用不可变的区别

需要特别注意的是,不可变类的不可变是指该类的实例不可变而非指向该实例的引用的不可变。

String s = "abc";
System.out.println("s:" + s); // 输出s:abc
s = "xyz";
System.out.println("s:" + s); // 输出s:xyz

以上代码显示,不可变类 String 貌似是可以改变值的,但实际上并不是。变量 s 只是一个指向 String 类的实例的引用,存储的是实例对象在内存中的地址。代码中第三行的 “改变” 实际上是新实例化了一个 String 对象,并将 s 的指向修改到新对象上,而原来的对象在内存中并未发生变化,只是少了一个指向它的引用,并且在未来被垃圾回收前它都将保持不变。

public class Immutable {

    public static void main(String[] args) {
String str = new String("abc");
String str2 = str;
System.out.println(str == str2); // true
str2 = "cba";
System.out.println(str == str2); // false System.out.println(str == row(str)); // true
System.out.println(str == other(str)); // false
} static private String row(String s){
return s;
}
static private String other(String s){
s="xyz"; //此处形参 s 指向了新的String对象,引用的地址发生变化
return s;
}
}

如此我们看到,对于不可变类的对象,都是通过新创建一个对象并将引用指向新对象来实现变化的。

通常,使用关键字 final 修饰的字段初始化后是不可变的,而这种不可变就是指引用的不可变。具体就是该引用所指对象的内存地址是不可变的,但并非该对象不可变。如果该对象也不可变,那么该对象就是不可变类的一个实例。

public class Immutable {

    public static void main(String[] args) {
Immutable immutable = new Immutable();
final Inner inner = immutable.new Inner();
inner.value = 123; // 实例可变
// 下面语句编译错误,inner 是final的,无法让它指向新的对象(改变指向地址)
// inner = it.new Inner();
Inner inner2 = inner; // 复制了一份引用,inner和inner2指向同一个对象
System.out.println(inner); // 将调用 toString 方法输出对象内存地址
System.out.println(inner2); // inner和inner2具有相同的地址
System.out.println(inner.value); // 输出 123
System.out.println(inner2.value); // 输出123
inner2.value = 321;
System.out.println(inner); // 输出321
} class Inner{
private int value;
}
}

三、不可变类是如何实现的

immutable对象的状态在创建之后就不能发生改变,任何对它的改变都应该产生一个新的对象。

因此,一个不可变类的定义应当具备以下特征:

  1. 所有成员都是 private final 的
  2. 不提供对成员的改变方法,例如:setXXXX
  3. 确保所有的方法不会被重载。手段有两种:使用final Class(强不可变类),或者将所有类方法加上final(弱不可变类)。
  4. 如果某一个类成员不是基本类型(primitive type)或不可变类,必须通过在成员初始化(in)或者getter方法(out)时通过深度拷贝(即复制一个该类的新实例而非引用)方法,来确保类的不可变。
  5. 如果有必要,重写hashCode和equals方法,同时应保证两个用equals方法判断为相等的对象,其hashCode也应相等。

下面是一个示例:

public final class ImmutableDemo {
private final int[] myArray;
public ImmutableDemo(int[] array) {
// this.myArray = array; // 错误!
this.myArray = array.clone(); // 正确
}
public int[] get(){
return myArray.clone();
}
}

上例中错误的方法不能保证不可变性,myArray 和形参 array 指向同一块内存地址,用户可以在 ImmutableDemo 实例之外通过修改 array 对象的值来改变实例内部 myArray 的值。正确的做法是通过深拷贝将 array 的值传递给 myArray 。同样, getter 方法中不能直接返回对象本身,而应该是克隆对象并返回对象的拷贝,这种做法避免了对象外泄,防止通过 getter 获得内部可变成员对象后对成员变量直接操作,导致成员变量发生改变。

对于不可变类,String 是一个典型例子,看看它的源码也有助于我们设计不可变类。

四、不可变类的优点

不可变类有两个主要有点,效率和安全。

  • 效率

    当一个对象是不可变的,那么需要拷贝这个对象的内容时,就不用复制它的本身而只是复制它的地址,复制地址(通常一个指针的大小)只需要很小的内存空间,具有非常高的效率。同时,对于引用该对象的其他变量也不会造成影响。

    此外,不变性保证了hashCode 的唯一性,因此可以放心地进行缓存而不必每次重新计算新的哈希码。而哈希码被频繁地使用, 比如在hashMap 等容器中。将hashCode 缓存可以提高以不变类实例为key的容器的性能。

  • 线程安全

    在多线程情况下,一个可变对象的值很可能被其他进程改变,这样会造成不可预期的结果,而使用不可变对象就可以避免这种情况同时省去了同步加锁等过程,因此不可变类是线程安全的。

当然,不可变类也有缺点:不可变类的每一次“改变”都会产生新的对象,因此在使用中不可避免的会产生很多垃圾。

更多关于深度拷贝的知识,参阅这篇:java对象深复制、浅复制(深拷贝、浅拷贝)的理解

Java中的不可变类理解的更多相关文章

  1. Java中的不可变类

    概念:不可变类的意思是创建该类的实例后,该实例的属性是不可改变的.java中的8个包装类和String类都是不可变类.所以不可变类并不是指该类是被final修饰的,而是指该类的属性是被final修饰的 ...

  2. 《Java中的不可变类》

    //不可变类举例: /* 下面程序试图定义一个不可变类Person类,但=因为Person类包含一个引用类型的成员变量, 且这个引用类是可变类,所以导致Person类也变成了可变类. */ class ...

  3. 关于java中的不可变类(转)

    如何在Java中写出Immutable的类? 要写出这样的类,需要遵循以下几个原则: 1)immutable对象的状态在创建之后就不能发生改变,任何对它的改变都应该产生一个新的对象. 2)Immuta ...

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

    深入理解Java中的不可变对象 不可变对象想必大部分朋友都不陌生,大家在平时写代码的过程中100%会使用到不可变对象,比如最常见的String对象.包装器对象等,那么到底为何Java语言要这么设计,真 ...

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

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

  6. java中容器的学习与理解

    以前一直对于java中容器的概念不理解,虽然学习过,但始终没有认真理解过,这几天老师提出了这样一个问题,你怎么理解java中的容器.瞬间就蒙了.于是各种搜资料学习了一下,下面是我学习后整理出来的的一些 ...

  7. 为什么 String 在 Java 中是不可变的(终极答案)

    为什么 String 在 Java 中是不可变的(终极答案) 我们可以从2个角度去看待这个问题: 1.为什么要设计成不可变2.如何保证不可变? 1.为什么设计不可变? 1.String对象缓存在Str ...

  8. JAVA中封装JSONUtils工具类及使用

    在JAVA中用json-lib-2.3-jdk15.jar包中提供了JSONObject和JSONArray基类,用于JSON的序列化和反序列化的操作.但是我们更习惯将其进一步封装,达到更好的重用. ...

  9. Java中直接输出一个类的对象

    例如 package com.atguigu.java.fanshe; public class Person { String name; private int age; public Strin ...

随机推荐

  1. rock-paper-scissors

    rock-paper-scissors维护三个前缀和,然后注意顺序,最后做差来确定可行的答案,因为答案比较小,可以考虑这种暴力做法,像这种方案数可以++的题真的不多,如果想不出来特别优秀的想法,不妨简 ...

  2. php mysql 编码问题

    php mysql 编码问题 问题: PHP从数据库中读取数据,并echo出来,数据中文显示正常:但是echo出新定义的中文字符串,新定义的字符串会乱码. 由此可能是数据库中提取出来的中文编码和php ...

  3. git 将主分支的提交合并到分支上(主分支同步到分支)

    通常都会遇到将分支修改的内容合并到主分支中,但是在主分支中修改了内容怎么同步到分支上呢,这个时候需要将主分支上的提交操作在分支上再做一次: 1.首先在主分支上执行: git log 2.找到你想要同步 ...

  4. Django基础(三)

    知识预览 Ajax前戏:json Ajax简介 jquery实现的ajax JS实现的ajax 回到顶部 Ajax前戏:json 什么是json? 定义: JSON(JavaScript Object ...

  5. python 元类的简单解释

    本文转自博客:http://www.cnblogs.com/piperck/p/5840443.html 作者:piperck python 类和元类(metaclass)的理解和简单运用 (一) p ...

  6. 2017-9-13-Linux移植:u-boot的移植

    1.u-boot下载地址:http://ftp.denx.de/pub/u-boot/ 2.Linux环境下使用tar命令解压压缩包:tar -xzvf file.tar.gz tar -xvf fi ...

  7. [JOISC2014]ストラップ

    [JOISC2014]ストラップ 题目大意: 有\(n(n\le2000)\)个挂饰,每个挂饰有一个喜悦值\(b_i(|b_i|\le10^6)\),下面有\(b_i(b_i\le10^6)\)个挂钩 ...

  8. BZOJ3253 : 改编

    设$f[x][y]$表示从x和y出发相遇的期望长度,则$f[x][x]=0$,且$f[x][y]$对称,共$C(n,2)$个未知量. 列出方程组$G$,得到$G\times F=B$. 高斯消元求出$ ...

  9. shell脚本使用技巧3--调试

    1.使用-x,开启shell脚本的跟踪调试功能 ex:bash -x script.sh or sh -x script.sh 2.使用set -x 和 set +x对脚本进行部分调试(输入中间的内容 ...

  10. MySql 数据库安装、环境变量配置 以及 本地连接

    一.准备 MySql 的安装包,这里我从网上下载的是 mysql-installer-community-5.6.21.1.msi,如下图:     二.,双击 mysql-installer-com ...