如果问你,开发过程中用的最多的类是哪个?你可能回答是HashMap
,一个原因就是HashMap的使用量的确很多,还有就是HashMap的内容在面试中经常被问起。
但是在开发过程中使用最多的类其实并不是HashMap类,而是“默默无闻”的String类。假如现在问你String类是怎么实现的?这个类为什么是不可变类?这个类为什么不能被继承?这些问题你都能回答么。本文就从String源代码出发,来看下String到底是怎么实现的,并详细介绍下String类的API的用法。
String源码结构
首先要说明的是本文的源码是以JDK11为基准,选择JDK11的原因是JDK11是一个LTS版本(长期支持版本),没选择现阶段还在广泛使用的JDK8的原因是想在看源码的过程中学习下JDK的新特性。
还有要说下的就是:大家在看源码时一定要注意JDK的版本,因为不同版本的实现有较大的差异。比如说String的实现在高低版本中就差异比较大。如果你是一个博客主,更加要注明代码的版本了,不然读者可能会很疑惑,为什么和自己之前看的不一样。
好了,下面就言归正传来看下String在JDK11中的实现代码。
public final class String implements Serializable, Comparable<String>, CharSequence {
@Stable
//字节数组,存放String的内容,如果你看的是较低版本的源代码,这个变量可能是char[]类型,这个其实是JDK9开始对String做的一个优化
//具体是做了什么优化我们下面再讲,这边先卖个关子
private final byte[] value;
//也是和String压缩优化有关,指定当前的LATIN1码还是UTF16码
private final byte coder;
//哈希值
private int hash;
//序列化Id
private static final long serialVersionUID = -6849794470754667710L;
//优化压缩开关,默认开启
static final boolean COMPACT_STRINGS = true;
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
public static final Comparator<String> CASE_INSENSITIVE_ORDER = new String.CaseInsensitiveComparator();
static final byte LATIN1 = 0;
static final byte UTF16 = 1;
//... 下面部分代码省略
}
从实现的接口看,String类有如下特点:
- String类被final关键字修饰,因此不能被继承。
- String的成员变量value使用final修饰,因此是不可变的,线程安全;
- String类实现了Serializable接口,可以实现序列化。
- String类实现了Comparable,可以比较大小。
- String类实现了CharSequence接口,String本质是个数组,低版本中是char数组,JDK9以后优化成byte数组,从String的成员变量value就可以看出来。
这边说一个看源代码的小技巧:看一个类的源代码时,我们先看下这个类实现了哪些接口,就可以大概知道这个类的主要作用功能是什么了。
JDK9对String的优化
这边首先要讲下JDK 9
中对String的优化,如果你不了解这块优化点的话,看String的代码时会感到非常疑惑。
背景知识
在Java中,一个字节char占用两个字节的内存空间。在低版本的JDK中,String的内部默认维护的是一个char[]数组,也就是说一个字符串中包含一个字符,这个字符串内部就包含一个相应长度的字符数组。这样就会出现下面这种情况:
String s = "ddd";
String s1 = "自由之路";
上面两个字符串内部的情况实际上是:
char[] value = ['d','d','d'];
char[] value1 = ['自','由','之','路'];
对于字符串s,我们发现其中每个字符其实都是可以用一个字节表示的,而现在使用两个字符的char类型来表示,明显就浪费了一倍的内存空间。
而且根据统计,在实际程序运行中,字符串中包含的字符大多都是可以用一个字节表示的字符,所以优化的空间很大。优化的方式就是在String内部使用byte[]数组来表示字符串,而不是使用char[]数组。当检测到,字符串中的所有字符在Unicode码集中的码值可以使用一个字节表示时,就可以节省一半的空间。
时间换空间的方式
JDK6 中的Compressed Strings
其实在JDK6中就对String类做过类似的优化:在Java 6引入了Compressed Strings,对于one byte per character的字符串使用byte[],对于two bytes per character的字符串继续使用char[]。
使用-XX:+UseCompressedStrings来开启上面的优化。不过由于开启这个特性后会造成一些不可知的异常,这个特性在java7中被废弃了,然后在java8被移除。
JDK9中的Compact String
Java 9 重新采纳字符串压缩这一概念。
和JDK6不同的是:无论何时我们创建一个所有字符都能用一个字节的 LATIN-1 编码来描述的字符串,都将在内部使用字节数组的形式存储,且每个字符都只占用一个字节。另一方面,如果字符串中任一字符需要多于 8 比特位来表示时,该字符串的所有字符都统统使用两个字节的 UTF-16 编码来描述。因此基本上能如果可能,都将使用单字节来表示一个字符。
//占用3个字节
String ss = new String("ddd");
//占用14个字节
String s = "自由之路ddd";
现在的问题是:所有的字符串操作如何执行? 怎样才能区分字符串是由 LATIN-1 还是 UTF-16 来编码?为了处理这些问题,字符串的内部实现进行了一些调整。引入了一个 final
修饰的成员变量 coder
, 由它来保存当前字符串的编码信息。
//所有的字符串都用byte数组存储
private final byte[] value;
//用coder标示字符串中所有的字符是不是都可以用一个字节表示,它的值只有两个LATIN1:1,标示所有字符都可以用一个字节表示,UTF16:标示字符串中部分字符需要两个字节表示。
private final byte coder;
//下面是两个常量
static final byte LATIN1 = 0;
static final byte UTF16 = 1;
现在,大多数的字符串操作都将检查 coder
变量,从而采取特定的实现:
public int indexOf(int ch, int fromIndex) {
return isLatin1()
? StringLatin1.indexOf(value, ch, fromIndex)
: StringUTF16.indexOf(value, ch, fromIndex);
}
private boolean isLatin1() {
return COMPACT_STRINGS && coder == LATIN1;
}
我们再看下String的一个常用方法:
public int length() {
return value.length >> coder;
}
这个方法是要计算字符串的长度,含义也很清楚。根据coder字段判断当前的字符串中一个字符使用几个字节表示,如果是coder等于0,也是LATIN1模式,那么所有字符都是用一个字节表示,直接返回byte[]数组的长度就可以。
如果coder等于1,那么标示字符串中所有字符都是用两个字节表示的,计算字符串的长度需要将byte[]数组除以2。value.length >> coder
就是这个意思。
因为对String做了上面的优化,所以String的很多方法在操作时都需要判断现在的模式是LATIN1还是UTF16模式,具体的方法这边就不一一举例了。但是这些判断对使用String的开发者时无感的。
当然,String的这个优化特性可以关闭,使用下面的启动参数就可以。
+XX:-CompactStrings
String的常用构造方法
//构建空字符串
public String() {
this.value = "".value;
this.coder = "".coder;
}
//根据已有的字符串,创建一个新的字符串
@HotSpotIntrinsicCandidate
public String(String original) {
this.value = original.value;
this.coder = original.coder;
this.hash = original.hash;
}
//根据字符数组,创建字符串,创建的过程中有压缩优化的逻辑,具体见下面的方法
public String(char[] value) {
this((char[])value, 0, value.length, (Void)null);
}
String(char[] value, int off, int len, Void sig) {
if (len == 0) {
this.value = "".value;
this.coder = "".coder;
} else {
if (COMPACT_STRINGS) {
//如果发现这个字符数组可以压缩,就使用LATIN1方式
byte[] val = StringUTF16.compress(value, off, len);
if (val != null) {
this.value = val;
this.coder = 0;
return;
}
}
//不能进行压缩优化,还是使用UTF16的方式
this.coder = 1;
this.value = StringUTF16.toBytes(value, off, len);
}
}
String中还有很多构造方法,但是都会大同小异,大家可以自己看源代码。
String常用方法总结
这边总结下String的常用方法,一些比较简单的方法就不具体讲了。我们挑选一些比较重要的方法,具体讲下他们的使用方法。
- codePointAt(int index):返回下标是index的字符在Unicode码集中的码点值;
- codePoints():返回字符串中每个字符在Unicode码集中的码点值;
- compareToIgnoreCase(String other):忽略大小写比较字符大小;
- concat(String other):字符串拼接函数;
- equalsIgnoreCase(String other):忽略大小写比较字符串;
- format:字符串格式化函数,比较有用;
- getBytes(String charSet):获取字符串在特定编码下的字节数组;
- indexOf(String s):返回字符串s的下标,不存在返回-1;
- intren():作用是检测常量池中是否有当前字符串,有的话就返回常量池中的对像,没有的话就将当前对像放入常量池。
- isBlank():如果字符串为空或只包含空白字符,则返回true,否则返回false,JDK11新加的API;
- length():返回字符长度;
- lines():从字符串返回按行分割的Stream,行分割福包括:n ,r 和rn,stream包含了按顺序分割的行,行分隔符被移除了,这个方法会类似split(),但性能更好;这个也是JDK11新加的API
- matchs(String regex):和某个正则是否匹配;
- regionMatches(int firstStart, String other, int otherStart, int len):当某个字符串调用该方法时,表示从当前字符串的firstStart位置开始,取一个长度为len的子串;然后从另一个字符串other的otherStart位置开始也取一个长度为len的子串,然后比较这两个子串是否相同,如果这两个子串相同则返回true,否则返回false。
- repeat():返回一个字符串,其内容是字符串重复n次后的结果,JDK11新加入的函数;
- String[] split(String regex, int limit):分割字符串,注意limit参数的使用,下面会详细讲;
- startsWith(String prefix, int toffset):判断字符串是否以prefix打头;
- replace(char oldChar, char newChar):使用newChar替换所有的oldChar,不是基于正则表达式的;
- replace(CharSequence target, CharSequence replacement):替换所有,基于正则表达式的;
- replaceFirst(String regex, String replacement):替换regex匹配的第一个字符串,基于正则表达式;
- replaceAll(String regex, String replacement):替换regex匹配的所有字符串,基于正则表达式;
- strip() :去除字符串前后的“全角和半角”空白字符,这个函数在JDK中11才引入,注意和trim的区别,关于全角和半角的区别,可以参考这篇文章,还提供了stripLeading()和stripTrailing(),可以分别去掉头部或尾部的空格;
- subString(int fromIndex):从指定位置开始截取到字符串结尾部分的子串;
- subString(int fromIndex,int endIndex):截取字符串指定下标的子串;
- toCharArray():转换成字符数组;
- toUpperCase(Locale locale) :小写转换成大写;
- toLowerCase(Locale locale):大写转换成小写;
- trim():去除字符串前后的空白字符(空格、tab键、换行符等,具体的话是去除ascll码小于32的字符),注意trim和strip的区别;
valueof
系列方法:将其他类型的数据转换成String类型,比如将bool、int和long等类型转换成String类型。
concat字符串拼接函数
concat函数是字符串拼接函数,介绍这个函数并不是因为这个函数比较重要或者实现比较复杂。而是因为通过这个函数的源代码我们可以看出很多String的特性。
public String concat(String str) {
//如果被拼接的字符串的长度是0,直接返回自己
int olen = str.length();
if (olen == 0) {
return this;
} else {
byte[] buf;
//如果当前字符串和被拼接的字符串的编码模式相同,都是LATIN1或者都是UTF16
if (this.coder() == str.coder()) {
byte[] val = this.value;
buf = str.value;
//计算出新字符串所需字节的长度
int len = val.length + buf.length;
byte[] buf = Arrays.copyOf(val, len);
//使用系统函数拷贝
System.arraycopy(buf, 0, buf, val.length, buf.length);
//根据新的字节数组生成一个新的字符串
return new String(buf, this.coder);
} else {
//当前字符串和被拼接的字符串的编码模式不同,那么必须使用UTF16的编码模式
int len = this.length();
buf = StringUTF16.newBytesFor(len + olen);
this.getBytes(buf, 0, (byte)1);
str.getBytes(buf, len, (byte)1);
return new String(buf, (byte)1);
}
}
}
format函数
String的format方法是一个很有用的方法,可以用来对字符串、数字、日期和时间等进行格式化。
//对整数格式化,4位显示,不足4位补0
//超过4位,还是原样显示
int num = 999;
String str = String.format("%04d", num);
System.out.println(str);
//对日期进行格式化
String format = String.format("%tF", new Date());
System.out.println(format);
format方法还有很多用法,大家可以自己查询使用。
regionMatches
该方法的定义如下:
regionMatches(int firstStart, String other, int otherStart, int len)
当某个字符串调用该方法时,表示从当前字符串的firstStart位置开始,取一个长度为len的子串;然后从另一个字符串other的otherStart位置开始也取一个长度为len的子串,然后比较这两个子串是否相同,如果这两个子串相同则返回true,否则返回false。
该方法还有另一种重载:
str.regionMatches(boolean ignoreCase, int firstStart, String other, int otherStart, int len)
可以看到只是多了一个boolean类型的参数,用来确定比较时是否忽略大小写,当ignoreCase为true表示忽略大小写。
split函数
String的split函数我们平时也经常使用,但是估计很多人都没有注意这个函数的第二个参数:limit
public String[] split(String regex, int limit)
首先,split方法的作用是根据给定的regex去分割字符串,将分割完成的字符数组返回。其中limit参数的作用是:
-
当limit>0时,limit代表最后的数组长度,同时一共会分割limit-1次,最后没有切割完成的直接放在一起;
-
当limit=0时(默认值),会尽量多去分割,并且如果分割完的字符数组末尾是空字符串,会去除这个空字符串;
-
当limit<0时,会尽量多去分割,但不会去掉末尾的空字符串。
下面举个列子:
String s1 = "博客园|CSDN||";
String[] split1 = s1.split("\|", 2);
System.out.println("split1 length:" + split1.length);
System.out.println("split1 content:" + Arrays.toString(split1));
String[] split2 = s1.split("\|", 0);
System.out.println("split2 length:" + split2.length);
System.out.println("split2 content:" + Arrays.toString(split2));
String[] split3 = s1.split("\|", -1);
System.out.println("split3 length:" + split3.length);
System.out.println("split3 content:" + Arrays.toString(split3));
System.out.println("---换一个复杂点的字符串---");
s1 = "|博客园||CSDN|自由之路ddd|";
split1 = s1.split("\|", 2);
System.out.println("split1 length:" + split1.length);
System.out.println("split1 content:" + Arrays.toString(split1));
split2 = s1.split("\|", 0);
System.out.println("split2 length:" + split2.length);
System.out.println("split2 content:" + Arrays.toString(split2));
split3 = s1.split("\|", -1);
System.out.println("split3 length:" + split3.length);
System.out.println("split3 content:" + Arrays.toString(split3));
下面是输出结果,对照着这个结果大家就应该能明白split方法的使用了
split1 length:2
split1 content:[博客园, CSDN|自由之路ddd|]
split2 length:3
split2 content:[博客园, CSDN, 自由之路ddd]
split3 length:4
split3 content:[博客园, CSDN, 自由之路ddd, ]
---换一个复杂点的字符串---
split1 length:2
split1 content:[, 博客园||CSDN|自由之路ddd|]
split2 length:5
split2 content:[, 博客园, , CSDN, 自由之路ddd]
split3 length:6
split3 content:[, 博客园, , CSDN, 自由之路ddd, ]
再举个JDK中的列子:
The input "boo:and:foo", for example, yields the following results with these parameters:
Regex | Limit | Result |
---|---|---|
: | 2 | { "boo", "and:foo" } |
: | 5 | { "boo", "and", "foo" } |
: | -2 | { "boo", "and", "foo" } |
o | 5 | { "b", "", ":and:f", "", "" } |
o | -2 | { "b", "", ":and:f", "", "" } |
o | 0 | { "b", "", ":and:f" } |
总结
- String类被final关键字修饰,因此不能被继承;
- String的成员变量value使用final修饰,因此是不可变的,线程安全;
- String中的方法对字符串的操作都会生成一个新的String对象,如果你需要一个可修改的字符串,应该使用 StringBuffer 或者 StringBuilder。否则会有大量时间浪费在垃圾回收上,因为每次试图修改都有新的string对象被创建出来;
- JDK9开始对String进行了优化,内部彻底使用byte[]数组来代替char数组。
参考
- Java 9 新特性 - Compact Strings
- 聊聊Java 9的Compact Strings
- Java11新增的String方法
- split第二个参数limit的用法
- String之regionMatches方法
- String的格式化方法使用
公众号推荐
欢迎大家关注我的微信公众号「程序员自由之路」