Java学习7:String类、StringBuffer类、StringBuilder类的应用学习及源码分析

一、String类的学习与应用:

1.1、String类的对象是final的

首先要知道,在Java中,字符串多数都是运用String类去实例化和操作的,在Java中,字符串就是一个对象,字符串变量是执行这个对象的引用。但是这个引用是常引用(c++概念),常引用的意思是:不能通过改变常引用来改变被引用对象的内容。在Java中,为了避免大家不小心改变了常引用导致String对象内容被改变,所以不允许大家用下标和方括号来访问String对象的内容。比如下面的程序,提供了通过下标访问的方法:

public static void main(String[] args){
		String str = new String("Hello World");
		System.out.println(str);
		
		for(int i = 0;i < str.length();++i)
			System.out.print(str.charAt(i) + " ");
			// 不支持写法:System.out.print(str[i] + " ");
	}

那大伙想想,既然内置方法可以访问它,是否可以修改它呢?
也是不能直接修改的!这里大家要注意:

原理:

在Java中,我们可以通过一些内置方法来改变String对象,但是这些改变并不是真正意义上的修改了对象所指向的内容,而是创建了新的对象,把旧对象的内容拷贝过去,然后加以改变,让引用去重新指向! 然后未被任何一个引用指向的对象,会被垃圾回收机制自动回收掉,这样是很安全的,但是也是很浪费资源的!毕竟垃圾回收是需要时间的。

学习一下有哪些好用的内置方法:

构造方法:

Java学习7:String类、StringBuffer类、StringBuilder类的应用学习及源码分析

这里重点学习后三种,看看程序实例:
	public static void main(String[] args){
		char[] str1 = {'H', 'e', 'l', 'l', 'o'};
		System.out.println(str1);
		System.out.println(str1.length);
		
		String str2 = new String("Hello");
		String str3 = new String(str1);
		String str4 = new String(str3);
		
		System.out.println("长度的变化是   一开始是:" + str1.length + "后来是:" + str3.length());
		
		System.out.println(str2);
		System.out.println(str3);
		System.out.println(str4);
	}		

程序输出的结果是:
Java学习7:String类、StringBuffer类、StringBuilder类的应用学习及源码分析

这说明了什么呢???

说明1:从字符数组到字符串尾部不加’ \0’
说明2:可以字符数组实例化String也可String实例化String
常用的其他方法:

Java学习7:String类、StringBuffer类、StringBuilder类的应用学习及源码分析
Java学习7:String类、StringBuffer类、StringBuilder类的应用学习及源码分析

最为重要的(个人认为)是:
length():返回字符串长度(没有 ‘\0’)
charAt(index):返回第(index + 1)个字符,相当于下标访问(但是只读)
substring(s, t):返回区间[s, t)的子串的首地址(相当于引用)
compareTo(s1, s2):返回(s1 - s2)这个差值, 这个差值是:
如果第一个字符和参数的第一个字符相等,则以第二个字符和参数的第二个字符做比较,以此类推,直至比较的字符或被比较的字符有一方被比较完了,那么就是算长度的差值 这个是有效比较字符串的大小的方法!

全比较完,这时就比较字符的长度.
replace(OldChar, NewChar):把字符串中的所有OldChar换成NewChar
valueOf(Obj):把对象实例Obj整个地换成一个字符串(原模原样地换)

咱们来实例实验一下:
	public static void main(String[] args){
		
		// 实验方法:charAt(index)、length():
		String str1 = new String("abcde");
		for(int i = 0;i < str1.length();++i)
			System.out.print(str1.charAt(i) + " ");
		System.out.println();
		
		// 实验方法:substring(s, t):
		String str2 = new String(str1.substring(0, 3));
		System.out.print("str1 = " + str1 + "  str2 = " + str2 + ", str1 - str2 = " + str1.compareTo(str2));
	
		// 实验方法:A.compareTo(B):
		System.out.println();
		String str3 = new String("abcd");
		String str4 = new String("abec");
		System.out.println("str4 - str3 = " + str4.compareTo(str3));
		
		String str5 = new String("abecg");
		System.out.println("str5 - str4 = " + str5.compareTo(str4));
		
		// 实验方法:A.replace(Old, New)、valueOf(Obj);
		String str6 = "aabbcdeea";
		str6 = str6.replace('a', 'x');
		System.out.println("str6 = " + str6);
		
		double d = 3.1415926;
		String str7 = new String();
		str7 = String.valueOf(d);// valueOf()是静态方法
		System.out.println("str7 = " + str7);
			
	}	

