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

Hi,我是小彭。本文已收录到 GitHub · Android-NoteBook 中。这里有 Android 进阶成长常识体系,有气味相投的敌人,关注公众号 [彭旭锐] 带你建设外围竞争力。

前言

大家好,我是小彭。

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

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


学习路线图:


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 字节,只有无符号类型。
语言类型存储空间(字节)最小值最大值
Javachar2065535
Cchar(相当于signed char)1-128127
Csigned char1-128127
Cunsigned 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》中 可变性最小化准则,论述了不可变类的规定:

  • 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 410 iload 412 iload_313 if_icmpge 48 (+35)16 aload_217 iload 419 aaload20 astore 522 new #7 <java/lang/StringBuilder>25 dup26 invokespecial #8 <java/lang/StringBuilder.<init>>29 aload_130 invokevirtual #9 <java/lang/StringBuilder.append>33 aload 535 invokevirtual #9 <java/lang/StringBuilder.append>38 invokevirtual #10 <java/lang/StringBuilder.toString>41 astore_142 iinc 4 by 145 goto 10 (-35)48 aload_149 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 falseJDK1.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) - i$,位运算和减法运算的效率比乘法运算高。
  • 起因 2 - 31 是一个质数: 质数是只能被 1 和本身整除的数,应用质数作为乘法因子取得的散列值,在未来进行取模时,失去雷同 index 的概率会升高,即升高了哈希抵触的概率。
  • 起因 3 - 31 是一个不大不小的质数: 质数太小容易造成散列值汇集在一个小区间,提供散列抵触概率;质数过大容易造成散列值超出 int 的取值范畴(上溢),失落局部数值信息,散列抵触概率不稳固。
我是小彭,带你构建 Android 常识体系。技术和职场问题,请关注公众号 [彭旭锐]私信我发问。