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做了修饰);

相关文章
|
2月前
|
Java 索引
java基础(13)String类
本文介绍了Java中String类的多种操作方法,包括字符串拼接、获取长度、去除空格、替换、截取、分割、比较和查找字符等。
36 0
java基础(13)String类
|
14天前
|
Java 测试技术 开发者
Java零基础-indexOf(String str)详解!
【10月更文挑战第14天】Java零基础教学篇,手把手实践教学!
102 65
|
3月前
|
Kubernetes jenkins 持续交付
从代码到k8s部署应有尽有系列-java源码之String详解
本文详细介绍了一个基于 `gitlab + jenkins + harbor + k8s` 的自动化部署环境搭建流程。其中,`gitlab` 用于代码托管和 CI,`jenkins` 负责 CD 发布,`harbor` 作为镜像仓库,而 `k8s` 则用于运行服务。文章具体介绍了每项工具的部署步骤,并提供了详细的配置信息和示例代码。此外,还特别指出中间件(如 MySQL、Redis 等)应部署在 K8s 之外,以确保服务稳定性和独立性。通过本文,读者可以学习如何在本地环境中搭建一套完整的自动化部署系统。
69 0
|
26天前
|
存储 缓存 Java
java基础:IO流 理论与代码示例(详解、idea设置统一utf-8编码问题)
这篇文章详细介绍了Java中的IO流,包括字符与字节的概念、编码格式、File类的使用、IO流的分类和原理,以及通过代码示例展示了各种流的应用,如节点流、处理流、缓存流、转换流、对象流和随机访问文件流。同时,还探讨了IDEA中设置项目编码格式的方法,以及如何处理序列化和反序列化问题。
58 1
java基础:IO流 理论与代码示例(详解、idea设置统一utf-8编码问题)
|
14天前
|
Java 测试技术 开发者
Java零基础-indexOf(String str)详解!
【10月更文挑战第13天】Java零基础教学篇,手把手实践教学!
35 1
|
18天前
|
安全 Java 测试技术
Java零基础-StringBuffer 类详解
【10月更文挑战第9天】Java零基础教学篇,手把手实践教学!
19 2
|
1月前
|
IDE Java 开发工具
Java“未封闭的 String 表达式”怎么解决
要解决Java中的“未封闭的 String 表示”问题,需检查并修正字符串字面量,确保每个字符串被正确地用双引号括起来。若字符串跨越多行,可使用字符串连接操作符(+)或引入文本块(JDK 13 及以上版本)。这能帮助避免语法错误,并使代码更整洁易读。
|
29天前
|
存储 安全 Java
【一步一步了解Java系列】:认识String类
【一步一步了解Java系列】:认识String类
24 2
|
2月前
|
安全 Java API
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
String常量池、String、StringBuffer、Stringbuilder有什么区别、List与Set的区别、ArrayList和LinkedList的区别、HashMap底层原理、ConcurrentHashMap、HashMap和Hashtable的区别、泛型擦除、ABA问题、IO多路复用、BIO、NIO、O、异常处理机制、反射
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
|
2月前
|
存储 安全 Java
Java——String类详解
String 是 Java 中的一个类,用于表示字符串,属于引用数据类型。字符串可以通过多种方式定义,如直接赋值、创建对象、传入 char 或 byte 类型数组。直接赋值会将字符串存储在串池中,复用相同的字符串以节省内存。String 类提供了丰富的方法,如比较(equals() 和 compareTo())、查找(charAt() 和 indexOf())、转换(valueOf() 和 format())、拆分(split())和截取(substring())。此外,还介绍了 StringBuilder 和 StringJoiner 类,前者用于高效拼接字符串,后者用于按指定格式拼接字符串
50 1
Java——String类详解