做一个积极的人
编码、改bug、提升自己
我有一个乐园,面向编程,春暖花开!
一、初识String类
首先JDK API的介绍:
public final class String extends Object implements Serializable, Comparable, CharSequence
String类代表字符串。Java 程序中的所有字符串字面值(如 "abc" )都作为此类的实例实现。
字符串是常量;它们的值在创建之后不能更改。字符串缓冲区支持可变的字符串。因为 String 对象是不可变的,所以可以共享。例如:
String str = "abc";
等效于:
char data[] = {'a', 'b', 'c'}; String str = new String(data);
从JDK API中可以看出:
- String类是final类,那么String类是不能被继承的。
- 实现了Cloneable接口,即覆盖了函数clone(),能被克隆。
- 实现了Serializable接口,支持序列化,也就意味了String能够通过序列化传输。
二、字符串的不可变性
从上面的介绍中发现:字符串是常量,它们的值在创建之后不能更改。为什么会这样呢?要了解其原因,简单看一下String类的源码实现。
public final class String implements java.io.Serializable, Comparable, CharSequence { /** The value is used for character storage. */ private final char value[]; public String concat(String str) { int otherLen = str.length(); if (otherLen == 0) { return this; } int len = value.length; char buf[] = Arrays.copyOf(value, len + otherLen); str.getChars(buf, len); // 重新创建一个新的字符串 return new String(buf, true); } 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类其实是通过char数组来保存字符串的,注意修饰这个char前面的关键字 final。final修饰的字段创建以后就不可改变。
注意:private final char value[]; 这里虽然value是不可变,也就是说value这个引用地址不可变。但是因为其是数组类型,根据之前学过的内容,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};value[2]=100; //这时候数组里已经是{1,2,100}
所以String是不可变的关键都在底层的实现,而不是一个final。
也可以通过上面的concat(String str) 和replace(char oldChar, char newChar)方法简单进行了解,所有的操作都不是在原有的value[]数组中进行操作的,而是重新生成了一个新数组buf[]。也就是说进行这些操作后,最原始的字符串并没有被改变。
如果面试有问到的话要修改String中value[] 数组的内容,要怎么做,那么可以通过反射进行修改!实际使用中没有人会去这么做。
三、字符串常量池和 intern 方法
Java中有字符串常量池,用来存储字符串字面量! 由于JDK版本的不同,常量池的位置也不同,根据网上的一些资料:
jdk1.6及以下版本字符串常量池是在永久区中。
jdk1.7、1.8下字符串常量池已经转移到堆中了。(JDK1.8已经没有去掉永久区)
因为字符串常量池发生了变化,在String内对intern()进行了一些修改:
jDK1.6版本中执行intern()方法,首先判断字符串常量池中是否存在该字面量,如果不存在则拷贝一份字面量放入常量池,最后返回字面量的唯一引用。如果发现字符串常量池中已经存在,则直接返回字面量的唯一引用。
jdk1.7以后执行intern()方法,如果字符串常量池中不存在该字面量,则不会再拷贝一份字面量,而是拷贝字面量对应堆中一个引用,然后返回这个引用。
String 类型的常量池比较特殊。它的主要使用方法有两种:
- 直接使用双引号声明出来的 String 对象会直接存储在常量池中。
- 如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法。不同版本的intern 表现看上面介绍。
说明:直接使用new String() 创建出的String对象会直接存储在堆上
通过一个栗子,看一下上面说的内容:
String str1 = "aflyun";String str2 = new String("aflyun");System.out.println(str1 == str2);String str3 = str2.intern();System.out.println(str1 ==str3);
使用JDK1.8版本运行输出的结果: false 和 true 。
先上面示例的示意图:
str1直接创建在字符串常量池中,str2使用new关键字,对象创建在堆上。所以str1 == str2 为false。
str3是str2.intern(),根据上面的介绍,在jdk1.8首先在常量池中判断字符串aflyun是否存在,如果存在的话,直接返回常量池中字符串的引用,也就是str1的引用。所以str1 ==str3为true。
如果你理解了上面的内容,可以在看一下下面的栗子,运行结果是在JDK1.8环境:
栗子1:
String str1 = "hello";String str2 = "world";//常量池中的对象String str3 = "hello" + "world";//在堆上创建的新的对象String str4 = str1 + str2; //常量池中的对象String str5 = "helloworld";System.out.println(str3 == str4);//falseSystem.out.println(str3 == str5);//trueSystem.out.println(str4 == str5);//false
栗子2:
//同时会生成堆中的对象以及常量池中hello的对象,此时str1是指向堆中的对象的String str1 = new String("hello");// 常量池中的已经存在hellostr1.intern();//常量池中的对象,此时str2是指向常量池中的对象的String str2 = "hello";System.out.println(str1 == str2); // false// 此时生成了四个对象 常量池中的"world" + 2个堆中的"world" +s3指向的堆中的对象(注此时常量池不会生成"worldworld")String str3 = new String("world") + new String("world");//常量池没有“worldworld”,会直接将str3的地址存储在常量池内str3.intern(); // 创建str4的时候,发现字符串常量池已经存在一个指向堆中该字面量的引用,则返回这个引用,而这个引用就是str3String str4 = "worldworld"; System.out.println(str3 == str4); //true
栗子3:涉及到final关键字,可以试着理解一下
// str1指的是字符串常量池中的 java6String str1 = "java6";// str2是 final 修饰的,编译时候就已经确定了它的确定值,编译期常量final String str2 = "java";// str3是指向常量池中 javaString str3 = "java";//str2编译的时候已经知道是常量,"6"也是常量,所以计算str4的时候,直接相当于使用 str2 的原始值(java)来进行计算.// 则str4 生成的也是一个常量,。str1和str4都对应 常量池中只生成唯一的一个 java6 字符串。String str4 = str2 + "6";// 计算 str5 的时候,str3不是final修饰,不会提前知道 str3的值是什么,只有在运行通过链接来访问,这种计算会在堆上生成 java6String str5 = str3 + "6";System.out.println((str1 == str4));//trueSystem.out.println((str1 == str5));//false
总结:
- 直接定义字符串变量的时候赋值,如果表达式右边只有字符串常量,那么就是把变量存放在常量池里。
- new出来的字符串是存放在堆里面。
- 对字符串进行拼接操作,也就是做"+"运算的时候,分2中情况:
- 表达式右边是纯字符串常量,那么存放在字符串常量池里面。
- 表达式右边如果存在字符串引用,也就是字符串对象的句柄,那么就存放在堆里面。:
四、面试题
1、 String s1 = new String("hello");这句话创建了几个字符串对象?
情况1:
String s1 = new String("hello");// 堆内存的地址值String s2 = "hello";System.out.println(s1 == s2);// 输出false,因为一个是堆内存,一个是常量池的内存,故两者是不同的。System.out.println(s1.equals(s2));// 输出true
如果上面代码的话,这种情况总共创建2个字符串对象。常量池中没有字符串"hello" 的话,一个是new String 创建的一个新的对象,一个是常量“hello”对象的内容创建出的一个新的String对象。
情况2:
```java String s2 = "hello"; String s1 = new String("hello");
String s1 = new String("hello"); 此时就创建一个对象,而常量“hello”则是从字符串常量池中取出来的。### 2、有时候在面试的时候会遇到这样的问题:**都说String是不可变的,为什么我可以这样做呢,String a = "1";a = "2";**
java public class StringTest {
public static void main(String[] args) { String s = "aflyun"; System.out.println("s1.hashCode() = " + s.hashCode() + "--" + s); s = "hello aflyun"; System.out.println("s2.hashCode() = " + s.hashCode() + "--" + s); //运行后输出的结果不同,两个值的hascode也不一致, //说明设置的值在内存中存储在不同的位置,也就是创建了新的对象}
}
s1.hashCode() = -1420403061--aflyun s2.hashCode() = -855605863--hello aflyun ```
【首先创建一个String对象s,然后让s的值为“aflyun”, 然后又让s的值为“hello aflyun”。 从打印结果可以看出,s的值确实改变了。那么怎么还说String对象是不可变的呢?】
其实这里存在一个误区: s只是一个String对象的引用,并不是对象本身。对象在内存中是一块内存区,成员变量越多,这块内存区占的空间越大。引用只是一个4字节的数据,里面存放了它所指向的对象的地址,通过这个地址可以访问对象。
也就是说,s只是一个引用,它指向了一个具体的对象,当s=“hello aflyun”; 这句代码执行过之后,又创建了一个新的对象“123456”, 而引用s重新指向了这个新的对象,原来的对象“aflyun”还在内存中存在,并没有改变。内存结构如下图所示:
类似的一张图:
总结一下:“String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何改变操作都会生成新的对象”。
参考资料
java的线程安全、单例模式、JVM内存结构等知识学习和整理
Java-String.intern的深入研究
深入理解Java中的String
备注: 由于本人能力有限,文中若有错误之处,欢迎指正。
谢谢你的阅读,如果您觉得这篇博文对你有帮助,请点赞或者喜欢,让更多的人看到!祝你每天开心愉快!
不管做什么,只要坚持下去就会看到不一样!在路上,不卑不亢!
愿你我在人生的路上能都变成最好的自己,能够成为一个独挡一面的人