谈起String很多人脑海中浮现出的第一印象就是:String对象是不可变的,String的底层是通过字符数组来实现的,一经初始化就不能被改变,Sting之间的比较需要通过equals()方法来实现。String类提供了很多方法来操作比如subString()取子串,charAt(int index)取指定位置的字符。但是如果对其并没有深入的认识,不仅是使用上还是面试上往往容易出错。
话不多说,先来点代码吊吊胃口:
public class StringDemo02 {
public static void main(String [] args){
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s12 = s1+"b";
String s4 = "a"+"b";
String s5 = new String("ab");
System.out.println(s3==s4);
System.out.println(s3.equals(s5));
System.out.println(s3==s5);
System.out.println(s3.equals(s4));
System.out.println(s12==s3);
System.out.println(s12.equals(s3));
System.out.println(s12==s5);
System.out.println(s12.equals(s5));
System.out.println(s3==s5.intern());
System.out.println(s12==s5.intern());
System.out.println(s3==s12.intern());
System.out.println(s3.hashCode()==s12.hashCode());
System.out.println(s3.hashCode()==s5.hashCode());
}
如果上面所有的输出结果都知道,那么你对字符串的了解也应该差不多了,下面的分析也就没有看的必要了。
在说String之前先来看看Java的内存结构:
一:JAVA内存了解
JAVA主要有2个内存区域即堆和非堆,堆区按照JAVA官方的说法是运行时动态分配的区域,即所有动态分配的内存(类实列及数组)都是在这里分配出来的,但是堆区的存取速度比较慢因为他的内存管理是由GC来控制的。非堆区存放是基本类型已经对象的引用和一些JVM内部优化需要的代码,非堆中栈的存取速度很快,仅次于寄存器。比较多的说法是在堆中还有一个方法区,也有一个常量池,所有类型的常量都有一个常量池中的常量,String的常量也不例外。在常量池中保存在很多String常量对象(通常字符串时以表的形式保存,所有的常量字符保存在CONSTANT_String_info表中),因此提高了字符串的复用率,这也是JAVA这么设计的出发点。
二:常量的分类
1、静态常量池:指.class文件中的常量池,包括字符串,字面量及类的相关信息。静态常量池占用多半常量池内存。
2、动态常量池:也叫运行时常量池。及在编译的时候是不能被确定,只有等到运行时才能最终确定的。
三:实践分析
首先明确一点“==”是比较内存地址,equals比较字符串的值
/**
情景一
*/
String s1 = "abc";//在常量池中创建一变了并将地址赋予s1
String s2 = "abc";//检索到已经有“abc”直接将地址赋予s2
System.out.println("s1==s2:"+(s1==s2));//true
System.out.println("s1.equals(s2):"+(s1.equals(s2)));//true
第一句话在编译期间在常量池中创建一变量并将地址给s1,第二句话在常量池中检索到已经存在“abc”,所以直接将abc地址赋予s2。因此s1==s2是true;当然2个变量的内容一样的。所以s1.equals(s2)是true。整个过程只创建了一个变量。
/**
* 情景二
* 在编译时会进行优化,及ab+c操作在编译时及完成
*/
String s1 = "ab"+"c";
String s2 = "abc";
System.out.println("s1==s2:"+(s1==s2));//true
System.out.println("s1.equals(s2):"+(s1.equals(s2)));//true
因为ab+c操作在编译阶段已经完成,所以在运行的时候s1是指向常量池中的abc常量的。
/**
* 情景三
* 当遇到非字面量字符串拼接时
*/
String s1 = "ab";
String s2 = "c";
String s12 = s1+s2;//在编译时直接转为StringBuilder进行处理
String s3 = "abc";
System.out.println("s12==s3:"+(s12==s3));//false
System.out.println("s12.equals(s3):"+(s12.equals(s3)));//true
但JVM进行编译的时候遇到非字面时,并不能像人工一样只能计算出相应的值。所以上面的代码片段执行是先在常量池中建立以常量”ab”并赋值给s1,创建“c”常量并将地址赋给s2。JVM遇到非字面量的+处理时,自动转换为StringBulider处理,及先new一个StringBuilder变量然后append一个c形成新的字符串变量并赋值给s12。这是的s12其实是在堆区。所以s12与s3的地址肯定是不一样的。但是二者值是相同的。
/**
* 情景四
* 通过new产生新的对象
*/
String s1 = new String("abc");
String s2 = new String ("abc");
String s3 = "abc";
Stirng s4 = new String(s3);
System.out.println("s1==s3:"+(s1==s3));//false
System.out.println("s1.equals(s3):"+(s1.equals(s3)));//true
System.out.println("s1==s2:"+(s1==s2));//false
System.out.println("s1.equals(s2):"+(s1.equals(s2)));//true
通过new关键字都会在堆区产生新的对象,所以s1,s2,s3所表示得地址肯定是不一样的。s1,s2虽然都在堆中,但是表示不同的对象。而s3实在常量池中。但是s1,s2,s3所表示得内容都是一样的。s4虽然是用s3的内容所创建的但是也是通过new了一个新对象,所以表示地址不一样内容一样的。