上一篇写了基本数据类及其包装类详解,这篇我们就来讨字符串这个家族吧,这个也是重中之重啊!!!
文章结构:1.众说纷纷的equals和==;2.字符串家族的基本知识以及字符串家族的源码解读;3.字符串家族的坑以及使用推荐;
一、众说纷纷的equals和==:
总述:1. ==对于基本类型是比较其值,对于引用类型是比较地址,地址也可以是一个基本类型的值,因此可认为就是比较值的。2.equals只能用于对象的比较,是所有类的一个基本方法。如果用equals来比较基本类型的变量是有语法错误的,equals只是比较对象的内容。
(博主神经的牛角尖:假设两个对象用==比较,两个基本数据类型也用==比较,,它们是基于什么判断的,博主想了好久,想不通,以后有机会看编译原理的时候再出一篇)。
例子解析(输出什么??当然要大家亲自去编译查看啦,这才有好效果嘛。)
public class CompareTest {
public static void main(String[] args){
int t1=30;
int t2=90;
int t3=120;
int t4=120;
Boolean result1=(t1==t2); //验证不同值的比较是否相等
Boolean result2=((t1+t2)==t3); //验证基本数据类型只要数值相等即相等
Boolean result3=(t3==t4); //验证基本数据类型直接相等即相等
System.out.println("--【t1==t2】"+result1+"-----【(t1+t2)=t3】"+result2+"-----【t3=t4】"+result3);
//另外博主验证过了,只要在Integer缓存大小(-128-127)以内,只要数值相等,还是相等的。觉得大家应该动手试下这个就不贴太多出来了。
Integer s1 = Integer.valueOf(t1); //把基本数据类型传递给Integer包装类构建成对象
Integer s2 = Integer.valueOf(t2);
Integer s3 = Integer.valueOf(t3);
Integer s4 = Integer.valueOf(t4);
Integer s5 = Integer.valueOf(130);
Integer s6 = Integer.valueOf(130);
Boolean b1 = ((s1+s2)==s3); //验证只要数值相等,还是相等的。即使它是一个Integer对象相加
Boolean b2 = s3.equals(s3); //验证使用equals对象比较,因为还是在缓存区域以内,所以当然相等啦。
Boolean b3 = (s3==s4); //验证了Integer对象的缓存-128~127以内,值相等即可相等啦。但是只要超出缓存区域,就不相等了。
//以下就思考地址与对象的比较关系啦
Boolean b4 = (s5==s6); //验证啦超出缓存的对象啦,当然不相等啦。而且这个是对象地址的比较
Boolean b5 = s5.equals(s6); //验证比较两个拥有相同属性值的对象比较的话,就相等了嘛。只是比较对象的值,也就是对象的内容而已。
System.out.println("---【(s1+s2)==s3】"+b1+"-----【s3.equals(s3)】"+b2+"-----【s3==s4】"+b3+"-----【s5==s6】"+b4+"-----【s5.equals(s6)】"+b5);
}
}
补充:
“==”比较的是值–变量(栈)内存中存放的对象的(堆)内存地址
equals用于比较两个对象的值是否相同–不是比地址,是比较内容。
【特别注意】Object类中的equals方法和“==”是一样的,没有区别,而String类,Integer类等等一些类,是重写了equals方法,才使得equals和“==不同”(这个可参看我上一篇博客基本数据类及其包装类详解),所以,当自己创建类时,自动继承了Object的equals方法,要想实现不同的等于比较,必须重写equals方法。
另外,”==”比”equal”运行速度快,因为”==”只是比较引用。
二、字符串家族的基本知识:
(1)String:是不可变类,即一旦String对象被创建,包含这个对象的字符序列是不可改变的。
我们结合源码来看下重要的部分代码
//String类由final修饰,故String不能被继承。并且被标记了可序列化
//CharSequence 是字符串协议接口
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
//看到源码也这么说了,用于字符存储,并且它是一个final修饰的属性,所以String一旦创建即不可被修改。因此所有对String字符串的修改(如追加字符串,删除部分字符串,截取字符串)都不是在原来的对象基础上修改,而是新建一个String对象修改并返回,这会造成原对象被废弃,浪费资源且性能较差(特别是追加字符串和删除部分字符串),若遇到字符串将被频繁修改的情况,建议不要使用String,改用StringBuffer或StringBuilder。
private final char value[];
/**String类型的hash值**/
private int hash; // Default to 0
//数了下7个构造器,分析些常用和基本类似的吧。我们在生成一个String对象的时候必须对该对象的offset、count、value三个属性进行赋值,这样我们才能获得一个完成的String类型。
//这个就是直接赋值给字符存储的数组嘛。默认value为一个长度为0的数组,所以为null。
public String() {
this.value = new char[0];
}
//初始化新创建的字符串对象,它代表相同的字符序列的参数.新创建的字符串是参数字符串的副本(就是增删改string的时候的string对象),此构造函数的使用是因为字符串是不可变的。
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
//就是可以传入一个字符数组嘛。而且还是这个问题,string不可变,你一创建就被copy了。随后修改的字符对象已经不是原来的对象了。
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
//跟上面的构造器差不多,多点规范
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);//起始值下标小于0,抛异常
}
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);//取值长度小于0,抛异常
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);//起始值下标加长度大于数组长度,抛异常
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
//在构造对象时,传入了下标以及长度
public String(int[] codePoints, int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
// Note: offset or count might be near -1>>>1.
if (offset > codePoints.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
//构造对象后:(步骤一)精确计算String所需要的长度。自动计算string所需长度。
int n = count;
for (int i = offset; i < end; i++) {
int c = codePoints[i];
if (Character.isBmpCodePoint(c))
continue;
else if (Character.isValidCodePoint(c))
n++;
else throw new IllegalArgumentException(Integer.toString(c));
}
//构造对象后:(步骤二)分配和填充字符对象。提取每一位的字符,并将其放入String字符串。
final char[] v = new char[n];
for (int i = offset, j = 0; i < end; i++, j++) {
int c = codePoints[i];
if (Character.isBmpCodePoint(c))
v[j] = (char)c;
else
Character.toSurrogates(c, v, j++);
}
this.value = v;
}
//可以看到StringBuffer 和StringBuilder 传进来也可成为String对象。但是三者并没继承关系。
public String(StringBuffer buffer) {
//嘿嘿,神奇的一个线程关键字synchronized。一会有解析
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
//很普通的方法啦
public int length() {
return value.length;
}
public boolean isEmpty() {
return value.length == 0;
}
//用于取得字符串下标为i的字符
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
//返回指定索引处的字符(Unicode代码点)。该索引引用char值(Unicode代码单元),其范围从 0 到 length() - 1。就是返回一个Unicode值。
public int codePointAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return Character.codePointAtImpl(value, index, value.length);
}
//上面解析过,Integer,String这些类都是重写了equals才有不同的效果。比较字符串的值是否相同 。
public boolean equals(Object anObject) {
if (this == anObject) {
return true;//若比较的两个对象引用地址值相同,则为同一个对象,值当然相同
}
if (anObject instanceof String) {//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) {//将两个String对象的值放入数组中,遍历比较,全部相同才表示相同
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
//如果是直接String比较,很快的嘛。
public boolean equalsIgnoreCase(String anotherString) {
return (this == anotherString) ? true
: (anotherString != null)
&& (anotherString.value.length == value.length)
&& regionMatches(true, 0, anotherString, 0, value.length);
}
//比较两个字符串字典。什么意思呢?比较是基于字符串中的每个字符的Unicode值,就是遍历去比较两个字符串的每个字符Unicode值大小。其结果是负的整数,如果此String对象字典前面的参数字符串;其结果是一个正整数,如果此String对象字典如下的参数字符串;结果是零,如果两个字符串相等,CompareTo返回0时,equal(Object)方法将返回true。
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;
}
//判断一个字符串是否以prefix字符串开头,toffset是相同的长度
public boolean startsWith (String prefix,int toffset){
char ta[] = value;
int to = offset + toffset;
char pa[] = prefix.value;
int po = prefix.offset;
int pc = prefix.count;
// Note: toffset might be near -1>>>1.
if ((toffset < 0) || (toffset > count - pc)) {
return false;
}
while (--pc >= 0) {
if (ta[to++] != pa[po++]) {
return false;
}
}
return true;
}
//连接两个字符串
public String concat (String str){
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
char buf[] = new char[count + otherLen];
getChars(0, count, buf, 0);
str.getChars(0, otherLen, buf, count);
return new String(0, count + otherLen, buf);//哈哈哈,看到了吧,都是新建一个String对象,然后返回。
}
//String与基本类型的包装类转换源码啦。都是静态方法
public static String valueOf(boolean b) {
return b ? "true" : "false";
}
//这个比较有意思,用了一个非public的构造器,不过还是直接赋值给value属性去存储而已。但前提是传入一个字节数组,而不是字节
public static String valueOf(char c) {
char data[] = {c};
return new String(data, true);
}
public static String valueOf(int i) {
return Integer.toString(i);
}
public static String valueOf(long l) {
return Long.toString(l);
}
public static String valueOf(float f) {
return Float.toString(f);
}
//String截取方法,传入截取开始的下标
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);//当传入的开始下标符合且不为0时,新建一个String,注意这个String的值并没有变化,只是改变了偏移量
//传入开始index以及结束下标去截取
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);//与上面的方法类似,没有改变String的value属性,只是而是改变了偏移量和count长度。
}
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;//替代的是整个value中的oldChar,而不是从偏移量开始替代
int i = -1;
char[] val = value;
while (++i < len) {//先遍历数组中是否有原字母,没有就无需替换,高效的设计
if (val[i] == oldChar) {
break;
}
}
if (i < len) {//获得需要替换的char的下标,此下表以前的char直接复制,
此下标以后的char才开始一个一个比较,若等于oldchar则替换,高效
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;
}
}
}
嗯,String常用的源码基本解析完了。下面我们看些有趣的问题去思考。
下面就是java对String的深度优化问题啦。很恐怖。
String对象的地址问题,异同问题以及java对string的优化问题
public class StringTest {
public static void main(String[] args) {
String a="fuzhu"; //这样的方式也是创建一个string对象!!这样创建是放在常量池,new的方式是放在堆中。
String b="fuzhu";
String c=new String ("fuzhu");
String d=new String ("fuzhu");
System.out.println(a==b);//true,fuzhu被创建在String Pool中,a和b会指向同一个对象,
System.out.println(a==c);//a指向的fuzhu被创建在String Pool中,而c所指向的对象被创建在heap中,两者为不同的对象,地址值不同
System.out.println(d==c);//c和d所指的对象都被创建在堆中,但是两个不同的对象,地址值不同
System.out.println("---------重要的优化问题!!!----------");
String e = "ab";
String f = "a" + "b";
System.out.println((e == f));//答案是什么??是true!!因为什么?因为java对String有个字符串常量池,恐怖优化!下面有解析。
System.out.println("-----------验证什么时候是常量表达式!!--------");
String g = "a1";
String h = "a" + 1;
System.out.println((g == h)); //result = true
String i = "a" + true; //String i = "atrue";
String j = "atrue" ;
System.out.println((i == j)); //result = true
String k = "a" + 3.4; // String k = "a3.4";
String l = "a3.4" ;
System.out.println((k == l)); //result = true
System.out.println("---------验证非常量表达式的情况----------");
String o = "ab";
String p = "b";
String q = "a" + p;
System.out.println((o == q)); //result = false
}
}
1. String s=”a”是一种非常特殊的形式,和new 有本质的区别。它是java中唯一不需要new 就可以产生对象的途径。以 String s=”a”;形式赋值在java中叫直接量,它是在常量池中而不是象new 一样放在压缩堆中.
2. String s=”a”这种形式的字符串,在JVM内部发生字符串拘留,即当声明这样的一个字符串后,JVM会在常量池中先查找有有没有一个值为”a”的对象,如果有,就会把它赋给当前引用.即原来那个引用和现在这个引用指点向了同一对象,如果没有,则在常量池中新创建一个”a”,下一次如果有String s2 = “1”;又会将s1指向”abcd”这个对象,即以这形式声明的字符串,只要值相等,任何多个引用都指向同一对象.
3. 解析直接创建的java对string的优化过程:编译优化+ 常量池。String b = “a” + “b”;编译器将这个”a” + “b”作为常量表达式,在编译时进行优化,直接取结果”ab”。
至于怎么验证真的是直接取??当然只有看编译的二进制代码啦,我们可下载个ultraedit工具,去查看两个一模一样的类的十六进制。就可以看出来了。
4. 什么时候是常量表达式,什么时候不是??
肯定是的:(1)String + String(这的string指的是直接量);(2)string + 基本类型;
不是的情况:两个变量啊。比如两个string变量就不行了。
(2)StringBuffer:也代表字符串对象,与其余两个基本类似。但StringBuffer是线程安全,StringBuilder没有线程安全功能,所以StringBuilder性能略高。
因为StringBuffer和StringBuilder都继承了AbstractStringBuilder ,所以我们先看AbstractStringBuilder 。
AbstractStringBuilder 源码:
//为抽象类,主要的属性有两个,一个为value,一个为count,value用于存放值,count用于管理该类的容量
abstract class AbstractStringBuilder implements Appendable, CharSequence {
//用来存字符串
char[] value;
//用来计算存储使用的字符数量
int count;
public int length() {//length方法返回的是count的值,而不是value.length
return count;
}
//抽象类的构造器没啥必要看了吧
//这个才是返回容量,字符串总长度
public int capacity() {
return value.length;
}
//一层扣一层的封装的扩容机制:
public void ensureCapacity(int minimumCapacity) {
if (minimumCapacity > 0)
ensureCapacityInternal(minimumCapacity);
}
private void ensureCapacityInternal(int minimumCapacity) {
if (minimumCapacity - value.length > 0)//如果最小容量大于长度就要扩容了
expandCapacity(minimumCapacity);
}
void expandCapacity(int minimumCapacity) {
int newCapacity = value.length * 2 + 2;//自动扩容机制,每次扩容(value.length+1)*2
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;//若传入的参数小于0,则直接把容量设置到Integer的最
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;//若扩容后的容量还是小于传入的参数,则将传入的参数设为容量
}
value = Arrays.copyOf(value, newCapacity);//当count小于value.length时,将value多余长度的值删除,这时value.length的长度等于count
}
//用于保留value的值,保留的长度为count的值,只有当count的值小于value.length时才起作用,
public void trimToSize() {
if (count < value.length) {
value = Arrays.copyOf(value, count);
}
}
public void setLength(int newLength) {
if (newLength < 0)
throw new StringIndexOutOfBoundsException(newLength);
ensureCapacityInternal(newLength);//当传入的值大于等于0时,需要扩容
if (count < newLength) { //当传入值大于字符统计量
Arrays.fill(value, count, newLength, '\0');//为新扩容的元素赋值'\0',为结束符
}
count = newLength;//排除那堆不合理参数干扰后就是那个真正的字符统计量了。
}
//append依赖的一个方法,用以添加一个字符串数组
public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
{
if (srcBegin < 0)
throw new StringIndexOutOfBoundsException(srcBegin);
if ((srcEnd < 0) || (srcEnd > count))
throw new StringIndexOutOfBoundsException(srcEnd);
if (srcBegin > srcEnd)
throw new StringIndexOutOfBoundsException("srcBegin > srcEnd");
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);//用于添加字符串,将value的值添加到dst[]中
}
//在对象后拼接字符串或其他对象,效率较高,可以观察到,在拼接时并没有创建新的对象,也没有舍弃旧的对象,相对于String的机制,性能提升相当明显
//根据传入参数对应不同的方法
public AbstractStringBuilder append(Object obj) {
return append(String.valueOf(obj));
}
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();//若传入的字符串长度为0,则默认添加null这个字符串
int len = str.length();
ensureCapacityInternal(count + len);//扩容到这么大
str.getChars(0, len, value, count);//然后用getChar方法去添加字符串数组
count += len;//确定存储了这么多的字符
return this;
}
//拼接StringBuffer 也是可以的
public AbstractStringBuilder append(StringBuffer sb) {
if (sb == null)
return appendNull();
int len = sb.length();
ensureCapacityInternal(count + len);
sb.getChars(0, len, value, count);
count += len;
return this;
}
//这意味只要是AbstractStringBuilder 就可以拼接(暗指builder吧)
AbstractStringBuilder append(AbstractStringBuilder asb) {
if (asb == null)
return appendNull();
int len = asb.length();
ensureCapacityInternal(count + len);//直接检验容量,有需要则执行扩容
asb.getChars(0, len, value, count);//然后用getChar方法去添加字符串数组
count += len;//确定存储了这么多的字符
return this;
}
//添加null这个字符串
private AbstractStringBuilder appendNull() {
int c = count;
ensureCapacityInternal(c + 4);
final char[] value = this.value;
value[c++] = 'n';
value[c++] = 'u';
value[c++] = 'l';
value[c++] = 'l';
count = c;
return this;
}
//同理char的字节。就不看了,我们看下Interger等一些基本类型包装类,其余都基本类似的。
public AbstractStringBuilder append(int i) {
if (i == Integer.MIN_VALUE) {
append("-2147483648");//Integer最小值为特例,特殊处理
return this;
}
int appendedLength = (i < 0) ? Integer.stringSize(-i) + 1
: Integer.stringSize(i);//判断Integer的位数,负数有负号,要多加一位
int spaceNeeded = count + appendedLength;//确定字符串大小
ensureCapacityInternal(spaceNeeded);//还是扩容
Integer.getChars(i, spaceNeeded, value);//还是添加,不过是用Integer的静态方法
count = spaceNeeded;
return this;
}
//以删除一部分的字符,传入开始下标以及截止下标
public AbstractStringBuilder delete(int start, int end) {
//验证参数的有效性
if (start < 0)
throw new StringIndexOutOfBoundsException(start);
if (end > count)
end = count;//结束下标大于count时,将count设为结束下标
if (start > end)
throw new StringIndexOutOfBoundsException();//开始下标就大于结束下标,当然异常
int len = end - start;
if (len > 0) {
//System的静态方法来实现数组之间的复制。src:源数组; srcPos:源数组要复制的起始位置;dest:目的数组;destPos:目的数组放置的起始位置;length:复制的长度。注意:src and dest都必须是同类型或者可以进行转换类型的数组.
//这个函数可以实现自己到自己复制。解析:http://blog.csdn.net/kesalin/article/details/566354
System.arraycopy(value, start+len, value, start, count-end);//执行删除
count -= len;//重置count大小
}
return this;
}
//在对象中间插入字符串数组
public AbstractStringBuilder insert(int dstOffset, CharSequence s) {
//验证参数有效性
if (s == null)
s = "null";
if (s instanceof String)//这个必须要String才准插入
return this.insert(dstOffset, (String)s);
return this.insert(dstOffset, s, 0, s.length());
}
//传入的是插入开始下标,以及要插入的字符串。其他插入方法差不多,就不多说了。
public AbstractStringBuilder insert(int offset, String str) {
//验证参数有效性
if ((offset < 0) || (offset > length()))
throw new StringIndexOutOfBoundsException(offset);
if (str == null)
str = "null";
int len = str.length();
ensureCapacityInternal(count + len);//扩容
System.arraycopy(value, offset, value, offset + len, count - offset);//使用arraycopy方法去创建那么多长度先,就是先在value中建立起用于存放插入值的空位
str.getChars(value, offset);//向空位中插入str
count += len;//更新count值
return this;
}
//将对象本身的字符顺序调转后返回给原对象.。就是反转字符串
public AbstractStringBuilder reverse() {
boolean hasSurrogates = false;
int n = count - 1;
//采用从中间向两端遍历,对换对称位置上的字符
for (int j = (n-1) >> 1; j >= 0; j--) {
int k = n - j;
char cj = value[j];//两个暂存变量
char ck = value[k];
value[j] = ck;//直接对应位置交换
value[k] = cj;
//验证每个字符的编码是否在范围内
if (Character.isSurrogate(cj) ||
Character.isSurrogate(ck)) {
hasSurrogates = true;
}
}
if (hasSurrogates) {
//直接反转后,如果两字符顺序错了,就需要重新调整。考虑到存在增补字符,需要成对校验,就是超出了字符的编码范围的话就会重新翻转到原来那样
reverseAllValidSurrogatePairs();
}
return this;
}
//reverse的依赖方法--重新调整字符顺序
private void reverseAllValidSurrogatePairs() {
for (int i = 0; i < count - 1; i++) {
char c2 = value[i];
if (Character.isLowSurrogate(c2)) {
char c1 = value[i + 1];
if (Character.isHighSurrogate(c1)) {
value[i++] = c1;
value[i] = c2;
}
}
}
}
//返回一个新字符串,它是此字符串的一个子字符串。该子字符串从指定的 start索引处开始,一直到索引 end- 1 处的字符。因此,该子字符串的长度为 end-start。
public String substring(int start, int end) {
if (start < 0)
throw new StringIndexOutOfBoundsException(start);
if (end > count)
throw new StringIndexOutOfBoundsException(end);
if (start > end)
throw new StringIndexOutOfBoundsException(end - start);
return new String(value, start, end - start);
}
}
嗯,AbstractStringBuilder是Stringbuilder和StringBuffer的父类,两者的共同抽象出来很多相似性质嘛。
接下来我们看StringBuilder的关键源码:
也代表字符串对象。但这个是线程不安全的。
//StringBuilder类由final修饰,不能被继承,并且继承了AbstractStringBuilder类,并完成了toString方法,同时使用了AbstractStringBuilder类中大量的方法。
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence{
/** 序列号, 对象序列化和反序列化需要的唯一标识版本号,基本类中,只要接入Serializable接口都会分配一个id */
static final long serialVersionUID = 4383685877147921099L;
///默认构造方法
public StringBuilder() {
//使用父类的构造方法,默认初始化容量为capacity = 16 。基本所有jdk中实现类涉及初始化容量的大小都为16,加上一点扩容机制
super(16);
}
//带一个参数的构造方法,可以指定初始化容量
public StringBuilder(int capacity) {
super(capacity);
}
//带一个参数构造方法,与前一个不同,这是指定一个String来初始化
public StringBuffer(String str) {
// 这里可以注意下,指定String初始化StringBuffer的时候指定容量大小为String的长度加上16
super(str.length() + 16);
//然后追加到value中
append(str);
}
//其余的构造方法就都类似的啦
//下面是字符串修改方法。基本都是用父类的方法的,我们刚刚就分析过了,就不多分析了。但是要注意:修改的方法全部都为线程不安全,是牺牲了安全用以实现性能。若需要考虑线程的安全性,建议使用StringBuffer。一会会看到为啥Buffer是线程安全的。
@Override
public StringBuilder append(Object obj) {
return append(String.valueOf(obj));
}
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
@Override
public String toString() {
// Create a copy, don't share the array
//源码所说,是new了一个String对象,返回string出去,而不是直接给Builder出去。
return new String(value, 0, count);
}
//将对象序列化,写入了count和value。
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
s.defaultWriteObject();
s.writeInt(count);
s.writeObject(value);
}
//用于反序列化,将count和value属性读取出来
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
count = s.readInt();
value = (char[]) s.readObject();
}
}
(3)StringBuffer:代表一个字符序列可变的字符串。这个stringbuffer提供了一系列的修改方法去改变字符串对象序列。一旦StringBuffer生成了最终想要的对象,调用toString方法将它转换成一个String对象即可。
来观份源码,将会详细地去对比StringBuilder去讲解
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
//Builder没有的功能(一):最后一次修改后的缓存值(字符数组保存),只要修改了value,那么就会重置
private transient char[] toStringCache;
/** 序列号, 对象序列化和反序列化需要的唯一标识版本号 */
static final long serialVersionUID = 3388685877147921107L;
//构造方法基本跟builder差不多的啦
public StringBuffer() {
super(16);
}
public StringBuffer(int capacity) {
super(capacity);
}
//获取字符串字符数量,效率低,对象锁,所以这样才会线程安全
@Override
public synchronized int length() {
return count;
}
// 获取容量,效率低--对象锁
@Override
public synchronized int capacity() {
return value.length;
}
//确保容量不小于minimumCapacity
@Override
public synchronized void ensureCapacity(int minimumCapacity) {
if (minimumCapacity > value.length) {
// 当最小容量值(传进来的参数值)大于value.length(这个其实就是容量),那么就直接扩容
expandCapacity(minimumCapacity);
}
}
//扩容,在它父类讲过了。
void expandCapacity(int minimumCapacity) {
int newCapacity = value.length * 2 + 2;
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
value = Arrays.copyOf(value, newCapacity);
}
//将value数组中没有存入元素的部分去掉(类似去空格),此时容量大小和size大小相等。也就是删掉count后面的那堆空的东东
@Override
public synchronized void trimToSize() {
//用父类的。但线程安全
super.trimToSize();
}
//setLength讲过了就省略了。注意它也是线程安全。扩充字符串容量到newLength,并且用空格填充
@Override
public synchronized void setLength(int newLength) {
toStringCache = null;
//调用父类函数
super.setLength(newLength);
}
//根据指定索引获取字符,效率慢,对象锁.
@Override
public synchronized char charAt(int index) {
if ((index < 0) || (index >= count))
throw new StringIndexOutOfBoundsException(index);
return value[index];
}
/**
* 根据索引修改字符串中某个字符值
*/
@Override
public synchronized void setCharAt(int index, char ch) {
if ((index < 0) || (index >= count))
throw new StringIndexOutOfBoundsException(index);
toStringCache = null;//清除缓存,只要修改了value,此值就会clear
value[index] = ch;
}
//修改操作跟父类的唯一区别就是线程安全了:就不多贴了
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
/**
* 不存在内存泄漏,实现了线程安全
*/
@Override
public synchronized String substring(int start) {
return substring(start, count);
//结果是new String(value, start, end - start);弄了个新对象的。
// 没有复用char[]
}
//有一套这样的同步与不同步方法
/**
* 此方法不同步, 而且也没有 toStringCache = null;
* 如果需要同步,那么需要将boolean b转化为specific type特定类型(String)
*/
@Override
public StringBuffer insert(int offset, boolean b) {
super.insert(offset, b);
return this;
}
@Override
public synchronized StringBuffer insert(int offset, char c) {
toStringCache = null;
super.insert(offset, c);
return this;
}
//toStringCache之前都不知道这个字段的含义,看到这里似乎看懂了,。是提高toString函数的效率,不用每次都是调用。也就是有做了一个缓存
// Arrays.copyOfRange。。。但是字符串修改后这个值需要clear
//线程安全
@Override
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);//返回一个新的string对象过去
}
// 自定义序列化字段
//**transient 用于指定哪个字段不被默认序列化,如public transient int a;
//serialPersistentFields 用于指定哪些字段需要被默认序列化.如下:
private static final java.io.ObjectStreamField[] serialPersistentFields =
{
new java.io.ObjectStreamField("value", char[].class),
new java.io.ObjectStreamField("count", Integer.TYPE),
new java.io.ObjectStreamField("shared", Boolean.TYPE),
};
/**
* 序列化大到ObjectOutputStream,写入了count和value、shared
*/
private synchronized void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
java.io.ObjectOutputStream.PutField fields = s.putFields();
fields.put("value", value);
fields.put("count", count);
fields.put("shared", false);
s.writeFields();
}
/**
* 反序列化到对象,读出count和value。
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
java.io.ObjectInputStream.GetField fields = s.readFields();
value = (char[])fields.get("value", null);
count = fields.get("count", 0);
}
}
三、字符串家族的坑以及使用推荐
1.效率问题:效率:StringBuilder>StringBuffer>String
public static void main(String[] args) {
long start = System.currentTimeMillis();
String str = null;
for (int i = 0; i < 20000; i++) {
str = str + i + ",";//因为是不断弄出一个新的对象出来的
}
System.out.println("String耗时 "+ (System.currentTimeMillis() - start));
System.out.println("-------------------");
//buffer和builder都有自动扩容机制,不像string有两大缺点:1.返回对象使用大量new操作,产生很多垃圾;2.虽然最终调用的是系统复制数组操作,但调用之前开销非常大,只能靠复制来解决拼接问题。
start = System.currentTimeMillis();
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < 20000; i++) {
buffer.append(i + ",");//线程安全所以慢一点,但前提这里是单线程
}
System.out.println("StringBuffer耗时 "+(System.currentTimeMillis() - start));
System.out.println("-------------------");
start = System.currentTimeMillis();
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 20000; i++) {
builder.append(i + ",");//线程不安全,效率最高
}
System.out.println("StringBuilder耗时 "+(System.currentTimeMillis() - start));
}
/*
String耗时 2030
-------------------
StringBuffer耗时 5
-------------------
StringBuilder耗时 3
*/
2.线程安全与线程不安全问题:验证:
/**
* Created by ${符柱成} on 2017/1/11.
*/
public class StringBuilderTest {
public static void main(String[] args) {
/*
* 声明个字符串s,用下划线和井号是因为两个比较好区分。 分别实例化StringBuffer和StringBuilder两个对象
*/
String s = "####____";
StringBuffer sf = new StringBuffer(s);
StringBuilder sd = new StringBuilder(s);
/*
* 对sf和sd各自实例化两个反转他们的类
*/
// ThreadString sfr1 = new ThreadString(sf);
// ThreadString sfr2 = new ThreadString(sf);
ThreadString sdr1 = new ThreadString(sd);
ThreadString sdr2 = new ThreadString(sd);
/*
* 启动这四个线程,此时sf和sd各自有两个线程在对他们进行字符串反转操作
*/
// new Thread(sfr1).start();
// new Thread(sfr2).start();
new Thread(sdr1).start();
new Thread(sdr2).start();
}
}
class ThreadString implements Runnable {
/*
* 这个类用来完成字符串的反转工作,使用了Runnable接口来实现多线程 times是用来表示循环多少次的
* 因为懒的再写一个变量所以用了一个Object类型的s,后面再转化
*/
public Object s = null;
int times = 10;
/*
* 两个构造方法把s传进来
*/
public ThreadString(StringBuffer s) {
this.s = s;
}
public ThreadString(StringBuilder s) {
this.s = s;
}
/*
* 复写run方法实现多线程 在我的电脑上大概循环十几次可以看到效果了
*/
public void run() {
for (int i = 0; i <= times; i++) {
//sleep一下让输出更加清晰
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if (s instanceof StringBuffer) {
((StringBuffer) s).reverse();//直接调用翻转的方法
System.out.println("BUFFER->" + s);
} else if (s instanceof StringBuilder) {
((StringBuilder) s).reverse();//直接调用翻转的方法
System.out.println(" " + s + "<-Builder");
}
System.out.println(Thread.currentThread().getName());//输出下线程名字更加清晰啦
System.out.println("-------------------");
}
}
}
/*
* 最后看一下控制台的输出会发现反转后出现井号和下划线交错的都是StringBuilder的输出
*/
/*
我们输出的时候会发现,Builder在一次线程操作中甚至可以这样反转,但是Buffer就可观察到没有这样的抢占修改,是十分有序地修改。
-------------------
####____<-builder
____####<-builder
Thread-1
-------------------
*/
使用注意点总结:
1. 如果要操作少量的数据用 = String
2. 单线程操作字符串缓冲区下操作大量数据 –StringBuilder
3. 多线程操作字符串缓冲区 下操作大量数据 — StringBuffer
好了,深入Java基础(二)——字符串家族讲完了。本博客是经过仔细研究其他的博客,结合源代码注释,并在这里做出进一步拓展以及写出自己的理解。另外,这个系列会逐步更新,也希望阅读更多源码,分享经验给大家。欢迎在下面指出错误,共同学习!!你的点赞是对我最好的支持!!
这个系列的文章因为涉及很多源码,所以会根据网友们的反应以及我的学习深入去补充。希望大家一起来讨论学习。
转载请注明:【JackFrost的博客】