likes
comments
collection
share

String的相关问题都在这里了

作者站长头像
站长
· 阅读数 41

本文基于 JDK11。

String 类

// java.util.String
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    private final byte[] value;
    private final byte coder;
    private int hash; // Default to 0
    static final boolean COMPACT_STRINGS;
    static {
        COMPACT_STRINGS = true;
    }
    public static final Comparator<String> CASE_INSENSITIVE_ORDER
                                     = new CaseInsensitiveComparator();
}

value 字节数组存储字符串内容。int hash 缓存字符串的哈希码。

coder 表示字符串编码方式,编码方式有两种,0 表示使用 LATIN1 编码,每个字符占用 1 字节,1 表示使用 UTF16 编码,每个字符占用 2 字节。

用 0/1 表示两种编码,在计算字符串长度时可以直接使用 value.length >> coder

COMPACT_STRINGS 表示是否压缩字符串,如果为 false,将只会使用 UTF16 编码方式;JVM 默认该属性为 true,大部分场景使用 1 字节就够了。可以使用 -XX:-CompactStrings 参数来对此功能进行关闭。

CASE_INSENSITIVE_ORDER 是一个 Compactor,定义了字符串比较的规则(大小写敏感)。

String的不可变性

为什么不可变

  1. String 中的字节数组被 final 修饰,所以不能修改 value 的引用
  2. 字节数组还是 private 属性而且没有暴露任何修改 value 数组的方法
  3. String 本身是被 final 修饰的,无法被继承,从而避免了子类覆盖父类方法的行为
  4. String 中对字符串处理的方法(包括 +=)都会返回新的 String 对象并返回,不会影响原来的字符串

只有当成功地对字符串进行了相关操作,才会返回新的 String 对象。比如 trim() 是去除首尾的空格符,如果字符串的长度为0首尾没有空格符,会返回原对象(如 code 1)。

// code 1
String str1 = "abc";
String str2 = "";
String str3 = " def ";
System.out.println(str1 == str1.trim()); // true
System.out.println(str2 == str2.trim()); // true
System.out.println(str3 == str3.trim()); // false

注意,通过反射仍然可以修改字节数组的值(如 code 2)。

// code 2
String str = new String("abc");

Field field = str.getClass().getDeclaredField("value");
field.setAccessible(true);
byte[] value = (byte[]) field.get(str);
value[0] = 'd';

System.out.println(str); // sout: dbc

不可变的好处?

  1. 线程安全;
  2. 配合字符串常量池,如果 String 可变,那么一个引用改变就会影响其他的引用,常量池也就失去了其复用字符串的作用;
  3. 缓存哈希码,哈希码只需要计算一次;所以 String 作为 Map 的键可以提高性能。

字符数组改为字节数组的好处

在大多数场景下,1 个字节表示字符就足够了,所以 JDK9 将字符数组改为字节数组,并配合新增的 coder 属性,可以减少 String 的空间占用。

字符串常量池

作用

String 是使用频率很高,为了复用提高性能,引入了字符串常量池。字符串常量池位于中,JDK7 之前在方法区中。

字符串常量池的结构

字符串常量池可以看出一个哈希表,表中每一个Entry包含字符串的 hashCode 和一个指向String对象的指针(_literal,相当于是String对象的地址)。

所以如果字符串常量池中包含一个字面量时,结构如下图所示。

String的相关问题都在这里了

String的创建

创建 String 有两种方式:字面量new

当使用字面量时,如果常量池中已存在,直接返回引用(_literal的指向);如果不存在,就将该字符串加入常量池(即创建一个Entry)再返回引用。

当使用 new("xxx") 创建时,如果常量池中已经存在,在堆中创建一个 String 对象,并返回该对象的引用;如果常量池中不存在,则先在常量池中国创建该字符串,再创建 String 对象,返回该对象的引用。

所以,使用字面量方式引用同一个字符串,它们的引用一定相等。

String str1 = "abc";
String str2 = "abc";
System.out.println(str1 == str2); // true

使用 new 创建的对象即使字符串是相等的,引用也不相同,与字符串常量池的引用也不相同。

String str1 = "abc";
String str2 = new String("abc");
String str3 = new String("abc");
System.out.println(str1 == str2); // false
System.out.println(str2 == str3); // false

🛎️ 注意 对于多个字面量 += 的情况,编译时会对其优化,最终只会生成要给字面量。比如 String s = "1" + "2",优化后等价于 String s = "12"

除了以上两种主要的创建方式,创建一个String还可以通过其他的写法,这些写法都不包含字面量,不会在常量池中创建对象。这些写法包括不限于如下所示。

