Java基础: String及其相关类

本文深入探讨 Java 中 String 类及其相关类的功能与实现细节,包括 StringBuilder、StringBuffer 的内部机制及性能考量,StringJoiner 的使用场景,以及 String 类常用方法如 join、format 和 split 的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java基础: String及其相关类

AbstractStringBuilder

AbstractStringBuilder是一个抽象类,StringBuilder和StringBuffer都直接继承此类。
内部有两个属性值:

char[] value; // 存储数据
int count; // 数据的大小

数据存储在char数组中,数据存储范围是0~count。其数组扩容原理是原来数组长度*2+2,与传入的长度参数作比较,取两者的最大值。任何修改操作都是维护char数组和count值,原理都大同小异。下面重点讲reverse反转方法:

public AbstractStringBuilder reverse() {
    boolean hasSurrogates = false;
    int n = count - 1;
    for (int j = (n-1) >> 1; j >= 0; j--) {
        int k = n - j;
        char cj = value[j];
        char ck = value[k];
        value[j] = ck;
        value[k] = cj;
        if (Character.isSurrogate(cj) ||
            Character.isSurrogate(ck)) {
            hasSurrogates = true;
        }
    }
    if (hasSurrogates) {
        reverseAllValidSurrogatePairs();
    }
    return this;
}

private void reverseAllValidSurrogatePairs() {
    for (int i = 0; i < count - 1; i++) {
        char c2 = value[i];
        if (Character.isLowSurrogate(c2)) {
            char c1 = value[i + 1];
            if (Character.isHighSurrogate(c1)) {
                value[i++] = c1;
                value[i] = c2;
            }
        }
    }
}

第一行的hasSurrogates是什么意思呢。这里要先讲一下字符的Surrogates(代理)。

现实世界里的所有字符都可以用Unicode编码来表示,但要让计算机能存储,就要转换成二进制形式,叫做Unicode转换格式(Unicode Transformation Formats),即UTF。UTF有多种编码,有UTF-8/UTF-16/UTF-32,Java内部的字符编码为UTF-16,一般用2个字节(即16bit)来存储,那么就有2^16=65536个值。

但这个值远小于Unicode的数量,怎么办呢?

UTF-16编码范围为\u0000~\uFFFF,其中规定\uD800~\uDBFF为High Surrogates(高代理),\uDC00~\uDFFF为Low Surrogates(低代理),各有1024个,两者成对出现,高代理在前,低代理在后,组成一个SurrogatePair来表示一个Unicode码,这样就多出来了1024*1024个字符。详情可参考Character类,里面也有判断是否是High Surrogates/Low Surrogates/Surrogates/SurrogatePair的方法。

现在再来看reverse方法,for循环就是从中间索引 j 开始,交换(count-1-j) 索引的值,随着j--,直到 j==0,整个char数组就都反转了。但是如果存在Surrogates,就变成了低代理在前,高代理在后了,不是一个Unicode码了。如果存在这种情况就调用reverseAllValidSurrogatePairs方法,反转SurrogatePair。for循环里如果当前字符为Low Surrogates,下一个字符为High Surrogates,就调换。调换代码转换成这样更清晰易懂。

value[i] = c1;
i++;
value[i] = c2;

StringBuilder/StringBuffer

这两个类一起说,都是继承AbstractStringBuilder 类,默认初始容量都是16,里面的大部分方法也都是直接调用父类的方法,不同之处是StringBuffer 的大部分方法加了synchronized ,是线程安全的,里面也多了个属性char[] toStringCache,缓存父类value的0~count的数据,但任何修改操作都会将其置为null。推荐使用StringBuilder ,因为效率更高。

StringBuilder sb = new StringBuilder();
sb.append("a").append(1).append('-').append(true);
sb.toString(); // a1-true

StringJoiner

StringJoiner用于构造由分隔符分割,包含前缀和后缀的字符序列。

StringJoiner sj = new StringJoiner(",", "{", "}");
sj.add("George").add("Sally").add("Fred");
sj.toString(); // {George,Sally,Fred}

