Java基础之String

简介: Java基础之String

.1 String既然已经实现了Comparable接口, 为什么还要提供内部类----CaseInsensitiveComparator;
.2 使用 "+" 拼接String究竟干了什么? 为什么在循环中不让使用"+"拼接String;

  1. String为什么要提供内部类CaseInsensitiveComparator

先来看下String实现了Comparable接口后做了什么:

 public int compareTo(String anotherString) {
    int len1 = value.length;
    int len2 = anotherString.value.length;
    int lim = Math.min(len1, len2);
    char v1[] = value;
    char v2[] = anotherString.value;

    int k = 0;
    while (k < lim) {
        char c1 = v1[k];
        char c2 = v2[k];
        if (c1 != c2) {
            return c1 - c2;
        }
        k++;
    }
    return len1 - len2;
}

String::compareTo做了三件事:
.1 比较两个字符串的长度, 找出最小值;
.2 比较最小长度中的字符是否相同, 因底层使用ASCII码存储, 10进制的ASCII是纯数字, 可直接减得出比较结果(compareTo规定: 返回-1是小于; 0是等于; 1是大于);
.3 如果最小长度的字符都相同, 再比较两个字符串的长度是否相同;

字符串是可能含有大小写的, 在String::compareTo中认为A和a是不同的, 那么在忽略大小写的场景中就不适用了;既然String提供了基于Comparator的内部类, 是不是对这种场景做了特殊处理呢?我们接下来看CaseInsensitiveComparator的核心实现:

 public int compare(String s1, String s2) {
        int n1 = s1.length();
        int n2 = s2.length();
        int min = Math.min(n1, n2);
        for (int i = 0; i < min; i++) {
            char c1 = s1.charAt(i);
            char c2 = s2.charAt(i);
            if (c1 != c2) {
                c1 = Character.toUpperCase(c1);
                c2 = Character.toUpperCase(c2);
                if (c1 != c2) {
                    c1 = Character.toLowerCase(c1);
                    c2 = Character.toLowerCase(c2);
                    if (c1 != c2) {
                        // No overflow because of numeric promotion
                        return c1 - c2;
                    }
                }
            }
        }
        return n1 - n2;
    }

可以看到compare的逻辑和String:compareTo大同小异, 只是在第二步的时候做了特殊处理:
.1 先将char字符转换成大写作比较(如果是数字则不变);
.2 如果大写比较不符, 再转换成小写做比较;
.3 如果小写比较还是不符, 证明该char字符为数字, 直接比较即可;

上面只是说明了这两者实现的不同, 还是没有说明为什么这么实现; 要解答这个首先需要说明下Comparable 和 Comparator的异同:
.1 两者都是接口, 都是实现对象的比较的, 返回值都是{-1, 0, 1};
.2 Comparable需要重写Comparable::compareTo方法, 会对比较对象的代码形成侵入; Comparator由一个比较目标对象的策略类来实现, 同时比较策略则由编写者指定, 无需侵入比较对象的代码;
故而String实现Comparable接口提供了一种内排序的方式, 而Comparator提供了一种不改变比较对象代码, 实现比较的策略, 如果对CaseInsensitiveComparator的实现并不满意, 也可以自己实现MySelfComparator;

划重点:
.1 CaseInsensitiveComparator的实现只是String作者提供了一种不同于String::compareTo的比较策略, 如果说Compareable是比较的内部实现, 那么Comparator就是比较的外部实现;
.2 Comparator这种方式实现了策略模式, 将变与不变完美分类; 关于设计模式后面再开专题分享;
.3 Comparator接口中还有个equals方法没有实现, 不实现这个方法为什么不报错呢? 因为所有类的父类都是Object, Object::equals已经对这个方法做了实现, 也就不报错了;
.4 如果Compareable::compareTo 或者 Comparator::compare的实现的比较结果与equals不符时, 你需要考虑这种情况会不会有影响;比如HashMap中先调用equals再调用的compareTo, 这时候如果equals与compareTo的结果是不一致, 不就引起问题了; 虽然实现了Compareable接口不强制重写equals方法, 但是不一致的情况还是需要考虑下的;

  1. String字符串拼接的三种方式比较

