首页>>后端>>java->String的相关问题都在这里了

String的相关问题都在这里了

时间:2023-12-05 本站 点击:0

本文基于 JDK11。

String 类

// java.util.Stringpublic 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的不可变性

为什么不可变

String 中的字节数组被 final 修饰,所以不能修改 value 的引用

字节数组还是 private 属性而且没有暴露任何修改 value 数组的方法

String 本身是被 final 修饰的,无法被继承,从而避免了子类覆盖父类方法的行为

String 中对字符串处理的方法(包括 +=)都会返回新的 String 对象并返回,不会影响原来的字符串

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

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

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

// code 2String 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

不可变的好处?

线程安全;

配合字符串常量池,如果 String 可变,那么一个引用改变就会影响其他的引用,常量池也就失去了其复用字符串的作用;

缓存哈希码,哈希码只需要计算一次;所以 String 作为 Map 的键可以提高性能。

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

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

字符串常量池

作用

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

字符串常量池的结构

字符串常量池可以看出一个哈希表,表中每一个Entry包含字符串的 hashCode 和一个指向String对象的指针(_literal,相当于是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); // falseSystem.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对象。

下面通过几个例子说明:

例一

// Example1String s1 = String.valueOf(1234);String s2 = s1.intern();String s3 = "1234";System.out.println(s1 == s2);  // trueSystem.out.println(s1 == s3);  // true

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

例二

// Example2String s1 = String.valueOf(1234);String s3 = "1234";String s2 = s1.intern();System.out.println(s1 == s2);  // falseSystem.out.println(s1 == s3);  // falseSystem.out.println(s2 == s3);  // true

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

JDK7之前

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

String/StringBuilder/StringBuffer

区别 String StringBuilder StringBuffer 线程安全 是 否 是 可变性 是 否 否 性能 低 高 高

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) {}

基本使用如下:

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

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

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

字符串比较

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

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

StringLatin1#equals 的实现如下:

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

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

原文:https://juejin.cn/post/7097845709541474334


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/java/12237.html