StringJoiner内部维护了一个StringBuilder value 属性,数据格式为前缀+元素+分隔符形式,如 {George,Sally,Fred ,方便添加元素。只有调用toString()方法时 value 才追加后缀获取到String ,之后再移除value 的后缀,恢复到原来的格式。


String

String类方法有很多,我只讲我之前不熟的,呵呵。

  • join方法
public static String join(CharSequence delimiter, CharSequence... elements) {
    StringJoiner joiner = new StringJoiner(delimiter);
    for (CharSequence cs: elements) {
        joiner.add(cs);
    }
    return joiner.toString();
}

这没啥说的,StringJoiner的构造函数只传分隔符的话,默认前后缀都是"" ,看例子:

String message = String.join("-", "Java", "is", "cool");
// "Java-is-cool"
  • format方法
 public static String format(String format, Object... args) {
    return new Formatter().format(format, args).toString();
}

String格式化输出方法,直接调用的java.util.Formatter的方法,这个类以后再说,先简单举例:

String.format("%s, %d, %f", "Fred", 1, 1.0);
// Fred, 1, 1.000000
  • split方法
 public String[] split(String regex, int limit) {
    /* fastpath if the regex is a
     (1)one-char String and this character is not one of the
        RegEx's meta characters ".$|()[{^?*+\\", or
     (2)two-char String and the first char is the backslash and
        the second is not the ascii digit or ascii letter.
     */
    char ch = 0; // 要匹配的字符
    if (((regex.value.length == 1 &&
         ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
         (regex.length() == 2 &&
          regex.charAt(0) == '\\' &&
          (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
          ((ch-'a')|('z'-ch)) < 0 &&
          ((ch-'A')|('Z'-ch)) < 0)) &&
        (ch < Character.MIN_HIGH_SURROGATE ||
         ch > Character.MAX_LOW_SURROGATE))
    {
        int off = 0; // indexOf方法的偏移量
        int next = 0; // 符合条件的索引
        boolean limited = limit > 0; // 数组是否有大小限制
        ArrayList<String> list = new ArrayList<>();
        while ((next = indexOf(ch, off)) != -1) {
            if (!limited || list.size() < limit - 1) {
                list.add(substring(off, next));
                off = next + 1;
            } else {    // last one
                //assert (list.size() == limit - 1);
                list.add(substring(off, value.length));
                off = value.length;
                break;
            }
        }
        // If no match was found, return this
        if (off == 0)
            return new String[]{this};

        // Add remaining segment
        if (!limited || list.size() < limit)
            list.add(substring(off, value.length));

        // Construct result
        int resultSize = list.size();
        if (limit == 0) {
            while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
                resultSize--;
            }
        }
        String[] result = new String[resultSize];
        return list.subList(0, resultSize).toArray(result);
    }
    return Pattern.compile(regex).split(this, limit);
}
public String[] split(String regex) {
    return split(regex, 0);
}

split方法可以根据regexString分割成一个String数组,如果limit大于0,就分割成指定的大小。假设 String str = "boo:and:foo"

regexlimitresult
2{ “boo”, “and:foo” }
5{ “boo”, “and”, “foo” }
-2{ “boo”, “and”, “foo” }

再来看源码,上来有个if判断,如果不符合条件就调用正则表达式类
java.util.regex.Pattern 来分割字符串,这个现在先不管。符合if条件有几中情况:1. 当regex只有一个字符,并且不能是 .$|()[{^?*+\\ 其中任何一个(\\ 是反斜杠的转义,就是\);2. 由两个字符组成,第一个必须是\\ ,第二个不能是数字或字母;3. 前两个条件符合一个即可,若符合条件1,则第一个字符不属于字符高/低代理的范围,若符合条件2,则第二个字符不属于字符高/低代理的范围。

chif条件里就赋值了,regex 只有一个字符,ch = regex.charAt(0);只有两个字符,ch = regex.charAt(1) 。第一个while循环里,从0开始查找字符串里等于ch的字符,找到就截取0~next的字符串,放到list中,然后继续从next下一个字符(偏移量off = next + 1;)继续查找,直到 next = -1 跳出循环,或者list的大小等于limit-1,把剩余字符串都放到list中,如表格中 limit=2 的情况。

如果字符串中没有等于ch的字符,就返回只包含整个字符串的数组。

如果没用限制数组大小或者限定的数组大小太大,如表格中 limit=-2或者limit=5 的情况,就把字符串剩余的部分添加进去。因为这两种情况下,while循环里的else语句根本没有执行。

最后是将list转换为String数组,但是我没看懂为什么limit=0 时,不返回list中最后的元素为空的数据,这就导致了下面的两种情况:

String str = "a:b:c::";
str.split(":"); // [a, b, c]
str.split(":", 10); // [a, b, c, , ]

看源码时发现一个问题,就是regex\\| 的时候,匹配的字符第二个字符| ,而不是\\| ,这就导致了问题,如果我们想匹配的就是\\| ,而不是转移后的| 呢。看下面的两个例子就知道了:

String s1 = "a\\|b|c\\|d";
s1.split("\\|"); // 不走正则,返回错误结果:[a\, b, c\, d]
s1.split("\\\\\\|"); // 走正则,返回预期结果:[a, b|c, d]
// "\\\\\\|"转义后是"\\|"

我们想匹配的是\\| ,但第一种方法只匹配了| ,得出了错误结果。注意a\ 只是a\\ 转义后的结果。

String s2 = "a|b|c";
s2.split("\\|"); // 不走正则,返回预期结果:[a, b, c]
s2.split("|"); // 走正则,返回错误结果:[a, |, b, |, c]
// "|"在正则表达式中代表"或"的意思

这次我们想匹配 | ,参数就要写\\| 了。

调用整个方法,稍不注意就会出现不符合预期的结果,要多注意。看这个方法最大的收获就是知道了 Stringsplit()方法什么时候走的正则,什么时候不是走的正则。

先这样吧,以后再碰见String相关类再补充。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值