前言
String
类可谓是我们最常用的一个类了,但是对于这个类我们是否是真的了解呢?之前我们早就已经说过了String
对象是一个不可变的对象,在说到基本数据类型的包装类的时候,我们也提高了String
的常量池。虽然关于String
,我们已经讲了不少,但是都是太过于零散,这里我准备看看String
类型的源码是如何的。
String
漫谈
String
的声明
首先看一下String
的声明是如何的。
public final class String
extends Object
implements Serializable, Comparable<String>, CharSequence
String
类是final的,也就是我们是无法去继承String
类的。
Serializable
是一个标记接口,代表String
对象是可以序列化的。
Comparable<String>
是比较器。String
类实现了自然排序,所以很多排序的操作时,我们并不需要指定一个比较器给String
。(String
的排序是按照字典顺序的,源码太复杂了,这里就不贴出来了)
CharSequence
其实就是字符串的意思,其中只是提供了可以作为字符的基本方法。如
char charAt(int index)
,我们或许String
对象的第几位的字符的时候不可以使用[]
。比如说下面的代码示例。String a = "abc"; char b = a[1]; // Worng char b = a.charAt(1); // Right
int length()
。既然是字符串,那肯定是有长度的。
CharSequence subSequence(int start, int end)
。这个方法倒是有点儿想String
类中的subString
方法有点儿类似了。
String toString()
。这个方法其实每一个类都是有的,但是CharSequence
接口将这个方法设置为抽象方法,这也是理所应当的,作为一个字符串toString
的时候肯定是要输出点内容的。因为toString
方法默认输出的是getClass().getName() + "@" + Integer.toHexString(hashCode())
上面就是对String
类的声明做了一个简单的了解。现在就让我们看看String
的源码,看看为啥这个东西是不可变对象。要看一个类首先是从这个类的构造器开始看起。不过String
的构造器实在是太多了,我们就从官方文档中的说明开始看起。
String
的源码
String str = "abc";
相当于
char data[] = {'a', 'b', 'c'};
String str = new String(data);
文档中说这是一样的,但是依据我们之前所学的知识,这个是完全不一样的。上面的str
在Java的常量池中,而下面的str
是在堆中的。
不过我们要来看一看String
类是如何通过char[]
创建String
对象的。
public String(char value[]) {
this(value, 0, value.length, null);
}
String(char[] value, int off, int len, Void sig) {
if (len == 0) {
this.value = "".value;
this.coder = "".coder;
return;
}
if (COMPACT_STRINGS) {
byte[] val = StringUTF16.compress(value, off, len);
if (val != null) {
this.value = val;
this.coder = LATIN1;
return;
}
}
this.coder = UTF16;
this.value = StringUTF16.toBytes(value, off, len);
}
上面的第四个参数应该是一个返回值没有被用到的,但是又必须在函数的参数列表中存在的一个东西。因为我稍微看了一下源码,第三个参数基本上都是null
。初始化中使用到了两个字段value
和coder
。这个coder
明显是一个编码的措施,那么这个value
应该就是String
内部用来存储数据的东西。
@Stable
private final byte[] value;
private final byte coder; // LATIN1 or UTF16
其实我们可以取消对coder
的关注了。只用看value
。他的声明是private final byte[]
。
- 第一点,
String
的内部不是使用char[]
来储存的。因为之前观察C++的string
的源码,其实就是维护了一个char*
。一个字符串使用byte[]
作为底层,这真的很意外。我使用的jdk版本为jdk12
,或许是我的jdk版本太高了,因为我之前在网上查阅资料的使用我看源码还是private final char[] value
呢。为什么会变成byte[]
呢?根据我查阅的资料,这个改变是从jdk9
开始的,目的是为了节省字符串的内存占用从而优化性能。 - 第二点,
value
被声明为是final
的,而且还加上了注解@Stable
,这说明了这个数组是稳定的不可变的,这也就解释了为什么String
是一个不可变的对象。
这样我们基本上也就了解了String
对象是如何存储的,以及为什么是一个不可变的对象。String
类的原码有几千行,这并不是我们都需要看的,这里我们其实源码看的已经够了。
但是对于下面的代码,我确是有一个疑问。
String str = new String("abc");
这个到底是调用什么构造器呢?按道理来说应该是String(String str)
@HotSpotIntrinsicCandidate
public String(String original) {
this.value = original.value;
this.coder = original.coder;
this.hash = original.hash;
}
那么仅仅是一个“abc”
就可以作为是一个String
吗?那么这个abc
又是调用的什么构造器呢?这看起来是一个无限循环的问题。不过需要注意的是单独的“abc”
还真的是可以作为String
来使用的。
System.out.println("hello world".toUpperCase());
就比如说这行代码是没有任何问题的。其实上面是有String
对象的产生的,只不过是编译器给你生成的,就像我们之前说的内部类那一块一样。
String str = "hello world";
System.out.println(str.toUpperCase());
其实是上面这样子的,hello world
这个对象是位于常量池中的。后面的str.toUpperCase()
产生了一个新的String
对象,不过这个String
对象是位于堆中的。也就是说,如果我们使用如下的代码的时候。
String str = new String("abc");
String xx = "abc";
String str = new String(xx);
会生成两个String
对象,一个位于常量池中,一个位于堆中。至于常量池中的String
是如何创建的,这就不是我们可以通过阅读源码就可以解决的问题了。
String
相加
众所周知,C++是一门支持云算法重载的牛逼的语言,但是Java中是不支持运算符的重载的。不过有一个东西是非常的例外的,就是Strnig
中的重载了+
与+=
。至于如何实现的这个肯定是没办法从源码处得知的了。
String str = "a" + "b";
按道理来说,我们知道String
是一个不可变的对象。所以说应该是创建了两个String
然后连接起来。但是真的如此吗?其实不是的,JVM
可是非常聪明的,他会只创建一个ab
字符串,然后直接把地址赋给str。
String a = "a";
String str = "1" + a + "2";
看一下上面的代码,那么以我们聪明的JVM
来说,也应该是直接创建1a2
。不过,可能JVM
要让你失望了。普遍的说法是会生成字符串1a
,1a2
。如果+
的时候,存在已经定义过的变量,那么JVM
就不会直接搞天神下凡似的直接创建目标字符串。而是一个一个创建。但是我上网查阅了资料,发现懂汇编的大佬使用javap -c
反编译字节码文件的时候,发现实际上JVM
是创建一个StringBuilder
,然后使用append
方法来连接字符串。
String str = "";
for(int i = 0; i < num; i++) {
str += "a";
}
如果我们使用的是这样子的话,那就很恐怖了,每一次循环都会生成一个StringBuilder
对象来连接字符串,效率是非常的低下。此时我们应该使用如下的方式。
StringBuilder sb = new StringBuilder();
for (int i = 0; i < num; i++) {
sb.append("a")
}
这样的话,就只会产生一个StringBuilder
对象了。
上面是看了其他的对字节码的反编译才知道其实JVM
连接字符串的使用都是使用创建StringBuilder
对象然后使用append
方法的形式进行连接的。其实String
类中也有一个方法用于字符串的连接,不过这个方法是不涉及到StringBuilder
的,其实我一开始以为+
就是使用concat
方法的,没想到是使用StringBuilder
。
public String concat(String str) {
if (str.isEmpty()) {
return this;
}
if (coder() == str.coder()) {
byte[] val = this.value;
byte[] oval = str.value;
int len = val.length + oval.length;
byte[] buf = Arrays.copyOf(val, len);
System.arraycopy(oval, 0, buf, val.length, oval.length);
return new String(buf, coder);
}
int len = length();
int olen = str.length();
byte[] buf = StringUTF16.newBytesFor(len + olen);
getBytes(buf, 0, UTF16);
str.getBytes(buf, len, UTF16);
return new String(buf, UTF16);
}
StringBuilder
与StringBuffer
前面谈及String
相加的时候,提到了StringBuilder
。其实通过名字也可以知道,StringBuilder
使用构建String
的,其底层的存储字符串的数组应该是可变的。
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, Comparable<StringBuilder>, CharSequence
这个声明是jdk11
中的声明,多出了一个继承的类???jdk8
中是直接继承Object
的。看来是我太落后了,不过也没关系,我们直接看一下AbstractStringBuilder
。
abstract class AbstractStringBuilder implements Appendable, CharSequence
其中用来储存数据的数组的声明为
byte[] value;
不是final
的,说明其是可变的。我们使用这个类主要就是使用append
方法。那么我们就来简单的看一下append
方法。StringBuilder
中的append
方法,其实是使用的AbstractStringBuilder
中的append
方法。这个方法是对Appendable
接口的一个实现。
@Override
public AbstractStringBuilder append(char c) {
ensureCapacityInternal(count + 1);
if (isLatin1() && StringLatin1.canEncode(c)) {
value[count++] = (byte)c;
} else {
if (isLatin1()) {
inflate();
}
StringUTF16.putCharSB(value, count++, c);
}
return this;
}
这里是添加一个字符,看第三行,是确保value
数组中是否可以放下这一个字符。
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
int oldCapacity = value.length >> coder;
if (minimumCapacity - oldCapacity > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity) << coder);
}
}
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = value.length >> coder;
int newCapacity = (oldCapacity << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
int SAFE_BOUND = MAX_ARRAY_SIZE >> coder;
return (newCapacity <= 0 || SAFE_BOUND - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
可见,如果容量够那就直接放入那个字符。如果是没有足够的容量的时候,value
数组会进行扩容。扩容的规则就在上面的newCapacity
函数中,一般是二倍加上二,如果还不够大的话就变成minCapacity
(也就是加上之后的长度)。但是数组的长度是有限制的,最大也不能超过那个值。
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
不过,这个值,,,也是非常大了!!
再来看一下StringBuilder
的构造函数。
@HotSpotIntrinsicCandidate
public StringBuilder() {
super(16);
}
@HotSpotIntrinsicCandidate
public StringBuilder(int capacity) {
super(capacity);
}
@HotSpotIntrinsicCandidate
public StringBuilder(String str) {
super(str.length() + 16);
append(str);
}
可见,如果无参的话,默认的存储数组的大小是16
。如果给定了一个String
作为参数,那么默认的存储数组的大小就是str.length()+16
。看来jdk
的编写者是非常喜欢16
这个数字的。
上面对StringBuilder
的讲解基本上就结束了,至于StringBuffer
,我感觉是不需要讲的。其实这个StringBuffer
是和StringBuilder
是一样的,不过StringBuilder
是线程不安全的,但是StringBuffer
是线程安全的。StringBuffer
很多的方法都加上了synchronized
用来同步。这个就有点像ArrayList
与Vector
和HashMap
与Hashtable
。都是一个是线程不安全,一个是线程安全。(其实不一样的地方还是非常多的!)
总结
上面就通过源码简单的学习了一下Java当中的String
,StringBuilder
以及StringBuffer
的使用。不过基本上还是蛮简单的看源码的,没有很复杂的地方。还有就是关于String
对象的常量池机制和什么==
什么东西的,之前都是讲过的,这里就不再提及了。
近期评论