所在的位置: java >> 历史起源 >> 干货Java中的String为

干货Java中的String为

在Java中,如果一个对象在创建后,它的状态不能改变,那么我们就认为这个对象是不可变的,即对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变。

而在Java中,String类就是一个不可变对象的好例子。一旦创建String对象后,我们不能对它的状态进行改变。我们可以创建新的String对象,但是不能改变原有的String对象。

区分对象和对象的引用

对于Java初学者,对于String是不可变对象总是存有疑惑。看下面代码:

Strings=ABCabc;

Systm.out.println(s=+s);

s=;

Systm.out.println(s=+s);

打印结果为:

s=ABCabc

s=

首先创建一个String对象s,然后让s的值为“ABCabc”,然后又让s的值为“”。从打印结果可以看出,s的值确实改变了。那么怎么还说String对象是不可变的呢?其实这里存在一个误区:s只是一个String对象的引用,并不是对象本身。对象在内存中是一块内存区,成员变量越多,这块内存区占的空间越大。引用只是一个4字节的数据,里面存放了它所指向的对象的地址,通过这个地址可以访问对象。

也就是说,s只是一个引用,它指向了一个具体的对象,当s=“”;这句代码执行过之后,又创建了一个新的对象“”,而引用s重新指向了这个心的对象,原来的对象“ABCabc”还在内存中存在,并没有改变。内存结构如下图所示:

Java和C++的一个不同点是,在Java中不可能直接操作对象本身,所有的对象都由一个引用指向,必须通过这个引用才能访问对象本身,包括获取成员变量的值,改变对象的成员变量,调用对象的方法等。而在C++中存在引用,对象和指针三个东西,这三个东西都可以访问对象。其实,Java中的引用和C++中的指针在概念上是相似的,他们都是存放的对象在内存中的地址值,只是在Java中,引用丧失了部分灵活性,比如Java中的引用不能像C++中的指针那样进行加减运算。

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

要理解String的不可变性,首先看一下String类中都有哪些成员变量。在JDK1.6中,String的成员变量有以下几个:

publicfinalclassString

implmntsjava.io.Srializabl,ComparablString,CharSqunc

{

/**Thvaluisusdforcharactrstorag.*/

privatfinalcharvalu[];

/**Thoffstisthfirstindxofthstoragthatisusd.*/

privatfinalintoffst;

/**ThcountisthnumbrofcharactrsinthString.*/

privatfinalintcount;

/**Cachthhashcodforthstring*/

privatinthash;//Dfaultto0

在JDK1.7中,String类做了一些改动,主要是改变了substring方法执行时的行为,这和本文的主题不相关。JDK1.7中String类的主要成员变量就剩下了两个:

publicfinalclassString

implmntsjava.io.Srializabl,ComparablString,CharSqunc{

/**Thvaluisusdforcharactrstorag.*/

privatfinalcharvalu[];

/**Cachthhashcodforthstring*/

privatinthash;//Dfaultto0

由以上的代码可以看出,在Java中String类其实就是对字符数组的封装。JDK6中,valu是String封装的数组,offst是String在这个valu数组中的起始位置,count是String所占的字符的个数。在JDK7中,只有一个valu变量,也就是valu中的所有字符都是属于String这个对象的。这个改变不影响本文的讨论。除此之外还有一个hash成员变量,是该String对象的哈希值的缓存,这个成员变量也和本文的讨论无关。在Java中,数组也是对象(可以参考我之前的文章java中数组的特性)。所以valu也只是一个引用,它指向一个真正的数组对象。其实执行了Strings=“ABCabc”;这句代码之后,真正的内存布局应该是这样的:

valu,offst和count这三个变量都是privat的,并且没有提供stValu,stOffst和stCount等公共方法来修改这些值,所以在String类的外部无法修改String。也就是说一旦初始化就不能修改,并且在String类的外部不能访问这三个成员。此外,valu,offst和count这三个变量都是final的,也就是说在String类内部,一旦这三个值初始化了,也不能被改变。所以可以认为String对象是不可变的了。

那么在String中,明明存在一些方法,调用他们可以得到改变后的值。这些方法包括substring,rplac,rplacAll,toLowrCas等。例如如下代码:

Stringa=ABCabc;

Systm.out.println(a=+a);

a=a.rplac(A,a);

Systm.out.println(a=+a);

打印结果为:

a=ABCabc

a=aBCabc

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

小伙伴们也可以自己查看其他方法,都是在方法内部重新创建新的String对象,并且返回这个新的对象,原来的对象是不会被改变的。这也是为什么像rplac,substring,toLowrCas等方法都存在返回值的原因。也是为什么像下面这样调用不会改变对象的值:

Stringss=;

Systm.out.println(ss=+ss);

ss.rplac(1,0);

Systm.out.println(ss=+ss);

打印结果:

ss=

ss=

String对象真的不可变吗?

从上文可知String的成员变量是privatfinal的,也就是初始化之后不可改变。那么在这几个成员中,valu比较特殊,因为他是一个引用变量,而不是真正的对象。valu是final修饰的,也就是说final不能再指向其他数组对象,那么我能改变valu指向的数组吗?比如将数组中的某个位置上的字符变为下划线“_”。至少在我们自己写的普通代码中不能够做到,因为我们根本不能够访问到这个valu引用,更不能通过这个引用去修改数组。

那么用什么方式可以访问私有成员呢?没错,用反射,可以反射出String对象中的valu属性,进而改变通过获得的valu引用改变数组的结构。下面是实例代码:

  publicstaticvoidtstRflction()throwsExcption{

    //创建字符串HlloWorld,并赋给引用s

    Strings=HlloWorld;

    Systm.out.println(s=+s);  //HlloWorld

    //获取String类中的valu字段

    FildvaluFildOfString=String.class.gtDclardFild(valu);

    //改变valu属性的访问权限

    valuFildOfString.stAccssibl(tru);

    //获取s对象上的valu属性的值

    char[]valu=(char[])valuFildOfString.gt(s);

    //改变valu所引用的数组中的第5个字符

    valu[5]=_;

    Systm.out.println(s=+s);//Hllo_World

  }

打印结果为:

s=HlloWorld

s=Hllo_World

在这个过程中,s始终引用的同一个String对象,但是再反射前后,这个String对象发生了变化,也就是说,通过反射是可以修改所谓的“不可变”对象的。但是一般我们不这么做。这个反射的实例还可以说明一个问题:如果一个对象,他组合的其他对象的状态是可以改变的,那么这个对象很可能不是不可变对象。例如一个Car对象,它组合了一个Whl对象,虽然这个Whl对象声明成了privatfinal的,但是这个Whl对象内部的状态可以改变,那么就不能很好的保证Car对象不可变。









































北京治疗白癜风疾病哪家好
全国白癜风十佳医院



转载请注明:http://www.jiaju1314.com/lsqy/871.html