String类型是继 byte char short int long float double 及boolean八大类型之外的又一重要类型,任何语言中都离不开它,String又称为字符串或者字符串常量。在任何语言的底层都不会提供直接的字符串类型,现在所谓的字符串类型只是高级语言为了给用户方便开发而已。在Java中字符串也是用字符数组来存放的。
源码:
private final char value[]; //就是该数组,被final修饰,也是字符串不可变的原因
String str = “hello java”;
String str = new String(“hello Java”);
那么这两种方式到底有何区别呢?在分析其区别之前先看这样一段代码:
public class Test {
public static void main(String[] args) {
int x= 20;
int y = 20;
System.out.println(x == y); //true
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1 == str2); //false
}
}
输出结果:
为啥会是这样的结果呢?先来看一下它在内存中的存放。
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1.equals(str2));//true
面试题:请叙述String类 == 与 equals 的区别
== 比较的是两个变量栈空间的数值。这里的变量分为基本数据类型和引用数据类型,对于基本数据类型,栈空间存的就是它们自身的数值,因此对于基本数据类型 == 比较的是它们自身的数值是否相等,而对于引用数据类型,栈空间存的是引用数据类型的地址,因此对于引用数据类型来说 == 比较的是两个引用数据类型的地址值是否相等。
equals方法:在String类中,覆写了Object类的equals()方法,比较的是两个字符串内容是否相同。而对于别的没有覆写equals()的类,仍然比较的是该类对象的引用(地址)是否相同,基本类型不能调用该方法。
Object类的equals方法源码:
public boolean equals(Object obj) {
return (this == obj);
}
public class Test {
public static void main(String[] args) {
String str1 = new String("hello");
System.out.println("hello".equals(str1));
System.out.println(str1.equals("hello"));
}
}
代码中直接写的“hello”就是String类的匿名对象,那该如何证明它是一个对象呢?其实不难证明,在代码中我们可以看到该匿名 字符串调用了equals方法,因此它是一个String类的对象。而在之前的第一种定义中String str = "hell";
本质上就是为匿名字符串设置名字。
这是为何呢?不妨看这样一段代码: 空指针异常的问题
public class Test {
public static void main(String[] args) {
String str1 = null;// 假设str的值根据用户输入而定
System.out.println(str1.equals("hello"));
}
}
public class Test {
public static void main(String[] args) {
String str1 = null;// 假设str的值根据用户输入而定
System.out.println("hello".equals(str1));
}
}
public class Test {
public static void main(String[] args) {
String str1 = "javaSE";
String str2 = "javaSE";
String str3 = new String ("javaSE");
String str4 = new String ("javaSE");
String str5 = str3.intern();//字符串入池函数
System.out.println(str1 == str2); //true
System.out.println(str3 == str4); //false
System.out.println(str1 == str5); //true
}
}
输出结果:
在分析代码之前,必须先了解一个非常重要的东西:共享设计模式
在JVM底层,有一个叫做对象池(字符常量池)的东西,对于直接赋值产生的String类对象如String str = “hello”,它先会在常量池中找有没有该字符串即“hello”,如果没有则在常量池中创建一个,然后把该字符串在常量池中的地址保存在栈空间中,如果有“hello”则直接引用(保存地址),不再创建。
而对于通过new关键字创建的对象如:String str = new String(“hello”),首先会在堆上开辟一块空间,然后去字符串常量池中找要创建的字符串(即找“hello”),看有没有一致的,如果有,则直接引用,即直接把该字符串在常量池中的地址保存到堆空间内,如果没有呢?则先在常量池中创建该字符串匿名对象即“hello”,然后再将字符串在常量池的地址保存到堆空间中。
看一下内存结构图:
想必此时已经非常清楚了入池操作,仅是改变栈空间的指向,由由以前的堆空间改变为常量池。
为什么说,堆空间保存得是常量池的地址呢?
String类的构造方法源码;
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
在这里插入代码片
引用类型做参数,为引用传递,函数体中是将一个数组名赋给String类的Value数组,本质是两个数组名都指向了常量区的字符串。如下代码所示
public class Test {
public static void main(String[] args) {
int[] arr1 = new int[2];
int[] arr2 = new int[2];
arr2 = arr1; //arr1,arr2都指向了arr1所指向的堆空间。
}
}
面试题:new String ("javaSE");
创建了几个对象?
答:一个或者两个,若常量池中原来就有“JavaSE”,则只创建一个对象,否则创建两个对象。一个在堆上,一个在常量池。
在String类内部,维护者着一个char类型的数组,且该数组被final修饰因此String类型是不可变更的。
private final char value[];
但是我们好像可以对String对象做很多变更操作,正如这样一段代码:
public static void main(String[] args) {
String str = "hello";
str += "world";
System.out.println(str);
}
输出结果:helloworld
这不是与字符串不可变更相矛盾吗?对于这段代码,会在堆上重新创建一个String对象,里面存储的是world,然后再重新开辟一块堆空间,存放两个字符串相加的结果即helloworld,并让str指向这块堆空间,因为前面的两块堆空间都没有了具体的栈空间指向,因此成为了垃圾值,会被垃圾回收系统回收。
具体图解:
由此可见对于String类来说,执行每一次+操作就会生成两块垃圾空间,为了解决这个问题,引入了StringBuffer,和 StringBuilder这两个类。
看这样一段代码
public static void main(String[] args) {
String str = "hehe";
for(int i = 0; i < 100; i++){
str += i;
}
System.out.println(str);
}
如果使用String类型每次+操作都会生成两块垃圾空间,造成资源浪费,因此在JVM内部会使用StringBuilder来优化String的+操作;
具体字节码如下
那岂不是说我们只要使用Sting就好了,反正底层都会帮我们优化。
再看这样一段代码
public static void main(String[] args) {
String str1 = "hello";
String str2 = str1 + "world";
String str3 = str1 + new String("java");
String str4 = str1 + str3;
}
反编译后:
可以发现new了很多StringBuilder对象。因此在涉及大量的字符串拼接工作还是使用StringBuffer或者String Builder。