请点赞关注,你的支持对我意义重大。

Hi,我是小彭。本文已收录到 GitHub · Android-NoteBook 中。这里有 Android 进阶成长知识体系,有志同道合的朋友,关注公众号 [彭旭锐] 带你建立核心竞争力。

前言

大家好,我是小彭。

过去两年,我们在掘金平台上发布 JetPack 专栏文章,小彭也受到了大家的意见和鼓励。最近,小彭会陆续搬运到公众号上。

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


学习路线图:

金九21道java面试题(金九银十收下这份)(1)


1. C 和 java 中字符串和字符数组的对比1.1 内存表示不同

java.lang.String

public final class String { private final char value[]; private int hash; ... }

1.2 char 类型的数据长度

语言类型存储空间(字节)最小值最大值Javachar2065535Cchar(相当于signed char)1-128127Csigned char1-128127Cunsigned char10255


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-无BOM1 ~ 4 字节无大端序UTF-81 ~ 4 字节EF BB BF大端序UTF-16-无BOM2 / 4 字节无大端序UTF-16BE(默认)2 / 4 字节FE FF大端序UTF-16LE2 / 4 字节FF FE小端序UTF-32-无BOM4 字节无大端序UTF-32BE(默认)4 字节00 00 FE FF大端序UTF-32LE4 字节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》中 可变性最小化原则,阐述了不可变类的规则:

以上规则 String 均满足。

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

提示: 反射可以破坏 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") 的区别6.2String#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; }

我是小彭,带你构建 Android 知识体系。技术和职场问题,请关注公众号 [彭旭锐]私信我提问。

,