金九银十,收下这份 Java String 面试题

简介: 金九银十,收下这份 Java String 面试题


学习路线图:


image.png

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


1.1 内存表示不同


  • 在 C 语言中,字符串和字符数组相同。字符串本质上是以 \0 为结束符的字符数组字符数组,因此字符串和字符数组在本质上相同,都是一块连续的内存空间,以需要转义 \0 为结束符。C 语言是不关心 char[] 里存储字符的编码方式的,只有通过程序的上下文确定;
  • 在 Java 中,字符串和字符数组不同。字符串是 String 对象,而字符数组是数组对象,均不需要结束符。如果是数组对象,对象内存区域中有一个字段表示数组的长度,而 String 相当于字符数组的包装类。内部包装了一个基于 UTF-16 BE 编码的字符数组(从 Java 9 开始变为字节数组)。其他字符编码输入的字节流在进入 String 时都会被转换为 UTF-16 BE 编码。


java.lang.String

public final class String {
    private final char value[];
    private int hash;
    ...
}
复制代码


1.2 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. 为什么 Java 9 String 内部将 char 数组改为 byte 数组?


Java String 的内存表示本质上是基于 UTF-16 BE 编码的字符数组。UTF-16 是 2 个字节或 4 个字节的变长编码,这意味着即使是 UniCode 字符集的拉丁字母,使用 ASCII 编码只需要一个字节,但是在 String 中需要两个字节的存储空间。


为了优化存储空间,从 Java 9 开始,String 内部将 char 数组改为 byte 数组,String 会判断字符串中是否只包含拉丁字母。如果是的话则采用单字节编码(Latin-1),否则使用 UTF-16 编码。


String.java (since Java 9)


private final byte coder;
static final boolean COMPACT_STRINGS;
static {
    COMPACT_STRINGS = true;
}
byte coder() {
    return COMPACT_STRINGS ? coder : UTF16;
}
@Native static final byte LATIN1 = 0;
@Native static final byte UTF16  = 1;
复制代码


不同编码实现的简单区别如下:


编码格式 编码单元长度 BOM 字节序
UTF-8-无BOM 1 ~ 4 字节 大端序
UTF-8 1 ~ 4 字节 EF BB BF 大端序
UTF-16-无BOM 2 / 4 字节 大端序
UTF-16BE(默认) 2 / 4 字节 FE FF 大端序
UTF-16LE 2 / 4 字节 FF FE 小端序
UTF-32-无BOM 4 字节 大端序
UTF-32BE(默认) 4 字节 00 00 FE FF 大端序
UTF-32LE 4 字节 FF EE 00 00 小端序


关于字符编码的更多内容,见: 计算机基础:今天一次把 Unicode 和 UTF-8 说清楚


3. String & StringBuilder & StringBuffer 的区别


3.1 效率


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


3.2 线程安全


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


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



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


4.1 如何让 String 不可变?


《Effective Java》中 可变性最小化原则,阐述了不可变类的规则:


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


以上规则 String 均满足。


4.2 为什么 String 要设计为不可变**?**


  • 1、不可变类 String 可以避免修改后无法定位散列表键值对: 假设 String 是可变类,当我们在 HashMap 中构建起一个以 String 为 Key 的键值对时,此时对 String 进行修改,那么通过修改后的 String 是无法匹配到刚才构建过的键值对的,因为修改后的 hashCode 可能是变化的。而不可变类可以规避这个问题。


  • 2、线程安全: 不可变对象本质是线程安全,不需要同步;


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


5. 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,在循环内统一操作这个对象。


6. String 对象的内存分配


6.1 "abc" 与 new String("abc") 的区别


  • "abc" => 虚拟机首先检查 运行时常量池 中是否存在 "abc",如果存在则直接返回,否则在字符串常量池中创建 "abc" 对象并返回。因此,多次声明使用的是同一个对象;
  • new String("abc") => 在编译过程中,Javac 会将 "abc" 加入到 Class 文件常量池 中。在类加载时期,Class 文件常量池会被加载进运行时常量池。在调用 new 字节码指令时,虚拟机会在堆中新建一个对象,并且引用常量池中的 "abc" 对象。


6.2 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
复制代码



7. 为什么 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;
}
复制代码


  • 原因 1 - 31 可以被编译器优化31∗i=(i<<5)−i31 * i = (i << 5) - i31i=(i<<5)i,位运算和减法运算的效率比乘法运算高。
  • 原因 2 - 31 是一个质数: 质数是只能被 1 和自身整除的数,使用质数作为乘法因子获得的散列值,在将来进行取模时,得到相同 index 的概率会降低,即降低了哈希冲突的概率。
  • 原因 3 - 31 是一个不大不小的质数: 质数太小容易造成散列值聚集在一个小区间,提供散列冲突概率;质数过大容易造成散列值超出 int 的取值范围(上溢),丢失部分数值信息,散列冲突概率不稳定。
目录
相关文章
|
13天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
44 2
|
2月前
|
Java 测试技术 开发者
Java零基础-indexOf(String str)详解!
【10月更文挑战第14天】Java零基础教学篇,手把手实践教学!
113 65
|
2天前
|
Java 程序员
Java社招面试题:& 和 && 的区别,HR的套路险些让我翻车!
小米,29岁程序员,分享了一次面试经历,详细解析了Java中&和&&的区别及应用场景,展示了扎实的基础知识和良好的应变能力,最终成功获得Offer。
23 14
|
18天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
23天前
|
存储 缓存 Oracle
Java I/O流面试之道
NIO的出现在于提高IO的速度,它相比传统的输入/输出流速度更快。NIO通过管道Channel和缓冲器Buffer来处理数据,可以把管道当成一个矿藏,缓冲器就是矿藏里的卡车。程序通过管道里的缓冲器进行数据交互,而不直接处理数据。程序要么从缓冲器获取数据,要么输入数据到缓冲器。
Java I/O流面试之道
|
20天前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
43 4
|
21天前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
71 4
|
2月前
|
存储 Java 程序员
Java面试加分点!一文读懂HashMap底层实现与扩容机制
本文详细解析了Java中经典的HashMap数据结构,包括其底层实现、扩容机制、put和查找过程、哈希函数以及JDK 1.7与1.8的差异。通过数组、链表和红黑树的组合,HashMap实现了高效的键值对存储与检索。文章还介绍了HashMap在不同版本中的优化,帮助读者更好地理解和应用这一重要工具。
55 5
|
2月前
|
存储 Java
[Java]面试官:你对异常处理了解多少,例如,finally中可以有return吗?
本文介绍了Java中`try...catch...finally`语句的使用细节及返回值问题,并探讨了JDK1.7引入的`try...with...resources`新特性,强调了异常处理机制及资源自动关闭的优势。
23 1
|
2月前
|
Java 测试技术 开发者
Java零基础-indexOf(String str)详解!
【10月更文挑战第13天】Java零基础教学篇,手把手实践教学!
45 1