为什么 String 是 immutable 类
二哥,你能给我说说为什么 String 是 immutable 类(不可变对象)吗?我想研究它,想知道为什么它就不可变了,这种强烈的愿望就像想研究浩瀚的星空一样。但无奈自身功力有限,始终觉得雾里看花终隔一层。二哥你的文章总是充满趣味性,我想一定能够说明白,我也一定能够看明白,能在接下来写一写吗?
收到读者小 R 的私信后,我就总感觉自己有一种义不容辞的责任,非要把 immutable 类说明白,否则我就怎么地——你说了算!
01、什么是不可变类
一个类的对象在通过构造方法创建后如果状态不会再被改变,那么它就是一个不可变(immutable)类。它的所有成员变量的赋值仅在构造方法中完成,不会提供任何 setter 方法供外部类去修改。
还记得《神雕侠侣》中小龙女的古墓吗?随着那一声巨响,仅有的通道就被无情地关闭了。别较真那个密道,我这么说只是为了打开你的想象力,让你对不可变类有一个更直观的印象。
自从有了多线程,生产力就被无限地放大了,所有的程序员都爱它,因为强大的硬件能力被充分地利用了。但与此同时,所有的程序员都对它心生忌惮,因为一不小心,多线程就会把对象的状态变得混乱不堪。
为了保护状态的原子性、可见性、有序性,我们程序员可以说是竭尽所能。其中,synchronized(同步)关键字是最简单最入门的一种解决方案。
假如说类是不可变的,那么对象的状态就也是不可变的。这样的话,每次修改对象的状态,就会产生一个新的对象供不同的线程使用,我们程序员就不必再担心并发问题了。
02、常见的不可变类
提到不可变类,几乎所有的程序员第一个想到的,就是 String 类。那为什么 String 类要被设计成不可变的呢?
1)常量池的需要
字符串常量池是 Java 堆内存中一个特殊的存储区域,当创建一个 String 对象时,假如此字符串在常量池中不存在,那么就创建一个;假如已经存,就不会再创建了,而是直接引用已经存在的对象。这样做能够减少 JVM 的内存开销,提高效率。
2)hashCode 的需要
因为字符串是不可变的,所以在它创建的时候,其 hashCode 就被缓存了,因此非常适合作为哈希值(比如说作为 HashMap 的键),多次调用只返回同一个值,来提高效率。
3)线程安全
就像之前说的那样,如果对象的状态是可变的,那么在多线程环境下,就很容易造成不可预期的结果。而 String 是不可变的,就可以在多个线程之间共享,不需要同步处理。
因此,当我们调用 String 类的任何方法(比如说 trim()
、substring()
、toLowerCase()
)时,总会返回一个新的对象,而不影响之前的值。
String cmower = "沉默王二,一枚有趣的程序员";
cmower.substring(0,4);
System.out.println(cmower);// 沉默王二,一枚有趣的程序员
虽然调用 substring()
方法对 cmower 进行了截取,但 cmower 的值没有改变。
除了 String 类,包装器类 Integer、Long 等也是不可变类。
03、自定义不可变类
看懂一个不可变类也许容易,但要创建一个自定义的不可变类恐怕就有点难了。但知难而进是我们作为一名优秀的程序员不可或缺的品质,正因为不容易,我们才能真正地掌握它。
接下来,就请和我一起,来自定义一个不可变类吧。一个不可变诶,必须要满足以下 4 个条件:
1)确保类是 final 的,不允许被其他类继承。
2)确保所有的成员变量(字段)是 final 的,这样的话,它们就只能在构造方法中初始化值,并且不会在随后被修改。
3)不要提供任何 setter 方法。
4)如果要修改类的状态,必须返回一个新的对象。
按照以上条件,我们来自定义一个简单的不可变类 Writer。
public final class Writer {
private final String name;
private final int age;
public Writer(String name, int age) {
this.name = name;
this.age = age;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
}
Writer 类是 final 的,name 和 age 也是 final 的,没有 setter 方法。
OK,据说这个作者分享了很多博客,广受读者的喜爱,因此某某出版社找他写了一本书(Book)。Book 类是这样定义的:
public class Book {
private String name;
private int price;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}
2 个字段,分别是 name 和 price,以及 getter 和 setter,重写后的 toString()
方法。然后,在 Writer 类中追加一个可变对象字段 book。
public final class Writer {
private final String name;
private final int age;
private final Book book;
public Writer(String name, int age, Book book) {
this.name = name;
this.age = age;
this.book = book;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
public Book getBook() {
return book;
}
}
并在构造方法中追加了 Book 参数,以及 Book 的 getter 方法。
完成以上工作后,我们来新建一个测试类,看看 Writer 类的状态是否真的不可变。
public class WriterDemo {
public static void main(String[] args) {
Book book = new Book();
book.setName("Web全栈开发进阶之路");
book.setPrice(79);
Writer writer = new Writer("沉默王二",18, book);
System.out.println("定价:" + writer.getBook());
writer.getBook().setPrice(59);
System.out.println("促销价:" + writer.getBook());
}
}
程序输出的结果如下所示:
定价:Book{name='Web全栈开发进阶之路', price=79}
促销价:Book{name='Web全栈开发进阶之路', price=59}
糟糕,Writer 类的不可变性被破坏了,价格发生了变化。为了解决这个问题,我们需要为不可变类的定义规则追加一条内容:
如果一个不可变类中包含了可变类的对象,那么就需要确保返回的是可变对象的副本。也就是说,Writer 类中的 getBook()
方法应该修改为:
public Book getBook() {
Book clone = new Book();
clone.setPrice(this.book.getPrice());
clone.setName(this.book.getName());
return clone;
}
这样的话,构造方法初始化后的 Book 对象就不会再被修改了。此时,运行 WriterDemo,就会发现价格不再发生变化了。
定价:Book{name='Web全栈开发进阶之路', price=79}
促销价:Book{name='Web全栈开发进阶之路', price=79}
04、总结
不可变类有很多优点,就像之前提到的 String 类那样,尤其是在多线程环境下,它非常的安全。尽管每次修改都会创建一个新的对象,增加了内存的消耗,但这个缺点相比它带来的优点,显然是微不足道的——无非就是捡了西瓜,丢了芝麻。
如果觉得文章对你有点帮助,请微信搜索「 沉默王二 」第一时间阅读,回复【666】【1024】更有我为你精心准备的 500G 高清教学视频(已分门别类),以及大厂技术牛人整理的面经一份。
为什么 String 是 immutable 类的更多相关文章
- 创建immutable类
不可变对象(immutable objects) 那么什么是immutable objects?什么又是mutable Objects呢? immutable Objects就是那些一旦被创建,它们的 ...
- Java中如何创建不可变(immutable)类
什么是不可变类 1. 不可变类是指类的实例一经创建完成,这个实例的内容就不会改变. 2. Java中的String和八个基本类型的包装类(Integer, Short, Byte, Long, Dou ...
- Why string is immutable in Java ?
This is an old yet still popular question. There are multiple reasons that String is designed to be ...
- Why String is immutable in Java ?--reference
String is an immutable class in Java. An immutable class is simply a class whose instances cannot be ...
- Why String is Immutable or Final in Java
The string is Immutable in Java because String objects are cached in String pool. Since cached Strin ...
- JAVA —— String is immutable. What exactly is the meaning? [duplicate]
question: I wrote the following code on immutable Strings. public class ImmutableStrings { public st ...
- 为什么Java中的String是设计成不可变的?(Why String is immutable in java)
There are many reasons due to the string class has been made immutable in Java. These reasons in vie ...
- JAVA 中为什么String 是immutable的
本文翻译自:http://www.programcreek.com/2013/04/why-string-is-immutable-in-java/ 这是一个很老但很流行的问题,这里有几个原因Stri ...
- string、math类、random随机数、datetime、异常保护
今天讲的知识点比较多,比较杂,以至于现在脑子里还有点乱,慢慢来吧... string (1)string.length; (获得你string字符串的长度) (2)a = a.Trim(); 重新赋值 ...
随机推荐
- Zabbix调用外部脚本发送邮件:python编写脚本
Zabbix调用外部脚本发送邮件的时候,会在命令行传入两个参数,第一个参数就是要发送给哪个邮箱地址,第二个参数就是邮件信息,为了保证可以传入多个参数,所以假设有多个参数传入 #!/usr/bin/en ...
- sql执行过程
作为一个程序员,几乎所有人都使用过 SQL 语言,无论是在命令行执行.程序调用,还是在 SQL 工具里,你都做过这样的事:写一个规范的 SQL 语句,然后等待数据库返回的结果,然后再基于结果做各种逻辑 ...
- C++笔试常见问题
C++提供的编译预处理功能主要有以下三种: 宏定义 文件包含 条件编译 预编译又称为预处理 , 是做些代码文本的替换工作.处理#开头的指令 , 比如拷贝 #include包含的文件代码, #defin ...
- mysql--sql_mode报错整理
1.在5.7版本以上mysql中使用group by语句进行分组时, 如果select的字段 , 不是完全对应的group by后面的字段 , 有其他字段 , 那么就会报这个错误 ERROR 1055 ...
- c++ 如何清除上一次的输出?
#include <iostream.h>#include <stdlib.h>int main(){cout<<"PBY PBY PBY PBY PBY ...
- linux增加history时间戳
增加环境变量到/etc/profile export HISTTIMEFORMAT="%Y-%m-%d %H:%M:%S " export HISTSIZE=9999
- js之构造函数的理解
在JavaScript中,创建对象的方式包括两种:对象字面量和使用new表达式.对象字面量是一种灵活方便的书写方式,例如: 1 2 3 4 5 6 var o1 = { p:”I’m in ...
- Python爬虫开发教程
正文 现在Python语言大火,在网络爬虫.人工智能.大数据等领域都有很好的应用.今天我向大家介绍一下Python爬虫的一些知识和常用类库的用法,希望能对大家有所帮助.其实爬虫这个概念很简单,基 ...
- 二叉堆的BuildHeap操作
优先队列(二叉堆)BuildHeap操作 \(BuildHeap(H)\)操作把\(N\)个关键字作为输入并把它们放入空堆中.显然,这可以使用\(N\)个相继的\(Insert\)操作来完成.由于每个 ...
- 聊一聊关于MySQL的count(*)
0.背景 自从大家对于MySQL数据库的稳定性有了更高的追求后,经常有小伙伴有这样的疑问,对于count(*)这样的操作,有没有正确的姿势,或者有没有可以优化的地方? 但答案比较残酷,如果已经使用了正 ...