创建字符串
创建字符串一共有三种方式:
方式1
String str1= "abc"; System.out.println(str1); //输出结果 abc
方式2
String str2= new String("abc"); System.out.println(str2); //输出结果 abc
方式3
char[] value={'a','b','c'}; String str=new String(value); System.out.println(str); //输出结果 abc
三种方式的内存图
方式1 方式2
在这里我们首先介绍一下字符串常量池的概念:
String类的设计使用了共享设计模式
在JVM底层实际上会自动维护一个对象池,这个对象池称为字符串常量池
对于方式1中的赋值形式来说,因为是直接赋值,所以赋值的内容将自动保存到字符串常量池当中,此时abc存入了字符串常量池,并将abc得地址0x999赋给了str1这个引用
对于方式2的赋值形式来说,如果字符串常量池当中有abc,就直接将abc的地址0x999赋给value数组,然后再将String类在堆上实例化的对象的地址0x888赋给str2引用,如果字符串常量池当中没有abc,那么就把新开辟的字符串对象abc存入常量池当中以供下次使用,然后把存进去的abc的地址赋给value数组,然后再将String类在堆上实例化的对象的地址0x888赋给str2引用,至于这里为什么出现了value数组,需要仔细剖析:
首先我们进行了String这个类的对象的实例化操作,所以一定会在堆上开辟内存.
此时再来看String类的有参构造函数中参数为字符串的情况
可以看到我们将original的值赋给了value,再来看String类中value这个成员变量到底是什么把?
可以看到是私有的且被final所修饰的char类型的value数组
注意:被final所修饰的成员变量此时在String类中并没有进行初始化,所以需要在构造方法中进行初始化,所以这也是为什么this.value=original.val出现的原因.因为value这个数组此时并没有直接初始化,所以通过构造方法进行 传参从而对value这个数组进行初始化.
方式3
方式3的赋值方式的内存图如下:
我们来分析下为什么是这样画的:
首先value是一个局部变量,所以先在栈上开辟内存,同时在堆上开辟内存存储’a’,‘b’,‘c’,‘d’,'e’这五个元素,接下来再继续在栈上开辟内存,存储str3这个局部变量.同时在栈上开辟内存存储String这个类的实例化对象.
接下来我们来看String这个类的有参构造函数中参数为数组时的源码的情况:
我们会发现其使用了copyof方法拷贝了一个新的数组,而新拷贝的数组其实就是我们的value数组的复制品.相当于是将拷贝后的数组赋值给了String类中所定义的value数组.
所以就如图中所画的一样,此时新拷贝的数组的地址为0x888,将这个地址赋值给String类中的成员变量value数组,然后再将String类在堆上的实例化对象的地址0x999赋值给我们的str3这个引用.
总结
这三种创建字符串常量的方式,底层其实都与源码中被private和final所修饰的char类型的数组有关
理解池的概念
“池” 是编程中的一种常见的, 重要的提升效率的方式, 我们会在未来的学习中遇到各种 “内存池”, “线程池”, “数据库连接池” …
然而池这样的概念不是计算机独有, 也是来自于生活中. 举个栗子:
现实生活中有一种女神, 称为 “绿茶”, 在和高富帅谈着对象的同时, 还可能和别的屌丝搞暧昧. 这时候这个屌丝被称为 “备胎”. 那么为啥要有备胎? 因为一旦和高富帅分手了, 就可以立刻找备胎接盘, 这样 效率比较高.
如果这个女神, 同时在和很多个屌丝搞暧昧, 那么这些备胎就称为 备胎池.
回忆引用
我们曾经在讲数组的时候就提到了引用的概念.
引用类似于 C 语言中的指针, 只是在栈上开辟了一小块内存空间保存一个地址. 但是引用和指针又不太相同, 指针能进行各种数字运算(指针+1)之类的, 但是引用不能, 这是一种 “没那么灵活” 的指针.
另外, 也可以把引用想象成一个标签, “贴” 到一个对象上. 一个对象可以贴一个标签, 也可以贴多个. 如果一个对象上面一个标签都没有, 那么这个对象就会被 JVM 当做垃圾对象回收掉.
Java 中数组, String, 以及自定义的类都是引用类型.
由于 String 是引用类型, 因此对于以下代码
String str1 = "Hello"; String str2 = str1;
内存图如下所示:
此时两个引用指向了同一个对象
那么有同学可能会说, 是不是修改 str1 , str2 也会随之变化呢?下面来看一段代码:
String str1 = "Hello"; String str2 = str1; str1 = "hello"; System.out.println(str2); System.out.println(str1);
事实上,这样的代码并不算 “修改” 字符串, 而是让 str1 这个引用指向了一个新的 String 对象.
内存图如下所示:
因为字符串是一种不可变对象(接下来会细讲),它的内容不可变,Sting类的内部实现也是基于char[]来实现的,因为String类源码中定义char[]类型时是如下格式:
也就是说char[]类型的数组被final所修饰时,其地址是不能被修改的,假如我们此时修改了Hello字符串的首字母H改为小写h,相当于产生了一个新的字符串常量hello,那么在常量池上就相当于产生了一个新的对象,就要分配新的地址,就不可能在原来Hello的地址上将其改为hello了,所以就如上图所示了
那么要想在原地址改为hello,需要用到反射,在下面字符串不可变那一章节我们会介绍反射这个概念。
字符串判断相等
判断字符串引用是否相等
如果现在有两个int型变量,判断其相等可以使用 == 完成。
int x = 10 ; int y = 10 ; System.out.println(x == y); // 执行结果 true
如果说现在在String类对象上使用 == ,就是判断引用是否相等,来看下面几段代码,并判断字符串的引用是否相等
代码1
String str1 = "Hello"; String str2 = "Hello"; System.out.println(str1 == str2); // 执行结果 true
代码1内存布局:
我们发现, str1 和 str2 是指向同一个对象的. 此时如 “Hello” 这样的字符串常量是在 字符串常量池 中.
如 “Hello” 这样的字符串字面值常量, 也是需要一定的内存空间来存储的. 这样的常量具有一个特点, 就是不需要修改(常量嘛). 所以如果代码中有多个地方引用都需要使用 “Hello” 的话, 就直接引用到常量池的这个位置就行了, 而没必要把 “Hello” 在内存中存储两次.也就是说我们每次在创建字符串的时候便会看常量池当中到底有没有当前所需要创建的字符串,如果有就不用在常量池中再创建一次了。
代码2
String str = "abc"; String str2 = new String("abc"); System.out.println(str1 == str2); //输出结果 false
代码2 内存图如下
我们会发现此时最终代码结果为false.
原因如图所示:str1与str2引用所指向的对象均不相同,所以其存储的地址也是不相同的,那么最终的比较一定为false.
那么在这个地方假如我们想让str1与str2这两个引用所存储的地址相同的话,此时便引入了一特殊的概念:即手工入池:利用intern()方法
首先来看代码:
String str1 = "hello" ; String str2 = new String("hello") ; System.out.println(str1 == str2); // 执行结果 False --------------------------------------------------------------------- String str1 = "hello" ; String str2 = new String("hello").intern() ; System.out.println(str1 == str2); // 执行结果 true
来看这份代码的内存图:
我们可以看到此时str1与str2引用的地址相同,这是为什么呢?
答:这便是intern()方法的功劳,intern被称为手工入池处理,对于
String str2 = new String(“hello”).intern() ; 这段代码来说,是检查此时字符串常量池当中是否有hello这个字符串常量,如果有,便将常量池中的引用返回给当前的引用,对于上述代码来说,此时常量池中是有hello这个字符串常量的,那么就将其在常量池当中的地址赋给引用str2,则str1与str2此时拥有相同的地址了,最终str1==str2结果为true.
代码3
1.public static void main(String[] args) { 2. String str1 = "hello"; 3. String str2 = "hel" + "lo";//字符串常量在编译时就已经完成了字符串的拼接,所以此处等价于 String str2= "hello"; 4. String str3 = new String("hel") + "lo"; 5. String str4 = new String("hel") + new String("lo"); 6. 7. //true 8. System.out.println(str1 == str2); 9. //false 10. System.out.println(str3 == str1); 11. //false 12. System.out.println(str1 == str4); 13. //false 14. System.out.println(str3 == str4); 15.}
首先来看str1与str2,str3的比较:
此时我们str2中我们会发现是两个字符串常量在相加,常量相加有一个特点就是其在编译时期就已经确定了,所以此时如果两个字符串常量相加的话就等价于拼接后的字符串,那么str1==str2最后的结果便为true
现在来看str3,此时是一个new String对象加一个字符串常量,在内存中可以看到此时String对象中是是hel字符串,在常量池中并没有,则放入常量池中,然后字符串常量lo也没有,也放进去,则此时堆上的对象指向常量池当中的hel
我们的代码为两者的拼接,那么就会在堆上开辟一个新的内存去存储两者拼接后的新字符串hello,然后将这个新拼接的对象的地址赋给我们的str3引用,很显然这是跟str1完全不一样的地址,所以最终str1==str3的值为false
下面是str1与str4的比较:
同样我们可以看到是两个不同的地址,所以最终结果为false
代码4
1.public static void main(String[] args) { 2. String str1 = "hello"; 3. String str2 = "world"; 4. //st1是变量,变量在程序运行时才知道里面存储的内容 5. String str3 = str1 + "world"; 6. //两个字符串常量相加在编译时期就已经确定了,所以等价为helloworld 7. String str4 = "hello" + "world"; 8. String str5 = "helloworld"; 9. String str6=str1+str2; 10. //false 11. System.out.println(str3 == str5); 12. //true 13. System.out.println(str4 == str5); 14. //false 15. System.out.println(str5==str6); 16. }
此时我们可以看到str3中是一个变量和常量相加,在这里要注意,str1是变量,变量是只有在运行时才知道里面存储的是什么。而常量是编译时期就已经确定了,所以str3==str5为false。
Str4中是两个常量相加,而常量在编译的时候就已经确定了,所以str4等价于str5,则str4==str5为true
Str6同样为两个变量相加,变量是只有在运行时才知道里面存储的是什么,所以str5str6, str6str4都为false.
总结
String中使用==比较的时候比较的并不是其字符串内容是否相等,而比较的是两个引用类型地址是是否相同,也就是判断这两个引用是否指向了相同的对象,
判断字符串内容是否相等
如果要判断字符串的内容是否相等,此时就需要使用equals关键字
变量与变量进行比较
String str1 = new String("Hello"); String str2 = new String("Hello"); System.out.println(str1.equals(str2)); // System.out.println(str2.equals(str1)); // 或者这样写也行 // 执行结果 true
字符串常量与变量进行比较
现在需要比较 str 和 “Hello” 两个字符串是否相等, 我们该如何来写呢?
String str = new String("Hello"); // 方式一 System.out.println(str.equals("Hello")); // 方式二 System.out.println("Hello".equals(str));
在上面的代码中, 哪种方式更好呢?
我们更推荐使用 "方式二". 一旦 str 是 null, 方式一的代码会抛出空指针异常, 而方式二不会.例如:
String str = null; // 方式一 System.out.println(str.equals("Hello")); // 执行结果抛出 java.lang.NullPointerException 异常 // 方式二 System.out.println("Hello".equals(str)); // 执行结果 false