对于字符串拼接, 我们可以使用一下三种方式:
.1 "+", 加号拼接是我们最熟悉的;
.2 concat方法, 调用String::concat方法实现拼接;
.3 StringBuild::append方法实现拼接;
我们先来看看三种拼接方式的效率差异:

long startTime = System.currentTimeMillis();
    
    String temp = "123";
    for(int i = 0; i < 100000; i++) {
        temp = temp + "123";
    }
    System.out.println(String.format("+ 拼接用时: %d毫秒", System.currentTimeMillis() - startTime));
    
    startTime = System.currentTimeMillis();
    temp = "123";
    for(int i = 0; i < 100000; i++) {
        temp = temp.concat("123");
    }
    System.out.println(String.format("concat 拼接用时: %d毫秒", System.currentTimeMillis() - startTime));

    startTime = System.currentTimeMillis();
    StringBuilder str = new StringBuilder("123");
    for(int i = 0; i < 100000; i++) {
        str.append("123");
    }
    temp = str.toString();
    System.out.println(String.format("StringBuilder 拼接用时: %d毫秒", System.currentTimeMillis() - startTime));

这是实验代码, 分别使用"+", concat 和 StringBuild::append 进行了10万次的字符串拼接; 拼接的字符串统一使用""的静态字符串, 从前次的分享可知这种声明的字符串会被缓存在JVM的常量池中, 所以三种方式都是对同一个对象的不断拼接最终形成新的String对象;那我们来看看结果:

拼接结果一
这是按上面代码顺序执行的结果, 可以清晰的看到, 在10万这个数量级, 使用"+"进行拼接字符串的效率明显低于其他两种拼接方式, 为什么使用"+"拼接会这么慢呢?
.1我们来看下"+"拼接字符串的底层实现:
编译器对这种方式做了优化: 上面for循环中的代码被优化成:

temp = new StringBuilder(temp).append("123").toString();

. 每次拼接都会new 一个StringBuild对象;
. 调用StringBuild::append进行拼接;
. 再调用StringBuild::toString生成新的String对象;
知道了"+"拼接的底层原理, 试着来分析下这种方式慢的原因:
.1.1 每次拼接都会生成两个新的对象, StringBuild 和 String, 创建一次对象就要消耗一次操作时间;
.1.2 创建对象就需要申请内存, 而整个应用的内存空间是固定的, 循环次数多了以后, 必然导致创建对象时内存不够用, 这时候就会触发GC, 而GC为了清理无效对象, 会停止应用(stop the world), 这是一个及其耗时的操作;
.1.3 "+"拼接的方式慢在创建对象和GC;

.2 我们再来看下concat的拼接:

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}

从String::concat的实现可知这种拼接方式做了什么:
.1 判空, 拼接的字符串是空的, 返回原字符串;
.2 新生成一个char[], 长度是旧字符串和拼接字符串的长度之和;
.3 将旧字符串拷贝到新数组中;
.4 将拼接字符串拷贝到新数组中;
.5 返回一个机遇新数组的String对象;
从底层实现可看出这种拼接方式的耗时操作主要是新建String对象和两次数组拷贝操作; 同时也要看到String::concat也是每次调用都会返回一个新的String对象;

.3 最后来看下最快的StringBuild::append的实现:

public AbstractStringBuilder append(String str) {
    if (str == null)
        return appendNull();
    int len = str.length();
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;
}

可看出这种方式String::concat的核心思想基本相同, 但是有三点不同:
.1 StringBuild在生成时会维护一个长度可变的char[], 默认大小是构造函数传入字符串的长度加16; 所以每次每次都会判断是否拼接字符串的长度加上已有字符串的长度是否超过数组的长度; 超过数组就扩容(大小是当前数组长度 << 1 + 2), 然后拷贝现有数据至新数组;
.2 判空逻辑更改: 不会直接返回而会拼接"null"字符串;
.3 最后就是返回的不是String而是当前的StringBuild对象, 只有在调用StringBuild::toString时才会返回新的String对象;
StringBuild::append不仅减少新对象的产生, 连数组的拷贝操作也尽量减少了, 他拼接耗时最少也就不足为奇了;

