String类是java中特别重要也是用的最多的一个类了,掌握好这个类非常有必要,在面试中也是经常被问到。
常见的问题:
- String是基本类型吗?
- String中有哪些常用方法?
- String和StringBuffer,StringBuilder的区别?
- 为什么Java中String被设计成不可变类,有什么好处?
- String是线程安全的吗?
- String s1="abc"; String s2 = new String("abc"); s1 == s2?
- 什么是字符串常量池?
- String的intern方法作用?
String介绍:
我们知道Java中只有8种基本类型,分别是:
- boolean
- byte
- char
- short
- int
- float
- long
- double
这8种基本类型都有对应的包装类,分别是:
- Boolean
- Byte
- Character
- Short
- Integer
- Float
- Long
- Double
很显然String并不是一个基本类型,之所以会有这个问题,是因为我们常常这样去使用它:
String s = "abc";
但这种写法只是一种语法糖,并不表示String就是基本类型,这个需要注意。
String提供了很多方法,主要有二类:
- 静态方法
- 非静态方法
静态方法常用的有:
- String.valueOf 将其他类型对象转成其对应的字符串形式,有多个重载方法
- String.format 字符串格式化
非静态方法常用的有:
- trim 去掉字符串前后空字符
- toUpperCase/toLowerCase 字符串大小写转换
- split 字符串拆分,使用这个方法一定要注意方法参数是一个正则表达式,工作中新手很容易犯错
- replace/replaceAll 都是字符串替换,并且都是全部替换,区别是replace是根据字符串搜索替换的,replaceAll是根据正则表达式搜索替换的
- contains 是否包含某个子字符串
- subString 字符串截取
- indexOf/lastIndexOf 查询目标字符串的索引位置
- startsWith/endsWith 是否是某个前缀开头的或后缀结尾的
- charAt 返回指定索引处的字符
String被设计成一个不可变类,不可变主要体现在类的修饰符是final的,并且String内部用来存储其值的属性(一个名为value的字符数组)也是final的,同时没有暴露公共方法去修改value的值,因此String对象一旦创建就不能修改了(反射除外),在平时的编程中我们常用“+”号用来拼接字符串然后赋值给一个字符串变量,比如:
String s1 = s1 + "abc";
你要知道上面的写法并没有改变原s1指向的字符串值,而是新建了一个新的字符串,并且s1指向了新的字符串了,原字符串值并没有改变。
假如执行上面代码前,堆栈是这样的:
执行代码后:
正因为这样,如果在代码中,尤其在一个大的循环中,使用+号拼接字符串会产生大量临时字符串对象,对程序性能造成影响,因此才有了StringBuffer和StringBuilder,这二个类的实现原理都是在内部维护一个char数组,和String不同的是,String对象内部的char数组是不可变的,对象创建后就值就不再变了,但是StringBuilder和StringBuffer里面的char数组是动态可变的,有时我们也叫它动态字符串,就是当char数组长度不够时,创建一个新的数组并把老的数组复制到新数组中,等所有字符串拼接完成后再一次性根据char数组生成一个字符串对象,避免了频繁创建临时String对象的问题。
那么StringBuffer和StringBuilder有什么区别呢?这二个类的主要区别就是StringBuffer是线程安全的,StringBuilder是非线程安全的,但性能比StringBuffer好,通常在没有线程安全的情况下我们使用StringBuilder。
说了这么多,是不是我们以后在代码应该尽量避免使用+号拼接字符串呢?其实也不尽然,因为现在的java编译器已经自动将+号拼接字符串的代码用StringBuilder重写了,因此我们在代码中还是可以直接用+号拼接的,不过这仅限于简单的情况,对于一些复杂场景,比如在一个循环中拼接字符串:
String s = "";
for (int i = 0; i < 100; i++) {
s = s + i;
}
上面代码生成的字节码如下:
0 ldc #2
2 astore_1
3 iconst_0
4 istore_2
5 iload_2
6 bipush 100
8 if_icmpge 36 (+28)
11 new #3 <java/lang/StringBuilder>
14 dup
15 invokespecial #4 <java/lang/StringBuilder.<init> : ()V>
18 aload_1
19 invokevirtual #5 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
22 iload_2
23 invokevirtual #6 <java/lang/StringBuilder.append : (I)Ljava/lang/StringBuilder;>
26 invokevirtual #7 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
29 astore_1
30 iinc 2 by 1
33 goto 5 (-28)
上面的字节码对应的实际代码如下:
String s = "";
for (int i = 0; i < 100; i++) {
StringBuilder sb = new StringBuilder();
sb.append(s);
sb.append(i);
s = sb.toString();
}
你可以看到对于循环内部的拼接字符串的代码确实用StringBuilder重写了,但这不是我们想要的结果,我们期望的是StringBuilder在循环外创建。通过这个例子我们应该知道, 除了一些简单场景外,我们对于字符串拼接还是尽可能用StringBuilder来写,不能完全依赖编译器去优化。
上面我们说过String是不可变类,至于说为什么要这么设计,这个问题比较开放。我个人认为主要是String对象使用的太普遍了,而且一个系统中会用到大量的相同的字符串,如果每个字符串都创建一个对象并分配内存,那么将占用大量内存,并且这些字符串对象会频繁的创建和销毁,严重影响性能,如果能把这些字符串缓存起来放到一个地方,后面如果用到相同字符串的时候就不用再分配内存了,直接引用这些缓存的字符串就好了。
这里存放字符串缓存的地方就是字符串常量池,在JDK6及之前版本:字符串常量池是放在永久代中;在JDK7版本中:字符串常量池被移到了堆中。在HotSpot VM中字符串常量池是通过一个StringTable类(一个Hash表,并非java实现类,所以知道即可)实现的;这个StringTable在每个HotSpot VM的实例中只有一份,被所有的类共享;通过字符串常量池的使用避免了字符串常量的重复创建,节省了内存空间。
什么情况下生成的字符串才会被放到String Pool中呢?
-
代码中直接使用双引号引着的字符串都会被存储到字符串常量池中,如:String s = "abc";
-
调用String的 intern()方法,如果字符串内容是字符串常量池中没有的,那么会先复制一份内容到字符串常量池中,再返回字符串常量池中的引用。
既然要用字符串常量池,那字符串必然要不可变的,否则字符串共享复用会有问题,会有严重的线程安全问题,这便是我认为的字符串String被设计为不可变的根本原因。
对于String是否是线程安全的问题,既然String是不可变类,那么同一个字符串在多线程环境下,值不会发生改变,必然是线程安全的。
相信读到这里,文章开头的几个面试题你心中已有答案了吧,如还有疑问,欢迎评论区留言。