分类: Java
2019-10-20 20:00:17
最近有一位同事问我String、StringBuffer、StringBuilder之间的区别是什么,只能回想起这三个类最明显的区别,具体细节实在是说不出来了,因为这已经是很早以前看《Java编程思想》的时候看到过的基础知识点了(至少有五年了没有恶补过这样的基础知识了)。所以当时有些尴尬,答应我的这位同事过几天做一次透彻的讲解(其实是为了一杯小鹿茶而已)。今天是周六,刚刚吃完饭消化一下,准备详细的阐述一下这三个类的具体内容,以及JDK定义这三个类的基本出发点。(学习过程中的两位老师:JDK源码Java language Specification)。
首先是String类,对于Java研发的同学是一定不会不熟悉这个类的(因为无论是业务开发还是技术底层开发这是基本类)。但是大家这的了解这个类吗?今天我仔细看了一下String类的1.8版本的源码,才发现原来我个人所为的了解也只不过是看了很多博客或者百科之类的了解(还是期望大家多看源码,而不是一些吹的很牛逼或者由哪位大咖写的书)。阅读下面的内容之前大家一定要时刻记住String类是不可变更的字符串对象。这是非常重要的。
JDK定义:
The {@code String} class represents character
strings. All
* string literals in Java programs, such as {@code "abc"}, are
* implemented as instances of this class.
翻译:String这个类表示的字符串特性,在Java程序中所有的字符序列都是这个类的实例,例如:“abc”这种字符串。
特性:
Strings are constant; their values cannot be changed after they
* are created. String buffers support mutable strings.
* Because String objects are immutable they can be shared.
翻译:String是恒定不变的,字符串被创建之后它们的值是不能被改变的。为了支持可变字符串,提供了String Buffer类。
上述为JDK关于String类的类文件注释。再次表明了字符串的不变特性。为什么说String类所代表的字符串内容是不可改变的呢个人认为有两种原因:
原因一(主动原因):在于String类定义本身,内部定义了用于存储字符串值得成员变量(private final char value[];)变量修饰符final,表示对象一旦创建将不可进行修改,并且对于该private变量未提供任何直接对外访问权限。这也说明了String对象在进行new创建时,需要传参,如果没有则默认为空字符串。
原因二(被动原因):基于JAVA 内存模型定义,在Java代码被编译过程中所有的数值和字符串都定义为常量,编译后的类型均为finial类型。存储在静态方法区中。所以类似“abc”的这种字符串在编译期就已经定义为常量不可变更。
下面深入Java langue specification详细了解一下具体内容,在文档的、§3.10.4章节详细阐述了String class所代表的含义,在本文中我就不翻译原文内容了,直接将文中的一个demo展示给大家:
package testPackage;
class Test {
public static void main(String[] args) {
String hello = "Hello", lo = "lo";
System.out.print((hello == "Hello") + " ");
System.out.print((Other.hello == hello) + " ");
System.out.print((other.Other.hello == hello) + " ");
System.out.print((hello == ("Hel"+"lo")) + " ");
System.out.print((hello == ("Hel"+lo)) + " ");
System.out.println(hello == ("Hel"+lo).intern());
}
}
class Other { static String hello = "Hello"; }
//另外一个包
package other;
public class Other { public static String hello = "Hello"; }
result:
true true true true false true
文中同样给出了6点总结:
This example illustrates six points:
· Literal strings within the same class () in the same package () represent references to the same String object ().
· Literal strings within different classes in the same package represent references to the same String object.
· Literal strings within different classes in different packages likewise represent references to the same String object.
· Strings computed by constant expressions () are computed at compile time and then treated as if they were literals.
· Strings computed by concatenation at run time are newly created and therefore distinct.
· The result of explicitly interning a computed string is the same string as any pre-existing literal string with the same contents.
1、在同一个包内的同一个类中的相同字面字符串代表相同String对象的引用。
2、在同一个包内的不同类中的相同字面字符串代表相同String对象的引用。
3、在不同包内的不同类中的相同字面字符串代表系统String对象的应用。
4、在编译时期由常量表达式计算得到的字符串,认作为字面量
5、在运行时,有拼接运算符计算而得的字符串是新创建的,因此是不同的。
6、显示interning一个运行时计算所得的字符串对象与具有相同字符串内容的预存字面字符串比较,具有相同结果。
接下来向大家分享几个关于String类内部的几个函数来详细讲解String的特性:
1、构造系:
public String() {
this.value = "".value;
}
String(String):有默认值得构造函数,其实该构造函数的实现结果等同于String str = "abc" ,需要大家注意的是内部的实现细节:
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
此处有一个非常大的问题一直困扰着我,this.value = original.value,相当于将原始字符串内部的value直接赋值给了新的字符串对象,所以该操作并不会增加特别多内存,只是将新对象的value直接指向了original的value。具体效果如下:
public String(StringBuffer buffer) {
synchronized(buffer) {
this.value=Arrays.copyOf(buffer.getValue(),buffer.length());
}
}
将Buffer的value进行了数组拷贝(synchronized的出现是由于StringBuffer是线程安全的)。
String(StringBuilder):StringBuilder其实跟StringBuffer类似,只不过StringBuilder缺少了线程同步的操作synchronized过程。
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}Str.concat(string):字符串拼接函数,将入参字符串“拼接”到当前字符串,并将拼接好的字符串封装成一个新的String对象返回。具体实现:
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);
str.getChars(buf, len);
return new String(buf, true);
}
从上图中所述,在整个字符串拼接过程中涉及到了三个内存空间的分配,所以为了防止地址空间的过渡分配,JDK引入了StringBuffer和Stringbuilder两个类。
String.join(delimiter,charsequnce …):静态多字符串+分隔符拼接函数,即将多个字符串按照某个拼接符进行逐个拼接。具体实现如下:
public static String join(CharSequence delimiter, CharSequence... elements) {
Objects.requireNonNull(delimiter);
Objects.requireNonNull(elements);
// Number of elements not likely worth Arrays.stream overhead.
StringJoiner joiner = new StringJoiner(delimiter);
for (CharSequence cs: elements) {
joiner.add(cs);
}
return joiner.toString();
}
该方法内引入了StringJoiner类,本文不详细讲解该类,有兴趣的可以直接查看源码,其内部主要支撑是StringBuilder类。
Str.replace(oldChar,newChar):本文只讲解该方法的具体细节,其他的replace和replaceAll均是采用正则表达式的方式进行匹配替换的。而该方法个人感觉还是挺有意思的:
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
该方法第一步:确定被替换字符和新字符是否相同,如果相同则直接返回当前字符不进行任何查找,也就是说即使oldchar在字符串中不存在也会返回str,否则,通过第一次while循环查找字符串中是否存在oldChar字符,如果存在直接break。(关注一下这里的while(++i < len),而不是用i++的原因,个人觉得这个地方用的非常巧妙和恰当),接着进入第二个if判断,主要判断str中是否存在oldChar。如果存在则进入if体。先创建一个等size的char空数组,通过for循环并将i之前的所有char 全部复制到新数组中,接着从第i的位置开始(包括i)逐个进行判断,如果等于oldChar就用newChar填充i的位置,否则用原有的i位置上的元素进行替换,直到最后。整个过程充分考虑到了String对象不可修改的原则。
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];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
该算法规则:首先取两个字符串之间长度最短的字符串长度为一个中位点。如果在中位点之前两个字符串能够得出比较结果则直接返回,否则以字符串长度为基准。
Str.intern():该方法其实不是比较方法,正常不应该放到该系里面来讲解,只不过该方法的调用大多数情况下都是用来做比较的用途。故将此方法的讲解放到了此处。该方法是一个native方法(public native String intern();)Java中所有的native方法都是jvm底层实现的。该方法的底层实现实质上是通过jvm底层调用获取jvm内存结构中静态方法区中的字符串pool中与当前字符串具有相同value的字符串对象。