看看实验结果:
Java学习7:String类、StringBuffer类、StringBuilder类的应用学习及源码分析

我们来看看学到了什么?

第一:valueOf(Obj)是静态方法!静态方法的类级别的,不是对象级别的,所以我们调用静态方法需要使用:类名.方法名 的方式去调用
第二:对于那种返回了一个新的对象的引用的方法,我们一定要用一个引用名去接收这个返回值,否则可能会让这个方法失效!!!(切记!)

1.2、String类的源码选读:

1.2.1、compareTo();的源码:

 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;
    }
源码分析:

这份代码没什么高深的,我们主要是通过源码学习我们常用的一些方法。从这份代码我们可以看出,如果两个字符串去比较, 如果在它们的公共长度部分有字符不相等的,那就会返回俩字符的差值,这个差值是俩字符的ASCii码的差值。如果在公共长度部分的字符串完全一样,就返回长度的差值。 这个方法的确可以有效比较字符串,就是效率不是太高。有一说一,Java这个String的源码效率本身就不是太高,那个字符匹配用的是BF算法,就不拿来解读了。

1.2.2、equals(Obj);的源码:

 public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof 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;
    }
这份源码告诉我们两点:

第一点:
涉及到对象的参数,一定要先判空,做异常处理!
第二点:
由于String的对象实例化,假如是那种深拷贝的话,两个对象会指向同一个地址,这俩对象的引用都是一样的,那就不必多加判断了,直接返回true,这个操作确实值得学习。

1.2.3、hash’code();的源码:

第三份源码:重写了String对象的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];
            }
            hash = h;
        }
        return h;
        // 要我我会改进:
        // return h & 0x7fffffff;
    }

这份代码其实用的是BKDRHash算法,这是很常见的字符串哈希算法,一般选用hash种子,seed = 31、17、……之类的素数,然后计算多项式作为哈希的值,但是一般这样处理的hash值很大,需要取模,而取模又带来hash冲突,所以一般还要加大量的代码去处理hash冲突。说实话,看到这个源码我是失望的,因为我原以为String的hashcode能果搞出适应高bite位的数据并且合适的处理冲突的方法……其实写的还没我算法书上写的好……

二、StringBuffer类 & StringBuilder类

之前我们说了,String类的对象是final的,也就是说,如果我们想要改变String对象的值,其实是开辟了新的空间,获取新的内容,然后让对象引用指向新的内容,这样会浪费空间和时间,效率很低,所以我们有了在 字符串需要被修改的时候 ,运用StringBuffer和StringBuilder去提高效率,但是这俩的区别也还是蛮大的,我们来学习一下。

2.1、它俩和String类对象的区别:

最主要的区别就是StringBuffer和StringBuilder的对象不是final的,是可以直接在字符串上进行修改的,不需要开辟新的空间,
相比较之下,StringBuilder的效率要高于StringBuffer!但是StringBuilder不能做同步访问(多线程),所以在要求线程安全(能同步访问)的情况下,就要用效率相对较低的StringBuffer了!但是由于效率最高的还是StringBuilder ,所以推荐在一般情况下(不需要同步访问),就使用StringBuilder。
我们用一张图来描述它们的继承关系:
Java学习7:String类、StringBuffer类、StringBuilder类的应用学习及源码分析

2.2、StringBuffer和StringBuilder的实例化:

StringBuffer和StringBuilder的实例化都必须使用构造函数去实例化,不能像String那样,类似拷贝构造地去构造,那样是不可以的(其实那样也是不安全的,那是一种深拷贝……)
实例化方法:

public static void main(String[] args){
		
		StringBuffer str1 = new StringBuffer("I am StringBuffer");
		StringBuilder str2 = new StringBuilder("I am StringBuilder");
		
		System.out.println(str1);
		System.out.println(str2);
	}	

