String虽然不属于Java的八大基本类型,但是在我们的日常开发中,使用它的次数是很频繁的。但是我想问下大家你们真的了解String么?
下面我从几个方面带大家重新了解下我们的String:
我相信每个java开发者,都知道在我们java中的String它是不可变的。
但是它的不可变性具体体现在哪呢?
1.不可继承。
2.对象不可变
在我们的IDE中查看我们的String类,我们的String类,是final修饰的。我们都知道当final修饰一个类的时候,代表这个类是不能被继承的。
我们写个MyString类继承String。
可以看到编译器报错。
我们常说的String不可变,是对象的数据不可改变,而非引用不可变。String对象一旦创建,对象的值是不能修改的。
String a = "abc";
a = "ggg";
System.out.println(a);
我们这段代码的打印结果是"ggg",而非"abc"。a = “ggg”;这句代码我们只是改变a的引用,而非改变了“abc”这个对象的值。我们的“abc"还在我们的常量池里。
String在我们的Java使用过于频繁,为了避免String对象的重复创建,Java引入了常量池的概念。常量池是怎么避免String对象的重复创建的呢?
下面我们来看几行代码。
String a = "abc";
String b = "abc";
System.out.println(a == b);
运行程序,控制台打印输出为true,告诉我们这两个对象是一个对象。为什么会这样呢,这就是我们常量池的作用了。当我们执行第一句代码的时候,先去我们的常量池里找,看是否存在“abc”,不存在创建一个“abc”放入常量池,并且返回它的引用。当我们执行第二段代码时,发现常量池里面已经存在“abc”了,直接返回它的引用。上述两行代码只创建了一个对象。
我再看一段代码:
String a = "abc";
String b = new String("abc");
System.out.println(a == b);
这次控制台输出了false,为什么呢?因为我们的a引用的是我们常量池中的“abc”,而我们的b引用的是我们的java堆的上对象,new 关键字新建一个对象的时候,每次都是重新开辟一块内存空间。两者当然不是同一个对象。
经典问题:String a = new String(“abc”);?
分析这个问题,首先我们要明确执行这段代码,jvm做了哪些事。
首先jvm会去常量池中找,看是否存在“abc”,不存在就创建一个“abc”放入常量池。由于又存在new这个关键字,还会在java堆上创建一个对象,存储“abc”。
所以这个问题的答案是:如果常量池中存在“abc”上述代码就创建了一个对象,如果常量池中不存在,上述代码会创建两个对象,一个在常量,一个在java堆。
我们的intern()方法可以把一个字符串放入常量池。
作用:首先去常量池中查找,是否存在当前字符串,如果存在直接返回它的引用,如果不存在创建这个字符串放入常量池,并返回它的引用。我们要明确一点intern()方法返回的一定是常量池中的引用。
下面我们来看一段代码。
String a = "abc";
String b = new String("abc").intern();
System.out.println(a == b);
控制台输入为true。就如我们上面所说的,intern方法返回的是常量池中“abc”的引用,同a,所以我们的a和b又相等了。
一般字符串的拼接我们可以使用三种方法:
1:操作符"+"
2:StringBuilder
3:StringBuffer
我们先来看一个问题:
String a = “a”+“b”+“c”;创建了几个对象。可能有些人会说3个,有些人会说四个。其实这段代码只创建了一个对象。由于"a",“b”,"c"都是常量 ,对于常量,编译时就直接存储它们的字面值而不是它们的引用 。在jdk1.5之前会转化为,StringBuffer对象连续的append()操作。在jdk1.5之后会转化为StringBuilder对象连续的append操作。
String a = "abc";
String b = "a"+"b"+"c";
上述a,b是指向的同一个对象。
还有一点我要说的就是:避免在我们的for,while循环中使用“+”拼接字符串。
我们来看两端代码:
第一段:
String result = "";
for(int i = 0 ;i < 100 ;i++){
result += i;
}
System.out.println(result);
第二段:
StringBuilder sb = new StringBuilder();
for(int i = 0 ;i < 100 ;i++){
sb.append(i).append("");
}
System.out.println(sb.toString());
我们的上述两端代码打印的结果是一样的,实际开发中我推荐大家使用第二种。为什么?上面我们提到,通过“+”拼接字符串,底层都是通过StringBuilder的append()方法来实现的,如果我们在for循环中使用"+"拼接字符串,每一次循环,我们都会创建一个StringBuilder对象。如果循环1000次,10000次,显然会降低我们程序的性能。而第二段代码中,我们始终用的是一个StringBuilder来进行的字符串拼接操作。
查看我们的源代码
//StringBuilder的构造方法,默认初始化了一个容量大小为16的char类型的数组。
public StringBuilder() {
super(16);
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
//同StringBuilder
public StringBuffer() {
super(16);
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
可以看到我们的StringBuilder和StringBuffer内部都是通过char[]来实现的。(jdk1.9后,底层把char 数组变成了byte[]。)唯一不同的就是我们的StringBuffer内部操作方法都加上了synchronized关键字,因为保证了线程安全,同时效率相比StringBuilder较低。
为什么Java要把String设计成不可变的呢,下面我们来讲一讲String不可变的好处。
1.安全,由于我们的String是不可变的,天生就具备了线程安全。
2.String经常作为参数,String不可变,代表我们的参数不可变。
3.常量池的需要,我们的String在我们的开发中,经常被使用,常量池会对我们的String进行缓存,只有String不可变,常量池再有意义。节约我们的内存空间。
4.当String作为我们的HashMap或者其他散列表key的时候,因为String不可变,所以其hash值也不会发生改变,我们可以不需要每次去计算,可以缓存其hash值,可以提高Hash表的效率。