一、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. mysql(三) 数据表的基本操作操作

    mysql(三) 数据表的基本操作操作 创建表,曾删改查,主键,外键,基本数据类型. 1. 创建表 create table 表名( 列名 类型 是否可以为空, 列名 类型 是否可以为空 )ENGIN ...

  2. python数据结构之选择排序

    选择排序(select_sort)是一个基础排序,它主要通过查找已给序列中的元素的最大或者最小元素,然后将其放在序列的起始位置或者结束位置,并通过多次这样的循环完成对已知序列的排序,在我们对n个元素进 ...

  3. Junit4单元测试的基本用法

    看了一些Junit4的视频,简单了解了Junit4的一些基本用法,整理记录一下. 环境搭建 这里使用的开发工具是MyEclipse,首先新建一个Java工程,将Junit4的jar包引入,eclips ...

  4. Vue项目初始

    利用npm搭建Vue项目流程 安装 第一步: 官方下载node 或者pip install node 第二步:可忽略 :  npm install npm@latest -g 更新最新的稳定版本 第三 ...

  5. 【整理】Java 9新特性总结

    距Java 8正式发布三年多时间,Java 9 于2017年9月21日正式发布, 你可能已经听说过 Java 9 的模块系统(讨论的最多的),但是这个新版本还有许多其它的更新. 这里我整理了Java ...

  6. FTL常用标签及语法

    判断对象是否存在,若成立说明存在 <#if blockObjList ??></#if> <#if blockObjList ??>  <#else>  ...

  7. LeetCode(119. 杨辉三角 II)

    问题描述 给定一个非负索引 k,其中 k ≤ 33,返回杨辉三角的第 k 行. 在杨辉三角中,每个数是它左上方和右上方的数的和. 示例: 输入: 3 输出: [1,3,3,1] 进阶: 你可以优化你的 ...

  8. C++ 线段树—模板&总结

    在信息学竞赛中,经常遇到这样一类问题:这类问题通常可以建模成数轴上的问题或是数列的问题,具体的操作一般是每次对数轴上的一个区间或是数列中的连续若干个数进行一种相同的处理.常规的做法一般依托于线性表这种 ...

  9. React Native小白入门学习路径——一

    前言 过去这段时间一直忙着实验室考核任务,拼尽全力完成了自己的任务之后.正准备开始高强度的实验室的学习的时候,实验室组织了新老生交流会,这也应该是头一次这么近距离的面对大四前辈交流想法.感觉自己受益颇 ...

  10. 通过Quartz 配置定时调度任务:使用cron表达式配置时间点

    Cron官网入口 在后台经常需要一些定时处理的任务,比如微信相关应用所需的access_token,就要定时刷新,官方返回的有效性是7200s,也就是2小时,但是为了保险起见,除了在发现access_ ...