
前言
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对象的常量池机制和什么==什么东西的,之前都是讲过的,这里就不再提及了。




近期评论