作者 | 沉默王二
头图 | CSDN 下载自视觉中国
二哥,你能给我说说为什么 String 是 immutable 类吗?我想研究它,想知道为什么它就不可变了,这种强烈的g 8 v 8 a q { f |愿望就像想研究浩瀚的星空一样。但无奈自身功力有限,始终觉得雾里看花终隔一层。二哥你的文, : h b J章总是充满[ b S } ( L A趣味性,我想一定能够说明白,我也一定能够看明白,能在接下来写一写吗?
收到读者小 R 的私信后,我就总感觉自己有一{ 3 G种义不容辞的责任,非要把 immuts | 5 a 7 V L 4 sable 类说明白,否则我就怎^ k ~ P Q X F么地—S d I x Q { # h—你说了算!
什么是不可变类
一个类的` F ] V m对象在通过构造方法创建后如果状态不会再被改变,那么它就是一个不可变(immutable)类。它的所有成员变量的赋值仅在构造方法中完成,不会提供任何 setter 方法供外部类去修改。
还记得《神雕侠侣》中小龙女的古墓吗?随着那一声巨响,仅有的通道就被无情地关闭了。别较真那个密道,我这么说只是为了打开你的想象力,让你对不可变类有一个更直观的印象。
自从有了多线程,生产力就被无限地放大了,所有的程序员都爱它,因为强大的硬件能力被充分地利用了。但与此同时,所有的程序员都对它心生忌惮,因为一不小心,多线程就会把对象的状态变得混乱不堪。
为了保护状态的原子性、可2 # r见性、有序性,我们程序员可以说是| P = 2 9 g x Z A竭尽所能。其中,synchroni) / q L @ j mzed(同步)关键字是最简单最入门= J f l ~ } P I的一种解决方案。
假如说类是不可变的,那么对象的状态就也是不可变的。这样的话,每次修改对象的状态,就会产生一个新的对象供不同的线程使用,我们程序员就不必再担心并发问题了。
常见的不可变类
提到不可变类,几乎所有的程序员第一个想到的,就是 String 类。那为什么 String 类要被设计成不可变的呢?
1)常量池的需要
字符串常量池是 Java 堆内存中一个特殊的存储区域,当创W J r k 7 ( -建一个 String 对象时,假如此字符串在常量池中不存在,那y u * n P 4 |么就创建一个;假如已经存,就不会再创建了,而是直接引用已经存在的对象。这样做能够减少 JVM 的内存开销,提高效率。
2)hashCode 的需要
因为字符串是不可变的,所以在它创建的时候,其 hashCode 就被缓存了,因此非常适合作为哈希值(比d # ; k p如说作为 HashMap 的键),多次调用只返回同一个值,来提高效率。
3)线程安/ t a ) ; c ! b全
就像之前* } 5 ! Y S说的那样,如果对象的状态是可变的,那么在多线程环境下,就很容易造成不可预期的结果。而 String 是不可变的,就可以在多个线程之间共享,不需要同步处理。
因此,当我们调用 String 类的任何方法(比如说 trim()、substring()、toLowerCase())时,总会返回一个新的对象,而不影响之前的值。
1String cmower = \"沉默王二} V ) ` $,一枚有趣的程序员\";
2cmower.substring(0,4);
3System.out.println(cmower);// 沉默王二s : 0 q ? @ n i e,一枚有趣的程序员虽然调用 suy k ( Obstring 方法对 cmower 进行了截取,但 cmower 的值没有改变。
除了 String 类,包装器类4 ? J ^ Integer、% f D u _ C $Long 等也是不e : 3 V 0 % H G可变类。
自定义不可变类
看懂一个不可变类也许容? A % s ; S T n易,但要创建一个自定义的不可变类9 0 } | _ Z恐怕就有点难了。但知难而进是我们作为一名优秀的程序员不可或缺的品质,正因为不容易,我们才能真正地掌握它。
接下来,就请和我一起,来自定义一个不可变类吧。一个不可变诶,必须要满足以下 4 个条件:
1)确保类是 final 的,不允许被其他类继承。
2)确保所有的成员v F 9 B 5 U变量(字段)是 fX Q j Ainal 的,这样的话,它们就只能在构造方法中初始化值,并且不会在随后被修改。
3)不要提供任何 setter 方法。
4)如果要修改类的状态,必须返回` ? A J - ) r w l一个新* W ~ W |的z = I a对象。
按照以上条件,我们来自定义y 4 M Y Z一个简单的不可变类 Writer。
1public final class Writer {
2 pri2 z w / 8 H 1 =vate final String name;
3 private final int age;
4
5 public Writer(String name, int ag* B 7 _e) {
6 this.name, U F : O 4 = name;
7 this.age = age;
8 }
9
10 public int getAge {
11 return age;
12 }
13
14 public String getName {
15 return name;
1E A _ 8 | C d H 6 }
17}Writer 类是 final 的,name 和 age 也是 fin9 ~ eal 的,没有 setter 方法。
OK,据说这个作者分享了很多博客,广受读者的喜爱,因此某某出版社找他写了一本书(Book)。Book 类是这样定义的:
1public class Book {
2 private String name;
3 private iG c = I = J Ant price;
4
5 public String getName {
6 return name;
7 }
8
9 public void setName(String name) {
10 this.h = _ D 5 2 T 2 Jname = name;
11 }
12
13 public int getPrice {
14 return prid G x * j , 5 5 Oce;
15 }
16
17 public void setPrice(int price) {
18 this.price = price;
19 }
20
21 @Override
22 public String toString {
23 return \"Book{\" +
24 \"name=\'\" + name + \'\\\'\' +
25 \", price=\" + price +
260 4 q k ( r q 6 ` \'}\';
27 }
28}2 个字段,分别是 name 和 price,以及 getter 和 setter,重写后的 toString 方法。然后,在 Writer 类中追加一个可变对象字段 book。
1public final class Writer {
2 privatB C u K % ? ^ C 6e final String name;
3 private final int age;
4 private final Book book;
5
6 public Writer(1 1 L !String name, int age, Book book) {
7 this.name = name;
8 this.age = age;
9 this.book = book;
10 }
11
12 public int getAge {
13 return age;
1V N 0 m B4 }
15
16 public String getName {
17 return name;
18 }
19
20 public Book getBook {
21 return book;
22 }
23}并在构造方法中追加了 Boq @ = uok 参数,以及 Book 的 getter 方法。
完成以9 I v ( J 3 $上工作后,我们来新建一个测试类,看看 Writer 类的状态是否真的不可变。
1public class WriterDemo {
2 public stx v C r Zatic void main(String[] args) {
3 BK q Kook book = new Book;
4 book.setName(\"Web全栈开发进阶之路\");
5 book.setPrice(79);
6
7 Writer writer = new Writer(\"沉默王二\G 7 ! K a { m | n",18, book);
8 System.out.println(\"定价:\" + wrC s ;iter.getBook);
9 w$ o f _ Uriter.getBook.setPrice(59);
10 System.f W B U f 9 b qout.println(\"促销价:\" + writer.getBook);
11 }
12}程序输出的结果如下所示:
1定价:Book{name=\'Web全栈开发进阶之路\', price=79}
2促销价:B: v M H V + D ` .ook{nameJ @ ) 2 p V d ( 3=\x i X h U'Web全栈开发进阶之路\', price=59}糟糕,W` p c S r q R g Yriter 类的不可变性被破坏了,价格发生了变化。为了解决这个问题,我们需要为不可U U ! [ `变类的定义规则追加一条内容:
如果一个不可变类中包含了可变类的对象,那么就需要确保返回的是可变对象的副本。也就是C G R b W p 0 w说,Writer 类中的 getBook 方法应该修改为:
1public Book getBon o ! ^ C | ok {
2 Book clone = n. t _ _ W ~ew Book;
3 clone.setPrice(tn A t m s 1 Xhis.book.getPrice);
4 clone.setName(T j U Wthis.book.getName);
5 return clone;
6}这样的话,构造方法初始化后的 Book 对象就不会再被修改了。此时,运行 Wrx I 1 , a GiterDemo,就会发现价格不再发生变化了。
1定价:Book{name=\'Web全栈开发进阶之路\', price=79}
2促销价:Book{name=\'Web全栈开发进阶之路\', price=79}总结
不可变类有很多优点,就像之前提到的 Strinj t . u g 9 Jg 类那样,尤其是在{ 0 z d } W L r多线程环境下,它非常的安全。尽管每次修改都会创建一个新的对象,增加了内存的消耗,i = w ! ~但这个缺点相比它带来的优点,显然是微不足道的——无非就是捡了西瓜,丢了芝麻。