这两天在看 Java 面试相关的一些问题,很偶然也很幸运的看到了下面这篇文章。
这篇文章的作者有一系列关于 Java 深入学习的文章,很值得一看,个人觉得非常好,很有收获。
看完那篇文章有一段评论让我很有感触。忍不住写了点代码做了下测试,加以验证。
正如我们所理解的,通过
- String hello = "Hello World!";
和
- String xx = new String("Hello World!");
得到的字符串对象是不一样的,new 方式是在堆空间中创建的,而直接的字符串则是先被放到常量池中。如果有新的与之一样的对象被创建,则直接让这个新对象引用常量池中的这个地址即可。
这样的好处就是可以最大限度的节省内存空间。
而使用 new 方式创建的则就不一样了,只要是用了 new 创建字符串,就会在堆空间中开辟出一块内存,然后返回这个内存地址的引用。所以这样创建的对象,即使内容一致,也不会是指向同一个内存地址。
下面用几个简单的代码做下测试。
- /**
- *字符串中对于内容和地址的判定可以用下面两种方式,但侧重点不一样。
- */
- equals // 判断 两个字符串的内容是否一致
- == // 判断两个字符串的内存地址是否一致
且看下面的代码:
- public static void simple() {
- String s1 = "Hello World!";
- String s2 = "Hello World!";
- String s3 = new String("Hello World!");
- String s4 = new String("Hello World!");
- // 下面开始比较引用和内容的比较
- System.out.println("字符串赋值方式:");
- System.out.println(s1==s2);
- System.out.println(s1.equals(s2));
- System.out.println("\n字符串赋值方式和new方式:");
- System.out.println(s1==s3);
- System.out.println(s1.equals(s3));
- System.out.println("\nnew 方式:");
- System.out.println(s3==s4);
- System.out.println(s3.equals(s4));
- }
得到的结果如下:
- 字符串赋值方式:
- true
- true
- 字符串赋值方式和new方式:
- false
- true
- new 方式:
- false
- true
结果却是和我们所说的那样。
不出所料,String 确实是 "不可变的",每次改变底层其实都是创建了一个心的字符串对象,然后赋予了新值。
为什么会这样呢?我们也许可以在源码中找到真相。
哦,原来 Java 对于 String 类只是维护了一个 final 类型的字符数组啊。怪不得赋值之后就不能改变了呢。
但是也许你会有疑问,咦,不对啊,"我经常使用 String 的什么 replace 方法改变字符串的内容啊。你这则么解释呢?"
其实答案还是那样,它真的没变,我们并没有看到事情的真相,相信看完下面的源码,你就明白了。
- /**
- * Returns a string resulting from replacing all occurrences of
- * {@code oldChar} in this string with {@code newChar}.
- * * If the character {@code oldChar} does not occur in the
- * character sequence represented by this {@code String} object,
- * then a reference to this {@code String} object is returned.
- * Otherwise, a {@code String} object is returned that
- * represents a character sequence identical to the character sequence
- * represented by this {@code String} object, except that every
- * occurrence of {@code oldChar} is replaced by an occurrence
- * of {@code newChar}.
- * *Examples:
- * "mesquite in your cellar".replace('e', 'o')
- * returns "mosquito in your collar"
- * "the war of baronets".replace('r', 'y')
- * returns "the way of bayonets"
- * "sparring with a purple porpoise".replace('p', 't')
- * returns "starring with a turtle tortoise"
- * "JonL".replace('q', 'x') returns "JonL" (no change)
- *
- *
- * @param oldChar the old character.
- * @param newChar the new character.
- * @return a string derived from this string by replacing every
- * occurrence of {@code oldChar} with {@code newChar}.
- */
- 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;
- }
源码中很明确的使用了
- new String(buf, true);
的方式返回给调用者新对象了。
读到上面的内容,其实基本上已经够了。但是了解一下更深层次的内容,相信对我们以后编程来说会更好。
源码中清楚的使用 char[] value 来盛装外界的字符串数据。也就是说字符串对象的不可变的特性,其实是源自 value 数组的 final 特性。
那么我们可以这么想,我们不改变 String 的内容,而是转过头来改变 value 数组的内容(可以通过反射的方式来修改 String 对象中的 private 属性的 value),结果会怎样呢?
答案是真的会变哦。
可以先看下下面的代码
- private static void deep() throws NoSuchFieldException,
- IllegalAccessException {
- String hello = "Hello World!";
- String xx = new String("Hello World!");
- String yy = "Hello World!";
- /**
- * 判断字符串是否相等,默认以内存引用为标准
- */
- System.out.println(hello == xx);
- System.out.println(hello == yy);
- System.out.println(xx == yy);
- // 查看hello, xx, yy 三者所指向的value数组的真实位置
- Field hello_field = hello.getClass().getDeclaredField("value");
- hello_field.setAccessible(true);
- char[] hello_value = (char[]) hello_field.get(hello);
- System.out.println(hello_field.get(hello));
- Field xx_field = xx.getClass().getDeclaredField("value");
- xx_field.setAccessible(true);
- char[] xx_value = (char[]) xx_field.get(xx);
- System.out.println(xx_field.get(xx));
- Field yy_field = yy.getClass().getDeclaredField("value");
- yy_field.setAccessible(true);
- char[] yy_value = (char[]) yy_field.get(yy);
- System.out.println(yy_field.get(yy));
- /**
- * 经过反射获取到这三个字符串对象的最底层的引用数组value,发现如果一开始内容一致的话,java底层会将创建的字符串对象指向同一个字符数组
- *
- */
- // 通过反射修改字符串引用的value数组
- Field field = hello.getClass().getDeclaredField("value");
- field.setAccessible(true);
- char[] value = (char[]) field.get(hello);
- System.out.println(value);
- value[5] = ' ^ ';
- System.out.println(value);
- // 验证xx是否被改变
- System.out.println(xx);
- }
结果呢?
- false
- true
- false
- [C@6d06d69c
- [C@6d06d69c
- [C@6d06d69c
- Hello World!
- Hello^World!
- Hello^World!
真的改变了。
而我们也可以发现,hello,xx, yy 最终都指向了内存中的同一个 value 字符数组。这也说明了 Java 在底层做了足够强的优化处理。
当创建了一个字符串对象时,底层会对应一个盛装了相应内容的字符数组;此时如果又来了一个同样的字符串,对于 value 数组直接获取刚才的那个引用即可。(相信我们都知道,在 Java 中数组其实也是一个对象类型的数据,这样既不难理解了)。
不管是字符串直接引用方式,还是 new 一个新的字符串的方式,结果都是一样的。它们内部的字符数组都会指向内存中同一个 "对象"(value 字符数组)。
稍微有点乱,但是从这点我们也可以看出 String 的不可变性其实仍旧是对外界而言的。在最底层,Java 把这一切都给透明化了。我们只需要知道 String 对象有这点特性,就够了。
其他的,日常应用来说,还是按照 String 对象不可变来使用即可。
来源: http://www.bubuko.com/infodetail-1974347.html