二哥,你能告诉我为什么String是不可变类(不可变对象)吗?我想研究它,想知道为什么它是不可变的。这种强烈的愿望,就像是想要研究浩瀚的星空一样。但无奈,自己的本事有限,总觉得雾中看花终是别离。二哥,你的文章总是充满趣味。我觉得你解释的很清楚,我也能看懂。可以接着写吗?https://github.com/itwanger/toBeBetterJavaer01,什么是不可变类的对象如果通过构造函数创建后其状态不能改变,则该类的对象是不可变类。其所有成员变量的赋值只在构造函数中完成,不提供任何setter方法供外部类修改。还记得《神雕侠侣》里的小龙女古墓吗?随着那一声巨响,唯一的通道被无情的关闭。密道别当真,我说这些只是为了打开你的想象力,让你对不可变类有一个更直观的印象。自从多线程问世以来,生产力被无限放大,所有程序员都爱不释手,因为强大的硬件能力得到了充分发挥。但与此同时,所有的程序员都害怕它,因为一不小心,多线程就会把对象的状态弄乱。为了保护状态的原子性、可见性和有序性,我们程序员可以说是拼尽全力了。其中,synchronized(同步)关键字是最简单最入门的解决方案。如果类是不可变的,那么对象的状态也是不可变的。这样,每次修改对象的状态,都会产生一个新的对象供不同的线程使用,我们的程序员再也不用担心并发问题了。02.常见的不可变类说到不可变类,几乎所有程序员首先想到的都是String类。那么为什么String类要设计成不可变的呢?1)常量池的必要性字符串常量池是Java堆内存中的一个特殊的存储区域。创建String对象时,如果常量池中不存在该字符串,则创建一个;如果已经存在,则不会再次创建,而是直接引用已存在的对象。这样做可以减少JVM的内存开销,提高效率。2)需要hashCode因为字符串是不可变的,它的hashCode在创建的时候就被缓存了,所以很适合作为hash值(比如作为HashMap的key),多次调用只返回相同的值提高效率。3)线程安全前面说过,如果对象的状态是可变的,在多线程环境下很容易造成不可预知的结果。但是,String是不可变的,可以在不同步的情况下在多个线程之间共享。因此,当我们调用String类的任何方法(如trim()、substring()、toLowerCase())时,总会返回一个新的对象,而不会影响之前的值。Stringcmower="沉默王二,一个有趣的程序员";cmower.substring(0,4);System.out.println(cmower);//沉默王二,一个有趣的程序员虽然调用了substring()方法拦截了cmower,但是cmower的值并没有改变。除了String类之外,Integer、Long等包装类也是不可变类。03.动手做不可变类理解一个不可变类可能很容易,但是创建一个自定义的不可变类可能有点困难。但是知难而进,是我们作为一个优秀的程序员不可或缺的品质。正因为不容易,我们才能真正掌握它。接下来,请和我一起定义一个不可变类。一个不可变的必须满足以下四个条件:1)保证类是final的,不允许被其他类继承。2)确保所有成员变量(字段)都是final的,这样它们只能在构造函数中用值初始化,而不能在后续修改。3)不要提供任何setter方法。4)如果要修改类的状态,必须返回一个新的对象。根据以上条件,我们来定义一个简单的不可变类Writer。publicfinalclassWriter{privatefinalStringname;privatefinalintage;publicWriter(Stringname,intage){this.name=name;this.age=age;}publicintgetAge(){returnage;}publicStringgetName(){returnname;}}Writer类是final的,名字和年龄就可以了也是最终的并且没有setter方法。OK,据说这位作者分享了很多博客,广受读者喜爱,所以某出版社找他写了一本书(Book)。Book类的定义如下:;}@OverridepublicStringtoString(){return"Book{"+"name='"+name+'\''+",price="+price+'}';}}2个字段,分别是name和price,getter和setter,重写的toString()方法。然后,将可变对象字段book附加到Writer类。publicfinalclassWriter{privatefinalStringname;privatefinalintage;privatefinalBookbook;publicWriter(Stringname,intage,Bookbook){this.name=name;this.age=age;this.book=book;}publicintgetAge(){returnage;}publicStringgetName(){returnname;}publicBookgetBook(){returnbook;}}并在构造方法中添加Book参数和Bookgetter方法。完成以上工作后,我们再新建一个测试类,看看Writer类的状态是否真的不可变。publicclassWriterDemo{publicstaticvoidmain(String[]args){Bookbook=newBook();book.setName("Web全栈开发进阶之路");book.setPrice(79);Writerwriter=newWriter("沉默之王2",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()方法应该修改为:publicBookgetBook(){Bookclone=newBook();clone.setPrice(this.book.getPrice());clone.setName(this.book.getName());returnclone;}在这种情况下,构造函数初始化的Book对象将不会被修改。此时,运行WriterDemo,你会发现价格不再变化。定价:Book{name='Web全栈开发进阶之路',price=79}促销价:Book{name='Web全栈开发进阶之路',price=79}04.不可变类总结很多优点,就像之前提到的String类,尤其是在多线程环境下,是非常安全的。虽然每次修改都会创建一个新的对象,增加内存消耗,但是这个缺点和它的优点相比显然是微不足道的——无非是捡了西瓜丢了芝麻。