// 创建一个内容为"111"的字符串对象
String s1 = new String(new byte[]{49,49,49});
String s2 = String.valueOf(111);
String s3 = String.valueOf(1) + String.valueOf(11);
String s4 = String.format("%d%d%d", 1,1,1);
String s5 = new String("11") + "1";

intern() 方法

调用 intern() 时,如果该字符串在常量池中存在,那么返回常量池中的引用;如果常量池中不存在,则创建并返回对象引用。

🛎️ 注意 实际上,这里说不存在就创建并不准确,这是因为字符串常量池在 JDK7 以后从永久代移到了中,位置的变化就会导致 intern() 效果的变化。

JDK7及以后

牢记一点:由于字符串常量池就在堆中,所以会尽可能的避免重复创建,如果字符串常量池中不存在,就将 _literal 指向已经在堆中的 String 对象,而不是重新创建一个String对象。

下面通过几个例子说明:

  • 例一
// Example1
String s1 = String.valueOf(1234);
String s2 = s1.intern();
String s3 = "1234";
System.out.println(s1 == s2);  // true
System.out.println(s1 == s3);  // true

创建 s1 时没有生成 "1234" 的字面量,所以常量池还没有 "1234";所以调用 s1.intern() 时会将 s1 加入常量池(具体就是创建一个Entry加入常量池,将 _literal 指向 s1)。最终的结构如下:

String的相关问题都在这里了

  • 例二
// Example2
String s1 = String.valueOf(1234);
String s3 = "1234";
String s2 = s1.intern();
System.out.println(s1 == s2);  // false
System.out.println(s1 == s3);  // false
System.out.println(s2 == s3);  // true

首先 s1 和 s3 不相等,因为它们和 intern() 无关,在堆中和常量池分别创建不同的对象;调用 s1.intern() 时,常量池中已经存在了 "1234",直接返回它的引用,即 s3。

String的相关问题都在这里了

JDK7之前

由于常量池和堆空间是隔离的区域,就不存在共享同一个String的问题了,调用 intern() 时,如果常量池不存在,会重新创建一个 String 对象。

String/StringBuilder/StringBuffer

区别StringStringBuilderStringBuffer
线程安全
可变性
性能

String 的线程安全是因为它的不可变性,StringBuffer 线程安全是因为所有方法都是 synchronized方法。

StringBuilder 和 StringBuffer 中的字节数组不是 final 修饰,对字符串操作时直接修改 value 属性,操作方法返回的都是 this,所以它们可以使用更方便的链式调用

StringBuilder#append 为例,

public StringBuilder append(String str) {
    super.append(str);
    return this; // 返回自己
}

由于 String 每次修改都会创建新的对象,所以性能更低。不建议在循环中频繁使用 String 的 += 或其他操作。

总结,字符串修改较少时,可以使用 String;单线程下大量修改使用 StringBuilder;多线程下大量修改使用 StringBuffer。

StringJoiner

StringJoiner 可以方便地进行字符串拼接。有两个构造函数,必须指定一个分割符 delimiter,也可以指定拼接完成后加入的前缀和后缀。StringJoiner 不是线程安全的。

public StringJoiner(CharSequence delimiter) {}
public StringJoiner(CharSequence delimiter,CharSequence prefix,CharSequence suffix) {}

基本使用如下:

StringJoiner joiner = new StringJoiner(".");
String localhost = joiner.add("127").add("0").add("0").add("1").toString();
System.out.println(localhost); // sout: 127.0.0.1

拼接字符串还可以调用 String 的静态方法 join(),该方法就是利用 StringJoiner 实现的。

public static String join(CharSequence delimiter, CharSequence... elements) {
    // ignore some code
    StringJoiner joiner = new StringJoiner(delimiter);
    for (CharSequence cs: elements) {
        joiner.add(cs);
    }
    return joiner.toString();
}

字符串比较

字符串比较使用 equals() 方法,首先判断是否是同一个对象,如果不是,判断是否是 String 类型的对象,如果是,先比较长度是否相等,不相等直接返回 false;长度相等则逐个比较,全部相等返回 true,否则返回 false。

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String aString = (String)anObject;
        if (coder() == aString.coder()) {
            return isLatin1() ? StringLatin1.equals(value, aString.value)
                              : StringUTF16.equals(value, aString.value);
        }
    }
    return false;
}

StringLatin1#equals 的实现如下:

public static boolean equals(byte[] value, byte[] other) {
    if (value.length == other.length) {
        for (int i = 0; i < value.length; i++) {
            if (value[i] != other[i]) { return false; }
        }
        return true;
    }
    return false;
}

此外,String 还提供了 equalsIgnoreCase(String) 以忽略大小写的方式比较字符串。

转载自:https://juejin.cn/post/7097845709541474334
评论
请登录