结果:
Java学习7:String类、StringBuffer类、StringBuilder类的应用学习及源码分析

但是凡事都有例外,这个null就是可以不借助构造函数构造空字符串:

public static void main(String[] args){
		StringBuilder str1 = null;
		if(null == str1)
			System.out.println("Yes, str1 is null");
		StringBuffer str2 = null;
		if(null == str2)
			System.out.println("Yes, str2 is null");
	}	

结果:
Java学习7:String类、StringBuffer类、StringBuilder类的应用学习及源码分析
但是这样初始化和StringBuffer str = new StringBuffer();是完全不一样的,因为后面这个初始化空串其实还分配了16个缓冲区,是有空间的,是可以访问的。

常用的StringBuffer的构造函数:

Java学习7:String类、StringBuffer类、StringBuilder类的应用学习及源码分析
从这个构造函数我们可以知晓,为什么StringBuffer和StringBuilder是可以直接在字符串上进行操作,不需要重新开辟新的字符串对象去引用了。因为这些字符串本身自带缓冲区,这个缓冲区是适应于那种字符串长度改变的操作。
Java学习7:String类、StringBuffer类、StringBuilder类的应用学习及源码分析
是否可以自动增加容量呢?我们待会看源码就知晓了!

2.3、StringBuffer的常用方法:

Java学习7:String类、StringBuffer类、StringBuilder类的应用学习及源码分析

这些方法的几点说明:
第一:

println();是不会接受StringBuffer和StringBuilder的参数,如果需要访问需要先用toString();转换成String类型,但是这个我通过实验发现并不需要这样啊……

	public static void main(String[] args){
		StringBuffer str = new StringBuffer();
		str.append("Hello").append(" Java!");
		System.out.println(str);
	}	

Java学习7:String类、StringBuffer类、StringBuilder类的应用学习及源码分析
完全不受影响啊,难道是老师讲错了???我们来看看源码便知晓原因内涵:
Java学习7:String类、StringBuffer类、StringBuilder类的应用学习及源码分析
看到这个源码我们就知晓了,只要是输出,println();的函数参数可以是任何类型的对象 ,但是在真正输出的print里面,就只能把这个对象变成string类型,才能输出 !!

第二:

一般来说,我们要减少字符串连接符’+‘的操作,我们可以来解读一下这个过程:
比如:String str = "Hello" + "World";
这个过程实际上是:先实例化一个StringBuffer的对象Hello,然后调用"Hello".append(“World”);这个方法,去把这个字符串拼接起来,然后调用toString();方法,再最后返回一个构造函数给str,相当于把str实例化成这个整个的字符串,这样操作是很麻烦的,如果’+'的操作过多,是很不好的!效率会很低!

第四:

如果需要的容量比较大(大于16),就最好在实例化的时候,自己加上需要的容量,否则用默认容量(16)是会造成比较大的开销的(开销在扩容上),这是来自Java编程思想的内容,我们稍后看看这个扩容算法是如何实现的(猜测可能和c++的vector那样吧,搞位运算,2^k去扩容吧)

三、StringBuffer&StringBuilder源码:

子类源码没啥好读的,就是继承和多态而已,不说了,就看看父类的源码算了。

3.1、扩容代码:

 private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) {
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));
        }
    }

我们可以看到,如果我们需要的容量非常大却又不自己实现定义容量(也就是采用默认容量16)的时候,就可能经常调用这个方法,我们可以知道,这个copyof这个方法,肯定是一个O(n)的方法,调用的次数多了,就会导致效率很低下!所以如果使用容量比较大,就一定要自己在实例化的时候写明容量。

其他的代码:感觉也差不多,没啥特别的

但是我觉得我真正需要学习的就是这个代码的鲁棒性,这份源码执行效率确实不高,但是鲁棒性做的很到位!另外,这个线程安全与否的问题,等我学习了多线程、线程池之类的相关知识之后再来研究吧!虽然我貌似没在源码中发现相关的代码,稍后再补充吧!

上一篇:Java: StringBuffer类的运用


下一篇:java中Clob转String代码