划重点:
.1 字符串拼接耗时:StringBuild::append < String::concat < "+";
.2 在循环中不要使用"+"进行字符串拼接;
.3 对于上面的例子因为涉及到了JVM的常量池, 所以又做了一次验证, 把StringBuild::append 和 “+”的执行顺序做了对调, 下面是执行的结果:

拼接结果二
第一点的结论依然成立;
.4 StringBuild 和 StringBuffer都是继承了AbstractStringBuilder这个抽象类, 两个唯一的区别就是StringBuffer是线程安全的(所有方法都用了synchronized做了修饰);

相关文章
|
28天前
|
编解码 Java 开发者
Java String类的关键方法总结
以上总结了Java `String` 类最常见和重要功能性方法。每种操作都对应着日常编程任务,并且理解每种操作如何影响及处理 `Strings` 对于任何使用 Java 的开发者来说都至关重要。
186 5
|
3月前
|
存储 SQL 缓存
Java字符串处理:String、StringBuilder与StringBuffer
本文深入解析Java中String、StringBuilder和StringBuffer的核心区别与使用场景。涵盖字符串不可变性、常量池、intern方法、可变字符串构建器的扩容机制及线程安全实现。通过性能测试对比三者差异,并提供最佳实践与高频面试问题解析,助你掌握Java字符串处理精髓。
|
4月前
|
自然语言处理 Java Apache
在Java中将String字符串转换为算术表达式并计算
具体的实现逻辑需要填写在 `Tokenizer`和 `ExpressionParser`类中,这里只提供了大概的框架。在实际实现时 `Tokenizer`应该提供分词逻辑,把输入的字符串转换成Token序列。而 `ExpressionParser`应当通过递归下降的方式依次解析
277 14
|
Java 索引
java基础(13)String类
本文介绍了Java中String类的多种操作方法,包括字符串拼接、获取长度、去除空格、替换、截取、分割、比较和查找字符等。
162 0
java基础(13)String类
|
8月前
|
缓存 安全 Java
《从头开始学java,一天一个知识点》之:字符串处理:String类的核心API
🌱 **《字符串处理:String类的核心API》一分钟速通!** 本文快速介绍Java中String类的3个高频API:`substring`、`indexOf`和`split`,并通过代码示例展示其用法。重点提示:`substring`的结束索引不包含该位置,`split`支持正则表达式。进一步探讨了String不可变性的高效设计原理及企业级编码规范,如避免使用`new String()`、拼接时使用`StringBuilder`等。最后通过互动解密游戏帮助读者巩固知识。 (上一篇:《多维数组与常见操作》 | 下一篇预告:《输入与输出:Scanner与System类》)
175 11
|
8月前
|
Java
课时14:Java数据类型划分(初见String类)
课时14介绍Java数据类型,重点初见String类。通过三个范例讲解:观察String型变量、&quot;+&quot;操作符的使用问题及转义字符的应用。String不是基本数据类型而是引用类型,但使用方式类似基本类型。课程涵盖字符串连接、数学运算与字符串混合使用时的注意事项以及常用转义字符的用法。
189 9
|
Java 测试技术 开发者
Java零基础-indexOf(String str)详解!
【10月更文挑战第14天】Java零基础教学篇,手把手实践教学!
287 65
|
11月前
|
存储 JavaScript Java
Java 中的 String Pool 简介
本文介绍了 Java 中 String 对象及其存储机制 String Pool 的基本概念,包括字符串引用、构造方法中的内存分配、字符串文字与对象的区别、手工引用、垃圾清理、性能优化,以及 Java 9 中的压缩字符串特性。文章详细解析了 String 对象的初始化、内存使用及优化方法,帮助开发者更好地理解和使用 Java 中的字符串。
172 2
Java 中的 String Pool 简介
|
11月前
|
缓存 安全 Java
java 为什么 String 在 java 中是不可变的?
本文探讨了Java中String为何设计为不可变类型,从字符串池的高效利用、哈希码缓存、支持其他对象的安全使用、增强安全性以及线程安全等方面阐述了不可变性的优势。文中还通过具体代码示例解释了这些优点的实际应用。
243 1
java 为什么 String 在 java 中是不可变的?
下一篇
开通oss服务