一、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. 李宏毅机器学习笔记5:CNN卷积神经网络

    李宏毅老师的机器学习课程和吴恩达老师的机器学习课程都是都是ML和DL非常好的入门资料,在YouTube.网易云课堂.B站都能观看到相应的课程视频,接下来这一系列的博客我都将记录老师上课的笔记以及自己对 ...

  2. typescript精简版1:用ts表示常见数据类型

    一:工程准备: 1.全局安装typescript npm i typescript -g // 或 yarn global add typescript vscode 配置 在命令行执行 tsc -- ...

  3. linux 学习笔记 软件包管理

    >查询文件所属软件包 #rpm -qf /usr/share/pixmaps/xplns.png 查询某个文件所属软件包 >查询软件包所包含文件列表 #rpm -ql xplus-3.3. ...

  4. es6那些事儿

    一.参考链接 http://www.ecma-international.org/ecma-262/6.0/index.html http://www.ecma-international.org/e ...

  5. UVA 207 PGA Tour Prize Money

    知识补充: ①:ssprintf: int sprintf(char *str, const char *format, ...) 发送格式化输出到 str 所指向的字符串. char str[80] ...

  6. WPFToolkit DataGrid 使用介绍zz

      首先:这版本需要.NetFrameWork SP1的支持 WPF Toolkit - February 2010 Release 下载地址:http://wpf.codeplex.com/rele ...

  7. AE模板

    == 蓝色浩瀚星辰中一道光速展示企业LOGO AE模板 科技公司品牌开场动画. == 线条科技感图片商品介绍 Tech Grid Show免费下载 科技科幻风格. 产品介绍不错. == 炫光三维金属质 ...

  8. C# 中BindingSource 的用法

    .引言 BindingSource组件是数据源和控件间的一座桥,同时提供了大量的API和Event供我们使用.使用这些API我们可以将Code与各种具体类型数据源进行解耦:使用这些Event我们可以洞 ...

  9. Eclipse纯净版安装web插件

    打开 Help -> Install New Software. 在Install界面板中,点击Add按钮输入:然后在输入http://download.eclipse.org/releases ...

  10. Java笔记(五)泛型

    泛型 一.基本概念和原理 泛型将接口的概念进一步延申,“泛型”的字面意思是广泛的类型. 类.接口和方法都可以应用于非常广泛的类型,代码与它们能够操作 的数据类型不再绑定到一起,同一套代码可以应用到多种 ...