一、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. 真正“搞”懂http协议01—背景故事

    去年读了<图解HTTP>.<图解TCP/IP>以及<图解网络硬件>但是读了之后并没有什么深刻的印象,只是有了一层模糊的脉络,刚好最近又接触了一些有关http的相关内 ...

  2. redis+mysql

    redis和mysql要根据具体业务场景去选型 mysql:数据放在磁盘   redis:数据放在内存 redis适合放一些频繁使用,比较热的数据,因为是放在内存中,读写速度都非常快,一般会应用在下面 ...

  3. 双11线上压测netty内存泄露

    最近线上压测,机器学习模型第一次应用到线上经历双11大促.JSF微服务有报错 LEAK: ByteBuf.release() was not called before it's garbage-co ...

  4. LOJ.6053.简单的函数(Min_25筛)

    题目链接 Min_25筛见这里: https://www.cnblogs.com/cjyyb/p/9185093.html https://www.cnblogs.com/zhoushuyu/p/91 ...

  5. NEO学习笔记,从WIF到地址

    2018开年,先给大家拜个年,Happy Neo Year. 开年总得写点什么,就打算继续开学习笔记系列,一点一点仔细的去分析NEO. 今天说一说从WIF到地址的这一串关系.   简单说就一张图: 或 ...

  6. Linux vi/vim命令高效助记图

    图片来源网上,如有侵权,请告知,我会删除掉,谢谢~ 常用编辑按键: 1 vi +[num] file 打开文件,并将光标置于第n行首 2 vi + file 打开文件,并将光标置于最后一行首 3 vi ...

  7. BZOJ3490 : Pa2011 Laser Pool

    与横线以及竖线的交点个数很容易求,那么只要求出横线竖线交点与运动轨迹的交点数即可. 运动轨迹可以划分成若干条贯穿边界的斜线,对于第一条和最后一条,可以用bitset暴力统计. 对于中间的部分,斜线都是 ...

  8. python网络编程(十三)

    协程-greenlet版 为了更好使用协程来完成多任务,python中的greenlet模块对其封装,从而使得切换任务变的更加简单 安装方式 使用如下命令安装greenlet模块: sudo pip ...

  9. python网络编程(十)

    select版-TCP服务器 1. select 原理 在多路复用的模型中,比较常用的有select模型和epoll模型.这两个都是系统接口,由操作系统提供.当然,Python的select模块进行了 ...

  10. python-校验密码小练习

    #校验密码是否合法的小练习#1.密码长度5到10位:#2.密码里面必须包含,大写字母,小写字母,数字#3.最多输入5次 写程序过程中遇到了两个问题,第二个循环里的P是把password的值循环传到p里 ...