美文网首页面试精选
「Java 路线」| String 常见面试题

「Java 路线」| String 常见面试题

作者: 彭旭锐 | 来源:发表于2020-12-22 15:28 被阅读0次

点赞关注,不再迷路,你的支持对我意义重大!

🔥 Hi,我是丑丑。本文 「Java 路线」| 导读 —— 他山之石,可以攻玉 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)

前言

  • 在每种编程语言里,字符串都是一个躲不开的话题,也是面试常常出现的问题;
  • 在这篇文章里,我将总结 Java 字符串中重要的知识点 & 面试题,如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

目录


1. C 和 Java 中字符串和字符数组的对比

  • 实现原理

在 C 语言中,字符串和字符数组本质上都是一块连续的内存空间,需要转义0(\0)结束符;

在 Java 中,字符串和字符数组有本质区别,字符串是 String 对象,而字符数组是数组对象,均不需要结束符。如果是数组对象,对象内存区域中有一个字段表示数组的长度,而 String 相当于字符数组的包装类。

java.lang.String

public final class String {

    private final char value[];

    private int hash;

    ...
}
  • char 类型的数据长度

在 C 语言中,char 类型占 1 字节,分为有符号与无符号两种;
在 Java 中,char 类型占 2 字节,只有无符号类型。

语言 类型 存储空间(字节) 最小值 最大值
Java char 2 0 65535
C char(相当于signed char) 1 -128 127
C signed char 1 -128 127
C unsigned char 1 0 255

2. String & StringBuilder & StringBuffer 的区别

  • 操作效率

String 是不可变的,每次操作都会创建新的变量,而另外两个是可变的,不需要创建新的变量;另外,StringBuffer 的每个操作方法都使用 synchronized 关键字保证线程安全,增加了更多加锁 & 释放锁的时间。因此,操作效率排序为:StringBuilder > StringBuffer > String。

  • 线程安全

String 不可变,所以 String 和 StringBuffer 都是线程安全的,而 StringBuilder 是非线程安全的。

类型 操作效率 线程安全
String 安全(final)
StringBuffer 安全(synchronized)
StringBuilder 非安全

3. 为什么 String 设计为不可变类?

  • 如何使得 String 不可变?
    《Effective Java》中可变性最小化原则,阐述了不可变类的规则:

    • 1、不对外提供修改对象状态的任何方法;
    • 2、保证类不会被扩展(声明为 final 类或 private 构造器);
    • 3、声明所有域为 final;
    • 4、声明所有域为 private;
    • 5、确保对于任何可变性组件的互斥访问。
  • 为什么设计为不可变类?

    • 1、相比可变类的不确定性,不可变类稳定可靠,适合作为散列表的键;
    • 2、不可变对象本质是线程安全的,不需要同步;
    • 3、风险:创建不可变类的对象代价可能很高,为提高性能使用可变配套类,StringBuilder 和 StringBuffer 可以理解为 String 的配置类。

提示: 反射可以破坏 String 的不可变性。


4. String + 的实现原理

String+操作符是编译器语法糖,编译后+操作符被替换为StringBuilder#append(...),例如:

源码:

String string = null;
for (String str : strings) {
    string += str;
}
return string;

编译产物:

String string = null;
for(String str : strings) {
    StringBuilder builder = new StringBuilder();
    builder.append(string);
    builder.append(str);
    string = builder.toString();
}

字节码:

 0 aconst_null
 1 astore_1
 2 aload_0
 3 astore_2
 4 aload_2
 5 arraylength
 6 istore_3
 7 iconst_0
 8 istore 4
10 iload 4
12 iload_3
13 if_icmpge 48 (+35)
16 aload_2
17 iload 4
19 aaload
20 astore 5
22 new #7 <java/lang/StringBuilder>
25 dup
26 invokespecial #8 <java/lang/StringBuilder.<init>>
29 aload_1
30 invokevirtual #9 <java/lang/StringBuilder.append>
33 aload 5
35 invokevirtual #9 <java/lang/StringBuilder.append>
38 invokevirtual #10 <java/lang/StringBuilder.toString>
41 astore_1
42 iinc 4 by 1
45 goto 10 (-35)
48 aload_1
49 areturn

可以看到,在循环里直接使用字符串+,会生成非常多中间变量,性能非常差,应该在循环外新建一个 StringBuilder,在循环内统一操作这个对象。


5. String 对象的内存分配

  • "abc" 与 new String("abc")

"abc" => 字符串常量池中的对象,多次声明使用的是同一个对象;
new String("abc") => 不管常量池中是否有相同的字符串,都新建一个对象,多次调用是不同对象。

  • String#intern() 的实现原理

如果字符串常量池中已经包含一个等于此 String 对象的字符串,则返回常量池中的这个字符串;否则,先将此 String 对象包含的字符串拷贝到常量池中,在常量池中的这个字符串。

从 JDK 1.7 开始,String#intern() 不再拷贝字符串到常量池中,而是在常量池中生成一个对原 String 对象的引用,并返回。

举例:
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);

 
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);

输出结果为:
JDK1.6以及以下:false false
JDK1.7以及以上:false true

6. 为什么 String#haseCode() 要使用 31 作为因子?

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}
  • 31 可以被编译器优化

31 * i = (i << 5) - i

提示: 位运算和减法运算的效率比乘法运算高。

  • 31 是一个质数

质数是只能被 1 和自身整除的数,使用质数作为乘法因子获得的散列值,在将来进行取模时,得到相同 index 的概率会降低,即降低了哈希冲突的概率。

  • 31 是一个不大不小的质数

质数太小容易造成散列值聚集在一个小区间,提供散列冲突概率;
质数过大容易造成散列值超出 int 的取值范围(上溢),丢失部分数值信息,散列冲突概率不稳定。


创作不易,你的「三连」是丑丑最大的动力,我们下次见!

相关文章

网友评论

    本文标题:「Java 路线」| String 常见面试题

    本文链接:https://www.haomeiwen.com/subject/azqsnktx.html