博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java基础知识强化101:Java 中的 String对象真的不可变吗 ?
阅读量:5111 次
发布时间:2019-06-13

本文共 5755 字,大约阅读时间需要 19 分钟。

1. 什么是不可变对象?

      众所周知, 在Java中, String类是不可变的。那么到底什么是不可变的对象呢?

  可以这样认为:如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的。

  不能改变状态的意思是:不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变。

 

2. 区分对象和对象的引用

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

1 String s = "ABCabc";2 System.out.println("s = " + s);3 4 s = "123456";5 System.out.println("s = " + s);

打印结果为:

1 s = ABCabc2 s = 123456

     首先创建一个String对象s,然后让s的值为"ABCabc", 然后又让s的值为"123456"。 从打印结果可以看出,s的值确实改变了。

   那么怎么还说String对象是不可变的呢?

    其实这里存在一个误区: s只是一个String对象的引用,并不是对象本身。对象在内存中是一块内存区,成员变量越多,这块内存区占的空间越大。引用只是一个4字节的数据,里面存放了它所指向的对象的地址,通过这个地址可以访问对象。

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

 

                               

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

 

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

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

 

1 public final class String 2     implements java.io.Serializable, Comparable
, CharSequence 3 { 4 /** The value is used for character storage. */ 5 private final char value[]; 6 7 /** The offset is the first index of the storage that is used. */ 8 private final int offset; 9 10 /** The count is the number of characters in the String. */11 private final int count;12 13 /** Cache the hash code for the string */14 private int hash; // Default to 0

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

1 public final class String  2     implements java.io.Serializable, Comparable
, CharSequence { 3 /** The value is used for character storage. */ 4 private final char value[]; 5 6 /** Cache the hash code for the string */ 7 private int hash; // Default to 0

     由以上的代码可以看出, 在Java中String类其实就是对字符数组的封装。JDK6中, value是String封装的数组,offset是String在这个value数组中的起始位置,count是String所占的字符的个数。在JDK7中,只有一个value变量,也就是value中的所有字符都是属于String这个对象的。这个改变不影响本文的讨论。 除此之外还有一个hash成员变量,是该String对象的哈希值的缓存,这个成员变量也和本文的讨论无关。在Java中,数组也是对象

  所以value也只是一个引用,它指向一个真正的数组对象。其实执行了String s = "ABCabc";这句代码之后,真正的内存布局应该是这样的:

                            

 

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

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

1 String a = "ABCabc";  2 System.out.println("a = " + a);  3 a = a.replace('A', 'a');  4 System.out.println("a = " + a);

上面代码反编译结果如下:

1 String s = "ABCabc";2 System.out.println((new StringBuilder()).append("a= ").append(s).toString());3 s = s.replace('A', 'a');4 System.out.println((new StringBuilder()).append("a= ").append(s).toString());

打印结果为:

a = ABCabca = aBCabc

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

1  /** 2      * Copies this string replacing occurrences of the specified character with 3      * another character. 4      * 5      * @param oldChar 6      *            the character to replace. 7      * @param newChar 8      *            the replacement character. 9      * @return a new string with occurrences of oldChar replaced by newChar.10      */11     public String replace(char oldChar, char newChar) {12         char[] buffer = value;13         int _offset = offset;14         int _count = count;15 16         int idx = _offset;17         int last = _offset + _count;18         boolean copied = false;19         while (idx < last) {20             if (buffer[idx] == oldChar) {21                 if (!copied) {22                     char[] newBuffer = new char[_count];23                     System.arraycopy(buffer, _offset, newBuffer, 0, _count);24                     buffer = newBuffer;25                     idx -= _offset;26                     last -= _offset;27                     copied = true;28                 }29                 buffer[idx] = newChar;30             }31             idx++;32         }33 34         return copied ? new String(0, count, buffer) : this;35     }

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

1 String ss = "123456";2 System.out.println("ss = " + ss);3 ss.replace('1', '0');4 System.out.println("ss = " + ss);

打印结果:

1 ss = 1234562 ss = 123456

 

4. String对象真的不可变吗 ?

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

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

1 public static void testReflection() throws Exception { 2     //创建字符串"Hello World", 并赋给引用s 3     String s = "Hello World";  4     System.out.println("s = " + s);    //Hello World 5     //获取String类中的value字段 6     Field valueFieldOfString =  7             String.class.getDeclaredField("value"); 8  9     //改变value属性的访问权限10     valueFieldOfString.setAccessible(true);11     //获取s对象上的value属性的值12     char[] value = (char[]) valueFieldOfString.get(s);13     //改变value所引用的数组中的第5个字符14     value[5] = '_';15     System.out.println("s = " + s);  //Hello_World16 }

打印结果如下:

1 s = Hello World2 s = Hello_World

      在这个过程中,s始终引用的同一个String对象,但是再反射前后,这个String对象发生了变化, 也就是说,通过反射是可以修改所谓的"不可变"对象的。但是一般我们不这么做。

  这个反射的实例还可以说明一个问题:如果一个对象,他组合的其他对象的状态是可以改变的,那么这个对象很可能不是不可变对象。例如一个Car对象,它组合了一个Wheel对象,虽然这个Wheel对象声明成了private final 的,但是这个Wheel对象内部的状态可以改变, 那么就不能很好的保证Car对象不可变。

转载于:https://www.cnblogs.com/hebao0514/p/5024135.html

你可能感兴趣的文章
032. asp.netWeb用户控件之一初识用户控件并为其自定义属性
查看>>
Ubuntu下安装MySQL及简单操作
查看>>
前端监控
查看>>
clipboard.js使用方法
查看>>
移动开发平台-应用之星app制作教程
查看>>
leetcode 459. 重复的子字符串(Repeated Substring Pattern)
查看>>
伪类与超链接
查看>>
centos 7 redis-4.0.11 主从
查看>>
博弈论 从懵逼到入门 详解
查看>>
永远的动漫,梦想在,就有远方
查看>>
springboot No Identifier specified for entity的解决办法
查看>>
慵懒中长大的人,只会挨生活留下的耳光
查看>>
"远程桌面连接--“发生身份验证错误。要求的函数不受支持
查看>>
【BZOJ1565】 植物大战僵尸
查看>>
VALSE2019总结(4)-主题报告
查看>>
浅谈 unix, linux, ios, android 区别和联系
查看>>
51nod 1428 活动安排问题 (贪心+优先队列)
查看>>
中国烧鹅系列:利用烧鹅自动执行SD卡上的自定义程序(含视频)
查看>>
Solaris11修改主机名
查看>>
latex for wordpress(一)
查看>>