Java面试题详解:String相关

String类是java中特别重要也是用的最多的一个类了,掌握好这个类非常有必要,在面试中也是经常被问到。

常见的问题:

  1. String是基本类型吗?
  2. String中有哪些常用方法?
  3. String和StringBuffer,StringBuilder的区别?
  4. 为什么Java中String被设计成不可变类,有什么好处?
  5. String是线程安全的吗?
  6. String s1="abc"; String s2 = new String("abc"); s1 == s2?
  7. 什么是字符串常量池?
  8. 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中呢?

  1. 代码中直接使用双引号引着的字符串都会被存储到字符串常量池中,如:String s = "abc";

  2. 调用String的 intern()方法,如果字符串内容是字符串常量池中没有的,那么会先复制一份内容到字符串常量池中,再返回字符串常量池中的引用。

既然要用字符串常量池,那字符串必然要不可变的,否则字符串共享复用会有问题,会有严重的线程安全问题,这便是我认为的字符串String被设计为不可变的根本原因。

对于String是否是线程安全的问题,既然String是不可变类,那么同一个字符串在多线程环境下,值不会发生改变,必然是线程安全的。

相信读到这里,文章开头的几个面试题你心中已有答案了吧,如还有疑问,欢迎评论区留言。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

emeson_ch

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值