String的定义方法归纳起来总共为三种方式:
- 使用关键字new,如:String s1 = new String(“myString”);
- 直接定义,如:String s1 = “myString”;
- 串联生成,如:String s1 = “my” + “String”;这种方式比较复杂,这里就不赘述了,请参见java–String常量池问题的几个例子。
一、第一种方式通过关键字new定义过程:在程序编译期,编译程序先去字符串常量池检查,是否存在“myString”,如果不存在,则在常量池中开辟一个内存空间存放“myString”;如果存在的话,则不用重新开辟空间,保证常量池中只有一个“myString”常量,节省内存空间。然后在内存堆中开辟一块空间存放new出来的String实例,在栈中开辟一块空间,命名为“s1”,存放的值为堆中String实例的内存地址,这个过程就是将引用s1指向new出来的String实例。
直接使用" "双引号的创建机制
用" "双引号创建的方法是一种非常特别的创建方法;例如下面这段代码:
- String s1 = "first";
- Stirng s2 = "first";
- System.out.println(s1 == s2);
2、运行期:JVM仅仅会查找维护常量池,拿着"first"在String Pool中查找是否存在内容相同的字符串(用equals()方法确认),如果存在,返回String Pool中相应内存单元的引用,赋值给s1(s1即是String Pool中存放"first"内存单元的地址);如果不存在,则创建一个"first"放在String Pool中,返回引用,赋值给s1;s2同理;
这个过程实际是调用intern()方法实现的;
此过程中,JVM绝不会在堆区(heap)创建String对象;
所以,上述代码,s1与s2指向String Pool中同一块内存区域,是同一个对象,故返回true。
二、第二种方式直接定义过程:在程序编译期,编译程序先去字符串常量池检查,是否存在“myString”,如果不存在,则在常量池中开辟一个内存空间存放“myString”;如果存在的话,则不用重新开辟空间。然后在栈中开辟一块空间,命名为“s1”,存放的值为常量池中“myString”的内存地址。
用new string("string")的创建机制
相当于创建两次String对象,一次在String Pool中,一次在堆区(heap)中;
- String s1 = new String("first");
- String s2 = "first";
- System.out.println("s1 == s2");
故s1与s2肯定不是同一个对象,只是存储字符串值相同,故返回false。
总结:
如果String指向的是一个字符串常量,那么会先在字符串常量池(栈)中查找,如果有就直接指向它;没有则在字符串常量池中创建该常量,然后String指向该常量。
如果String使用关键字new初始化,则会在堆中开辟固定的空间存放该字符串的值,然后String指向该常量。
使用字符串常量拼接,由于表达式先计算右值,因此相当于将String指向一个新拼接好的字符串常量。同样会在字符串常量池中查找,如果有就直接指向它;没有则在字符串常量池中创建该常量,然后String指向该常量。但是如果拼接中存在使用new生成的字符串,则新的字符串就等价于使用new在堆中创建的一样。
修改String的值,只能改变String的指向,不会改变String指向的对象本身。如果非要改变指向的对象本身,可以使用反射。
注意:
- 对于先声明的字符串字面量常量,会放入常量池,但是若使用字面量的引用进行运算就不会把运算后的结果放入常量池中了。
三、intern()方法的用处
public class Test4{
public static void main(String[] args){
String str = "laji";
String str2 = new String("laji");
String str3 = null;
System.out.println(str==str2);// 运行后结果为false
str3 = str2.intern();
System.out.println(str==str3);// 运行后结果为true
}
}
显然,str3在初始化的时候是从字符串常量池中获取到的值
四、String中的toString()
从源代码中可以看出,String的toString()返回它本身(它本身就是一个字符串)。