3488 2017-09-15 2020-06-25
前言:提起Java容器,很多人首先想到是List、Set、Map,这是对的。但其实还有一个更低一层次的容器类,即String类(char的容器,数组就先算了吧),尽管String并没有出现在上一节的类图中。
一、类结构
1、概述
类结构声明如下:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
private int hash;
// 其它略
}
从声明中我们可以看出,String类实现了代表可序列化的Serializable接口、可比较的Comparable接口、和字符序列CharSequence接口,这些接口的实现没有什么特别的,都是常规性的。值得一提的是,String类被设置为final,这就有点含义了。
2、为什么是final
final意味着不可更改,不能被继承。为什么要怎么设计?首先要理解String是不可变的,这样可以带来很多好处,如:
- 效率性:String类是一个对象类型,是对char数组的封装,内部维护的char数组不应该随意被更改(那么hashCode也不用多次计算),且在常量池能被多次引用(只读不改) 。示例如下:
String a = "123";
String b = a;
b = "456";
// 结果a="123",b="456"
String aa = "123";
String bb = "123";
System.out.println(aa == bb);
// 结果为true
- 安全性:String作为JDK底层核心类,使用非常广泛,需要配合特定的类加载器来保持唯一性,以便不会被外部轻易修改,这样可以避免很多安全问题。如String被广泛用于数据库、Socket中,如果String是可变,黑客改变字符串指向的对象的值,从而造成安全漏洞。
二、构造方法
1、概述
大概分为三种:
- 以String为参数:这种比较简单,直接把对类字段value、hash复制值即可。
- 以字节数组为参数:不作深究,了解即可,代码如下
// 将给定的字节数组转换为某种特定的编码的字符串,如utf8、gbk
public String(byte bytes[], int offset, int length, String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null)
throw new NullPointerException("charsetName");
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(charsetName, bytes, offset, length);
}
public String(byte bytes[], int offset, int length, Charset charset) {
if (charset == null)
throw new NullPointerException("charset");
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(charset, bytes, offset, length);
}
- 以char[]数组为参数:这种麻烦一点,但也是最为本质的一种构造参数,代码如下
public String(char value[]) {
// 表明不是通过简单的数组赋值,为什么呢? 我们等下再去验证
this.value = Arrays.copyOf(value, value.length);
}
public static char[] copyOf(char[] original, int newLength) {
char[] copy = new char[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
// 这就是那个native方法了,不做深究
JVM_ENTRY(void, JVM_ArrayCopy(JNIEnv *env, jclass ignored, jobject src, jint src_pos,
jobject dst, jint dst_pos, jint length))
JVMWrapper("JVM_ArrayCopy");
// Check if we have null pointers
if (src == NULL || dst == NULL) {
THROW(vmSymbols::java_lang_NullPointerException());
}
arrayOop s = arrayOop(JNIHandles::resolve_non_null(src));
arrayOop d = arrayOop(JNIHandles::resolve_non_null(dst));
assert(s->is_oop(), "JVM_ArrayCopy: src not an oop");
assert(d->is_oop(), "JVM_ArrayCopy: dst not an oop");
// Do copy
s->klass()->copy_array(s, src_pos, d, dst_pos, length, thread);
JVM_END
2、arraycopy
JDK源码不用简单的字符数组复制(即通过数组坐标),而采用native方法来实现字符数组复制操作,肯定是有原因的,大概猜测一下,是性能因素。测试代码如下:
public static void main(String[] args) {
char[] test1 = new char[]{'1', '2', '3'};
char[] test2 = test1;
test1[0] = '4';
// 结果是4,说明数组地址也是个引用,即数组不是原子类型
System.out.println(test2[0]);
// 简单复制耗时:0ms:ϧ
// native方法复制耗时:0ms:ϧ
testCopyArray(1000);
// 简单复制耗时:6ms:蚟
// native方法复制耗时:0ms:蚟,此时还看不出差距,往下
testCopyArray(100000);
// 简单复制耗时:14ms:陿
// native方法复制耗时:9ms:陿
testCopyArray(10000000);
// 简单复制耗时:246ms:,生僻字已经显示不出了
// native方法复制耗时:114ms:
testCopyArray(100000000);
}
private static void testCopyArray(int length) {
char[] chars1 = new char[length];
for (int i = 0; i < length; i++) {
chars1[i] = (char)i;
}
char[] chars1_copy1 = new char[length];
long startTime = System.currentTimeMillis();
for (int i = 0; i < length; i++) {
chars1_copy1[i] = chars1[i];
}
long endTime = System.currentTimeMillis();
System.out.print("简单复制耗时:" + (endTime - startTime) + "ms:");
System.out.println(chars1_copy1[length - 1]);
char[] chars1_copy2 = new char[length];
startTime = System.currentTimeMillis();
System.arraycopy(chars1, 0, chars1_copy2, 0, length);
endTime = System.currentTimeMillis();
System.out.print("native方法复制耗时:" + (endTime - startTime) + "ms:");
System.out.println(chars1_copy2[length - 1]);
}
现在可以得出结果了,是性能因素,至于内部原理,暂且不管。
三、常用方法
下面跟着我来看一下String类里面的对于字符串操作的方法封装代码(由于是修订版,这个部分就不重复写了)。
1、字符串比较
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];// A 65
char c2 = v2[k];// B 66
if (c1 != c2) {
return c1 - c2;// A - B = -1
}
k++;
}
return len1 - len2;
}
2、字符串连接
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);// buff里前len个为value的值,前面讲解过了
str.getChars(buf, len);// 等效于arraycopy(str.value, 0, buf, len, otherLen),即复制后半部分
return new String(buf, true);
}
void getChars(char dst[], int dstBegin) {
System.arraycopy(value, 0, dst, dstBegin, value.length);
}
3、字符串包含
这个方法包含一定的算法思想,单独拿出来讲解,算法大致分为这么几步:
- 纠错处理
- 从指定的位置上开始比较(这意味着我们可以指定忽视字符前n个比较字符)
- 当遇到第一个匹配的字符时停下,进行第二轮比较,即比较第二个匹配字符
- 当且仅当成功匹配目标字符的长度时,才算成功,否则一直向前比较,直到串尾
public boolean contains(CharSequence s) {
return indexOf(s.toString()) > -1;// 追踪此方法,有下一步-->
}
/**
* 中间有一些重构,但最终是会走到这一步
* 假设boolean isContains = "abcde".contains("cd");
* 相当于调用indexOf("abcde", 0, "abcde".length, "cd", 0, "cd".length, 0);
*/
static int indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex) {
if (fromIndex >= sourceCount) {// 如果开始对比的坐标大于等于源字符串的长度,如果目标字符串长度为0,返回源字符串长度,不然返回-1
return (targetCount == 0 ? sourceCount : -1);
}
if (fromIndex < 0) {// 如果开始比较的坐标小于0,那么归0处理
fromIndex = 0;
}
if (targetCount == 0) {// 如果目标字符串的长度为0,返回对比的坐标。
return fromIndex;
}
// 其实以上都是一些纠错处理,下面才是真正的对比算法步骤
char first = target[targetOffset];// 获取目标字符串的对比坐标所对应的值
int max = sourceOffset + (sourceCount - targetCount);// 暂时不知,设它为3-->而后得知它是表示找到首个匹配字符的最大次数
for (int i = sourceOffset + fromIndex; i <= max; i++) {// 循环了max-sourceOffset + fromIndex次,即3 + 0 + 0次
/* Look for first character. */
if (source[i] != first) {// 如果源字符串的第i个字符不是first
//while(1 <= max && 'a' != 'c')-->while(true)-->
//while(2 <= max && 'b' != 'c')-->while(true)-->
//while(3 <= max && 'c' != 'c')-->while(false)-->break if,此时i = 3;
while (++i <= max && source[i] != first);
}
/* Found first character, now look at the rest of v2 */
if (i <= max) {// i < = 3
int j = i + 1;// j = 4,即首个匹配字符后面的对比坐标
int end = j + targetCount - 1;// end = j + "cd".length - 1 = 5,标识二次比较的次数限制
// for(k = 0 + 1; 4 < 5 && source[4] == target[1]; j++, k++)-->true
// for(k = 2; 5 < 5 && source[5] == target[2]; j++, k++)-->true
for (int k = targetOffset + 1; j < end && source[j]
== target[k]; j++, k++);
if (j == end) {// 找到完全匹配的字符串
/* Found whole string. */
return i - sourceOffset;// 返回 3 - 0,即“首个”匹配字符坐标
}
}
}
return -1;
}
4、equals方法
String的equals方法,其本质还是char字符的比较。
public boolean equals(Object anObject) {
if (this == anObject) {// 如果是自己对比自己。由此我们可以得出,比较两个对象是不是同一对象,用==来
return true;
}
if (anObject instanceof String) {// 如果这个对象是String类型
String anotherString = (String)anObject;// 强制转型
int n = value.length;
if (n == anotherString.value.length) {// 先比较两者长度是否相等
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
5、hashCode方法
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];// h = 31 * h + int(value[i])
}
hash = h;
}
return h;
}
总结:当看到神秘的哈希码这样来的时,你是否和我一样,有着些许惊讶。于是乎,我断言,”AB”.hashCode() = 31 * (31 * 0 + 65) + 66 = 2081。我是对的。
6、忽视前后空格
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
while ((st < len) && (val[st] <= ' ')) {// (int)' ' = 32,由此推测(char)32之前可能都代表着某种空格含义
st++;// 记录下头部空格的个数,方便截取
}
while ((st < len) && (val[len - 1] <= ' ')) {
len--;// 记录下尾部空格的个数,方便截取
}
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;//截取
}
四、比较
下面我们比较下String、StringBuffer、StringBuilder的字符串拼接速度,测试代码如下
public class StringAppend {
private static int[] times = {10, 100, 1000, 10000, 100000, 200000};
public static void main(String[] args) {
testString();
testStringBuffer();
testStringBuilder();
}
private static void testString() {
long start = System.currentTimeMillis();
String test = "a";
for (int i = 0; i < times.length; i++) {
for (int j = 0; j < times[i]; j++) {
test = test + "a";
}
System.out.println("testString:次数为" + times[i] + ",执行耗时" + (System.currentTimeMillis() - start) + "ms");
}
System.out.println();
}
private static void testStringBuffer() {
long start = System.currentTimeMillis();
StringBuffer buffer = new StringBuffer("a");
for (int i = 0; i < times.length; i++) {
for (int j = 0; j < times[i]; j++) {
buffer.append("a");
}
System.out.println("testStringBuffer:次数为" + times[i] + ",执行耗时" + (System.currentTimeMillis() - start) + "ms");
}
System.out.println();
}
/**
* 顶呱呱
*/
private static void testStringBuilder() {
long start = System.currentTimeMillis();
StringBuilder builder = new StringBuilder("a");
for (int i = 0; i < times.length; i++) {
for (int j = 0; j < times[i]; j++) {
builder.append("a");
}
System.out.println("testStringBuilder:次数为" + times[i] + ",执行耗时" + (System.currentTimeMillis() - start) + "ms");
}
System.out.println();
}
}
// 输出结果如下
testString:次数为10,执行耗时0ms
testString:次数为100,执行耗时1ms
testString:次数为1000,执行耗时11ms
testString:次数为10000,执行耗时214ms
testString:次数为100000,执行耗时9566ms
testString:次数为200000,执行耗时38060ms
testStringBuffer:次数为10,执行耗时0ms
testStringBuffer:次数为100,执行耗时0ms
testStringBuffer:次数为1000,执行耗时0ms
testStringBuffer:次数为10000,执行耗时1ms
testStringBuffer:次数为100000,执行耗时7ms
testStringBuffer:次数为200000,执行耗时13ms
testStringBuilder:次数为10,执行耗时0ms
testStringBuilder:次数为100,执行耗时0ms
testStringBuilder:次数为1000,执行耗时0ms
testStringBuilder:次数为10000,执行耗时0ms
testStringBuilder:次数为100000,执行耗时4ms
testStringBuilder:次数为200000,执行耗时9ms
由输出发现String类在次数为100以后,与后两者根本不是一个级别的。下面是一个简单代码,用于测试String,如下
public class SimpleStringTest {
public static void main(String[] args) {
String test = "aa";
test = test + "bb";
System.out.println(test);
}
}
通过javap -verbose SimpleStringTest.class命令是看不出有什么特别的(具体原因不明,猜测可能是JDK为了节省篇幅或存在优化),通过第三方工具jclasslib查看,如下
0 ldc #2 <aa> // 将#2(aa)推送至栈顶
2 astore_1 // 将栈顶引用型数值存入第二个本地变量
3 new #3 <java/lang/StringBuilder> // 创建一个对象,并将其引用值压入栈顶
6 dup // 复制栈顶数值并将复制值压入栈顶(感觉这一步与上面一步有点重复)
7 invokespecial #4 <java/lang/StringBuilder.<init>> // 调用超类构造方法,实例初始化方法,私有方法
10 aload_1 // 将第二个引用类型本地变量推送至栈顶
11 invokevirtual #5 <java/lang/StringBuilder.append> // 调用实例方法
14 ldc #6 <bb> // 将#6(bb)推至栈顶
16 invokevirtual #5 <java/lang/StringBuilder.append> // 调用实例方法
19 invokevirtual #7 <java/lang/StringBuilder.toString> // 调用实例方法
22 astore_1 // 将栈顶引用型数值存入第二个本地变量
23 getstatic #8 <java/lang/System.out> // 获取指定类的静态域,并将其压入栈顶
26 aload_1 // 将第二个引用类型本地变量推送至栈顶
27 invokevirtual #9 <java/io/PrintStream.println> // 调用实例方法
30 return // 返回void
上面代码可理解为
public class SimpleStringTest1 {
public static void main(String[] args) {
String test = "aa";
test = test + "bb";
System.out.println(test);
StringBuilder test1 = new StringBuilder();
test1.append("aa");
test1.append("bb");
String temp = test1.toString();
System.out.println(temp);
}
}
上述代码其对应字节码为
0 ldc #2 <aa> // 将int、float或String类型常量值从常量池推送至栈顶
2 astore_1 // 将栈顶引用型数值存入第二个本地变量
3 new #3 <java/lang/StringBuilder> // 创建一个对象,并将其引用值压入栈顶
6 dup // 复制栈顶数值并将复制值压入栈顶(感觉new和dup是绑定的,有某种联系)
// 经过百度,大致是确实需要两个引用,一个有<init>调用,另一个则是<init>后的引用
7 invokespecial #4 <java/lang/StringBuilder.<init>> // 调用超类构造方法,实例初始化方法,私有方法
10 aload_1 // 将第二个引用类型本地变量推送至栈顶
11 invokevirtual #5 <java/lang/StringBuilder.append> // 调用实例方法
14 ldc #6 <bb> // 将#6(bb)推至栈顶
16 invokevirtual #5 <java/lang/StringBuilder.append> // 调用实例方法
19 invokevirtual #7 <java/lang/StringBuilder.toString> // 调用实例方法
22 astore_1 // 将栈顶引用型数值存入第二个本地变量
23 getstatic #8 <java/lang/System.out> // 获取指定类的静态域,并将其压入栈顶
26 aload_1 // 将第二个引用类型本地变量推送至栈顶
27 invokevirtual #9 <java/io/PrintStream.println> // 调用实例方法
// 分隔符 //
30 new #3 <java/lang/StringBuilder>
33 dup
34 invokespecial #4 <java/lang/StringBuilder.<init>>
37 astore_2 // 把SringBuild存到了Slot2
38 aload_2
39 ldc #2 <aa>
41 invokevirtual #5 <java/lang/StringBuilder.append>
44 pop // 将栈顶数值弹出
45 aload_2 // 加载第二个引用类型本地变量
46 ldc #6 <bb>
48 invokevirtual #5 <java/lang/StringBuilder.append>
51 pop
52 aload_2
53 invokevirtual #7 <java/lang/StringBuilder.toString>
56 astore_3
57 getstatic #8 <java/lang/System.out>
60 aload_3
61 invokevirtual #9 <java/io/PrintStream.println>
64 return
从字节码上看,一下子就明白了。
最后看下StringBuffer和StringBuilder的差别,跟踪两者源代码,如下
public final class StringBuffer extends AbstractStringBuilder implements Serializable, CharSequence {
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
}
public final class StringBuffer extends AbstractStringBuilder implements Serializable, CharSequence {
public StringBuilder append(String str) {
super.append(str);
return this;
}
}
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;
}
两者的区别在于StringBuffer加了一个关键字synchronized,做了同步化处理,所以性能稍稍慢点,而StringBuilder没有做同步化处理,所以性能稍稍好点,两者其实都是调用父类的方法。其他方法同理,都只是一个synchronized关键字的区别。
需要注意的是,如果在编译时期,一个String类型的常量值是确定的,那么编译器会对其进行优化,例子如下
String a = "aa" + "bb" + "cc";
// 完全等效于
String a = "aabbcc";
所以这种情况下,String是最快的。
总访问次数: 313次, 一般般帅 创建于 2017-09-15, 最后更新于 2020-06-25
欢迎关注微信公众号,第一时间掌握最新动态!