Java源码系列之String

简介: Java String 类源码解析

前言

字符串在我们的工作场景中应用广泛,不同于基本数据类型,Java中的字符串属于对象,Java中提供了 String 类来创建字符串,并提供了一系列的方法来对字符串进行替换、拆分和合并等操作。

总体结构

首先,我们来看一下String类的总体结构,如下图所示:

String类图结构.jpg

  • String类实现了Serializable接口,这说明String的对象能够被序列化
  • String类实现了Comparable接口,这使得我们可以重写compareTo方法来自定义String对象之间的比较操作
  • String类实现了CharSequence接口,并重写了接口中的length()、charAt()等方法

注释

我们首先通过注释来了解一下String,在源码学习中,注释往往能给我们提供很多有用的信息,因此在阅读源码之前,从注释中获取一些信息是非常重要的。从String类的注释中我们可以获取以下信息:

  • String类表示字符串。 所有Java程序中的字符串文字例如“ abc”都是作为此类的实例实现。
  • String类是不可变类,它们的值在创建之后就不能改变了。而StringBuffer、StringBuilder提供了字符串的可变类实现。
  • 因为String是不可变类,因此它们的值是可以被共享的,比如:String str = "abc";等价于char data[] = {'a', 'b', 'c'};String str = new String(data);
  • Java语言为String的连接操作提供了特殊的支持(通过“+”号来进行字符串的连接),而StringBuilder、StringBuffer内部通过append方法实现了字符串的连接。

不变性

上文提到了不可变类,而HashMap 的 key 通常建议使用不可变类,比如说 String 这种不可变类。这里说的不可变指的是一旦一个类的对象被创建出来,在其整个生命周期中,它的成员变量就不能被修改,如果被修改,将会生成新的对象。从源码中我们可以得知String是如何实现它的不可变性的:


publicfinalclassStringimplementsjava.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */privatefinalcharvalue[];
}
  1. String 被 final 修饰,说明 String 类绝不可能被继承了,也就是说任何对 String 的操作方法,都不会被继承覆写;
  2. String 中保存数据的是一个 char 类型的数组 value。 value 也是被 final 修饰的,也就是说 value 一旦被赋值,内存地址是绝对无法修改的,而且 value 的访问权限是 private 的,外部绝对访问不到,String 也没有开放出可以对 value 进行赋值的方法,所以说 value 一旦产生,内存地址就根本无法被修改。

以上两点就是 String 不变性的原因,充分利用了 final 关键字的特性,所以在自定义类时,如果你也希望类是不可变的,可以模仿 String 的这两点操作。

字符串乱码

在二进制转化操作时,并没有强制规定文件编码,而不同的环境默认的文件编码不一致就会导致字符串乱码。比如如下代码中创建的字符串s2会产生乱码:


Stringstr="nihao 你好";
byte[] bytes=str.getBytes("ISO-8859-1");
Strings2=newString(bytes);
System.out.println(s2);

在打印的结果中可以看到字符串产生了乱码:


nihao ??

这时,即使使用String s2 = new String(bytes,"ISO-8859-1");来统一字符集也还是会产生乱码,这是由于 ISO-8859-1 这种编码本身就对中文的支持有限,从而导致中文会产生乱码。而当我们统一使用 UTF-8 这种编码的时候就不会有乱码产生了。

首字母大小写

需要首字母小写的场景:

  • 通过 applicationContext.getBean(className); 这种方式得到 SpringBean,这时 className 必须是要满足首字母小写的
  • 在反射场景下面,我们也经常要使类属性的首字母小写,这时候我们一般都会这么做:
name.substring(0, 1).toLowerCase() +name.substring(1);

上面的 substring 方法主要用于截取字符串连续的一部分,substring 有两个方法:

  1. public String substring(int beginIndex, int endIndex) beginIndex:开始位置,endIndex:结束位置;
  2. public String substring(int beginIndex)beginIndex:开始位置,结束位置为文本末尾。

substring 方法的底层使用的是字符数组范围截取的方法 :Arrays.copyOfRange(字符数组, 开始位置, 结束位置); 从字符数组中进行一段范围的拷贝。

相反的,如果要修改成首字母大写,只需要修改成 name.substring(0, 1).toUpperCase() + name.substring(1) 即可。

相等判断

判断相等有两种办法,equals 和 equalsIgnoreCase。后者判断相等时,会忽略大小写。

String是通过其底层的结构来判断是否相等的,即挨个比较char数组value的每一个字符是否相等。


publicbooleanequals(ObjectanObject) {
// 判断内存地址是否相同if (this==anObject) {
returntrue;
    }
// 待比较的对象是否是 String,如果不是 String,直接返回不相等if (anObjectinstanceofString) {
StringanotherString= (String)anObject;
intn=value.length;
// 两个字符串的长度是否相等,不等则直接返回不相等if (n==anotherString.value.length) {
charv1[] =value;
charv2[] =anotherString.value;
inti=0;
// 依次比较每个字符是否相等,若有一个不等,直接返回不相等while (n--!=0) {
if (v1[i] !=v2[i])
returnfalse;
i++;
            }
