浅谈java中String类的不可变性(immutability)和final关键字修饰

要分析String类为什么是不可变的,首先我们需要弄明白什么是不可变类。

So what is 不可变类?

Java中分为可变类和不可变类。不可变类是指当创建了这个类的实例后,就不允许修改它的属性值。在JDK的基本类库中,所有基本类型的包装类,例如Integer,Long等,都是不可变类,java.lang.String也是不可变类,虽然他不是基本类型。
java中的基本类型变量:boolean,byte,char,double,float,int,long,short
java中的不可变类:JDK中的java.lang中的Boolean,Byte,Character,Double,Float,Integer,Long,Short,String,BigInteger
JDK中可变类,例如:StringBuffer类,java.util.Date

可变类和不可变类(Mutable and Immutable Objects)的初步定义:

可变类:当你获得这个类的一个实例引用时,你可以改变这个实例的内容。

不可变类:在一个实例被创建完成之后,不能再改变其状态。这包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变。

下面让我们先来看个例子:

String str = "hello";
System.out.println(str);
str = "world";
System.out.println(str)

输出结果:
hello
world

首先创建一个String对象str,然后让str的值为“hello”, 然后又让s的值为“world”。 从打印结果可以看出,s的值确实改变了。啥玩意儿?你不是要讨论String类是不可变的么?那么怎么还说String对象是不可变的呢?

区分对象和对象的引用

上述例子对于刚刚接触java的小鲜肉们来说往往存在这样的误区:str只是一个string对象的引用,并不是对象本身。对象在内存中是一块内存区;引用里面只是存放了它所指向的对象的地址,通过这个地址可以访问对象。所以上面例子中,str只是一个引用,它指向了一个具体的对象,即”hello”。当str=”world”; 这句代码执行过之后,又创建了一个新的对象”world”, 而引用str重新指向了这个新的对象,原来的对象”hello”还在内存中存在,并没有改变。

内存结构如下图所示:

这里写图片描述

为什么String对象是不可变的?

下面我们一起来看1.7JAVA的String类源码:

public final class String
    implements java.io.Serializable,Comparable<string>,CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0</string>

从上面的源码我们可以发现,String类其实就是对字符数组的封装。value[]中的所有字符都是属于String这个对象的,不过这个value他也是一个引用,指向的是一个数据对象。

真正的内存布局如图:

这里写图片描述

由于value他是私有的所以外部是无法修改它的。并且它还是final的,也就是说一旦String被初始化了,那么它就不能被改变了。
有的人以为故事就这样完了,其实没有。因为虽然value是不可变,也只是value这个引用地址不可变。挡不住Array数组是可变的事实。

final int[] value={1,2,3};
int[] another={4,5,6};
value=another;    //编译器报错,final不可变

value用final修饰,编译器不允许我把value指向堆区另一个地址。但如果我直接对数组元素动手,分分钟搞定。

final int[] value={1,2,3};
Array.set(array,2,100); //数组也被改成{1,2,100}

所以String是不可变,关键是因为SUN公司的工程师,在后面所有String的方法里很小心的没有去动Array里的元素,没有暴露内部成员字段。private final char value[]这一句里,private的私有访问权限的作用都比final大。而且设计师还很小心地把整个String设成final禁止继承,避免被其他人继承后破坏。所以String是不可变的关键都在底层的实现,而不是一个final。考验的是工程师构造数据类型,封装数据的功力。

下面我们再来看一个例子:

String str = "hello";
System.out.println("str = " + str);
str = str.replace('h', 'H');
System.out.println("str = " + str);

打印结果为:
str = hello
str = Hello

我们发现a的值看似改变了,其实这也是一个误区。再次说明, str只是一个引用, 不是真正的字符串对象,在调用str.replace(‘h’, ‘H’)时, 方法内部创建了一个新的String对象,并把这个新的对象重新赋给了引用str。String中replace方法的源码可以说明问题:

public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */

            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            if (i < len) {
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
           ******* return new String(buf, true); ******* 
            }
        }
        return this;
    }

注意加*的部分,其返回了一个新的String对象。

不过呢,通过反射我们还是可以去对String类中的内容去进行改变的。这个就设计到了反射的概念了,我就不提了。我们一般也不会这样去做。
我们只需要理解到String类是不可变的即可了!

String的不可变性和final的关系

我们知道String类用final修饰,网上经常有人将String的不可变性与final画上等号,其实这样是不合适的.

我们首先要弄明白final关键字究竟修饰了什么?

final使得被修饰的变量”不变”,但是由于对象型变量的本质是“引用”,使得“不变”也有了两种含义:引用本身的不变,和引用指向的对象不变。

引用本身:

final StringBuffer a=new StringBuffer("immutable"); 
final StringBuffer b=new StringBuffer("not immutable"); a=b;//编译期错误 

引用指向的对象:

final StringBuffer a=new StringBuffer("immutable"); 
a.append(" broken!"); //编译通过

可见,final只对引用的“值”(也即它所指向的那个对象的内存地址,也就是说那个变量)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会导致编译期错误。至于它所指向的对象的变化,final是不负责的。这很类似==操作符:==操作符只负责引用的“值”相等,至于这个地址所指向的对象内容是否相等,==操作符是不管的。

理解final问题有很重要的含义。许多程序漏洞都基于此—-final只能保证引用永远指向固定对象,不能保证那个对象的状态不变。在多线程的操作中,一个对象会被多个线程共享或修改,一个线程对对象无意识的修改可能会导致另一个使用此对象的线程崩溃。一个错误的解决方法就是在此对象新建的时候把它声明为final,意图使得它“永远不变”。其实那是徒劳的。

String为什么要用final修饰?

String基本约定中最重要的一条是immutable。的确声明String为final 和immutable是没有必然关系。但是假如String没有声明为final, 那么你的StringChilld就有可能是被复写为mutable的,这样就打破了成为共识的基本约定。
举个例子:一个方法可能本来接受String类型并返回其大写方式

public static String uppperString(String s){
        return s.toUpperCase();
}

你传入String 的s=”test”, 他不会修改字符串池中”test”, 而是直接新建立一个实例”TEST”返回。但如果你的StringChild的toUpperCase()被你重写(override)为mutable的方式,然后你调用这个方法的时候传入的是StringChild实例, 那么整体(依赖于方法uppperString的所有类)的行为就有可能出现错乱。
要知道,String是几乎每个类都会使用的类,特别是作为Hashmap之类的集合的key值时候,mutable的String有非常大的风险。而且一旦发生,非常难发现。

声明String为final一劳永逸。

以上内容受到以下两篇博文的启发:
http://blog.csdn.net/qq_25223941/article/details/50340107 —String类 理解—-final 不可变
http://www.2cto.com/kf/201401/272974.html —Java中的String为什么是不可变的? – String源码分析
http://blog.csdn.net/allen_zhao_2012/article/details/7839269 —J2SE基础夯实系列之String字符串不可变的理解,不可变类,final关键字到底修饰了什么

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值