returntrue;
        }
    }
returnfalse;
}

替换、删除

替换在很多场景中会使用到,有 replace 替换所有字符、replaceAll 批量替换字符串、replaceFirst 替换遇到的第一个字符串三种场景。

其中在使用 replace 时需要注意,replace 有两个方法,一个入参是 char,一个入参是 String,前者表示替换所有字符,如:name.replace('a','b'),后者表示替换所有字符串,如:name.replace("a","b"),两者就是单引号和多引号的区别。

需要注意的是, replace 并不只是替换一个,是替换所有匹配到的字符或字符串。

replace(char oldChar, char newChar)方法为例,从源码中可以看出来是通过逐个查找到需要替换的字符进行替换来实现的:


publicStringreplace(charoldChar, charnewChar) {
if (oldChar!=newChar) {
intlen=value.length;
inti=-1;
/*** 在一个方法中需要大量引用实例域变量的时候,* 使用方法中的局部变量代替引用可以减少getfield操作的次数,提高性能*/char[] val=value; /* avoid getfield opcode */// 查找到需要替换的字符oldChar在value数组中第一次出现的位置while (++i<len) {
if (val[i] ==oldChar) {
break;
            }
        }
if (i<len) {
charbuf[] =newchar[len];
for (intj=0; j<i; j++) {
buf[j] =val[j];
            }
// 找到所有需要替换的字符,逐一进行替换while (i<len) {
charc=val[i];
buf[i] = (c==oldChar) ?newChar : c;
i++;
            }
// 返回一个新字符串returnnewString(buf, true);
        }
    }
// 替换前后字符相等,直接返回returnthis;
}

当然我们想要删除某些字符,也可以使用 replace 方法,把想删除的字符替换成 "" 即可。

拆分和合并

拆分我们使用 split 方法,该方法有两个入参数。第一个参数是我们拆分的标准字符,第二个参数是一个 int 值,叫 limit,来限制我们需要拆分成几个元素。如果 limit 大于0并且比实际能拆分的个数小,按照 limit 的个数进行拆分。


Strings="boo:and:foo";
s.split(":"); // 结果:["boo","and","foo"]s.split(":",2); // 结果:["boo","and:foo"]s.split(":",5); // 结果:["boo","and","foo"]s.split(":",-2); // 结果:["boo","and","foo"]s.split("o"); // 结果:["b","",":and:f"]s.split("o",2); // 结果:["b","o:and:foo"]

limit 对拆分的结果,是具有限制作用的,还有就是拆分结果里面不会出现被拆分的字段。

以源码中可以看出来,public String[] split(String regex, int limit)方法针对不同的分隔符提供两种路径来进行字符串的分割:


charch=0;
/** 当regex是以下两种情况时的快速分割路径:* (1)一个字符的字符串并且这个字符不是以下正则表达式的元字符".$|()[{^?*+\\"之一* (2)两个字符的字符串并且第一个字符是反斜杠,第二个字符不是ASCII数字或ASCII字母。*/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)) {
// 用于记录每个拆分的子串起始位置的索引intoff=0;
// 用于记录每个分隔符在字符串中的索引值intnext=0;
booleanlimited=limit>0;
ArrayList<String>list=newArrayList<>();
// 从字符串开头开始将每个分隔符之前的子串保存在一个list中while ((next=indexOf(ch, off)) !=-1) {
if (!limited||list.size() <limit-1) {
list.add(substring(off, next));
off=next+1;
        } else {    // 最后一个子串// 当list的大小等于limit - 1,直接将剩下的字符串加入到list中list.add(substring(off, value.length));
off=value.length;
break;
        }
    }
// 如果字符串中没有匹配的分隔符,直接返回原字符串if (off==0)
returnnewString[]{this};
// 将剩下的子串加入到list中if (!limited||list.size() <limit)
list.add(substring(off, value.length));
// 构建结果intresultSize=list.size();
if (limit==0) {
// 忽略掉最后的空串while (resultSize>0&&list.get(resultSize-1).length() ==0) {
resultSize--;
        }
    }
// 将list中保存的字符串转换成字符串数组返回String[] result=newString[resultSize];
returnlist.subList(0, resultSize).toArray(result);
}
// 第二条路径,以正则匹配的方式分割字符串returnPattern.compile(regex).split(this, limit);

如果字符串里面有一些空值,空值是拆分不掉的,仍然成为结果数组的一员,如果我们想删除空值,只能自己拿到结果后再做操作,但 Guava(Google 开源的技术工具) 提供了一些可靠的工具类(如Splitter),可以帮助我们快速去掉空值。


Stringb=",a, ,  b  c ,";
List<String>list=Splitter.on(',')
    .trimResults()// 去掉空格    .omitEmptyStrings()// 去掉空值    .splitToList(b);
list.stream().forEach(System.out::println);

合并我们使用 join 方法,此方法是静态的,我们可以直接使用。方法有两个入参,参数一是合并的分隔符,参数二是合并的数据源,数据源支持数组和 List。从源码中可以看出,字符串的合并操作实际上是借助StringBuilder的append方法来完成的。


StringJoinerjoiner=newStringJoiner(delimiter);
for (CharSequencecs: elements) {
// 通过add方法来合并字符串joiner.add(cs);
}
returnjoiner.toString();


publicStringJoineradd(CharSequencenewElement) {
// 通过StringBuilder的append方法来合并字符串prepareBuilder().append(newElement);
returnthis;
}


// 初始化StringJoiner中的StringBuilder对象privateStringBuilderprepareBuilder() {
if (value!=null) {
value.append(delimiter);
    } else {
value=newStringBuilder().append(prefix);
    }
returnvalue;
}

join方法在使用的时候,有两个不太方便的地方:

  1. 不支持依次 join 多个字符串,比如我们想依次 join 字符串 s 和 s1,如果你这么写的话 String.join(",",s).join(",",s1) 最后得到的是 s1 的值,第一次 join 的值被第二次 join 覆盖了;
  2. 如果 join 的是一个 List,无法自动过滤掉 null 值。

而 Guava 正好提供了 Joiner API,用于解决上述问题:


// 依次 join 多个字符串Joinerjoiner=Joiner.on(",").skipNulls();
Stringresult=joiner.join("hello",null,"china");
log.info("依次 join 多个字符串: "+result);
List<String>list=Lists.newArrayList(newString[]{"hello","china",null});
log.info("自动删除 list 中空值: "+joiner.join(list));

注:以上分析皆基于JDK 1.8版本来进行。

目录
相关文章
|
1月前
|
Java
【Java基础面试三十一】、String a = “abc“; ,说一下这个过程会创建什么,放在哪里?
这篇文章解释了在Java中声明`String a = "abc";`时,JVM会检查常量池中是否存在"abc"字符串,若不存在则存入常量池,然后引用常量池中的"abc"给变量a。
|
1月前
|
Java
【Java基础面试三十二】、new String(“abc“) 是去了哪里,仅仅是在堆里面吗?
这篇文章解释了Java中使用`new String("abc")`时,JVM会将字符串直接量"abc"存入常量池,并在堆内存中创建一个新的String对象,该对象会指向常量池中的字符串直接量。
|
20天前
|
Kubernetes jenkins 持续交付
从代码到k8s部署应有尽有系列-java源码之String详解
本文详细介绍了一个基于 `gitlab + jenkins + harbor + k8s` 的自动化部署环境搭建流程。其中,`gitlab` 用于代码托管和 CI,`jenkins` 负责 CD 发布,`harbor` 作为镜像仓库,而 `k8s` 则用于运行服务。文章具体介绍了每项工具的部署步骤,并提供了详细的配置信息和示例代码。此外,还特别指出中间件(如 MySQL、Redis 等)应部署在 K8s 之外,以确保服务稳定性和独立性。通过本文,读者可以学习如何在本地环境中搭建一套完整的自动化部署系统。
51 0
|
5天前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
本文是“Java学习路线”专栏的导航文章,目标是为Java初学者和初中高级工程师提供一套完整的Java学习路线。
|
6天前
|
安全 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版)
|
5天前
|
存储 安全 Java
Java——String类详解
String 是 Java 中的一个类,用于表示字符串,属于引用数据类型。字符串可以通过多种方式定义,如直接赋值、创建对象、传入 char 或 byte 类型数组。直接赋值会将字符串存储在串池中,复用相同的字符串以节省内存。String 类提供了丰富的方法,如比较(equals() 和 compareTo())、查找(charAt() 和 indexOf())、转换(valueOf() 和 format())、拆分(split())和截取(substring())。此外,还介绍了 StringBuilder 和 StringJoiner 类,前者用于高效拼接字符串,后者用于按指定格式拼接字符串
11 1
Java——String类详解
|
1天前
|
安全 Java
Java StringBuffer 和 StringBuilder 类详解
在 Java 中,`StringBuffer` 和 `StringBuilder` 用于操作可变字符串,支持拼接、插入、删除等功能。两者的主要区别在于线程安全性和性能:`StringBuffer` 线程安全但较慢,适用于多线程环境;`StringBuilder` 非线程安全但更快,适合单线程环境。选择合适的类取决于具体的应用场景和性能需求。通常,在不需要线程安全的情况下,推荐使用 `StringBuilder` 以获得更好的性能。
|
1天前
|
Java 索引
Java String 类详解
Java 中的 `String` 类用于表示不可变的字符序列,是 Java 标准库 `java.lang` 包的一部分。字符串对象一旦创建,其内容不可更改,修改会生成新对象。
|
1月前
|
Java
【Java基础面试二十六】、说一说String和StringBuffer有什么区别
这篇文章区分了Java中的String和StringBuffer类:String是不可变类,一旦创建字符序列就不能改变;而StringBuffer代表可变的字符串,可以通过其方法修改字符序列,最终可以通过`toString()`方法转换为String对象。
【Java基础面试二十六】、说一说String和StringBuffer有什么区别
|
30天前
|
Java
Java系列之 For input string: ““
这篇文章讨论了Java中因尝试将空字符串转换为其它数据类型(如int)时出现的`For input string: ""`错误,并提供了通过非空检查来避免此错误的解决方法。