首先要知道,在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对象,但是这些改变并不是真正意义上的修改了对象所指向的内容,而是创建了新的对象,把旧对象的内容拷贝过去,然后加以改变,让引用去重新指向! 然后未被任何一个引用指向的对象,会被垃圾回收机制自动回收掉,这样是很安全的,但是也是很浪费资源的!毕竟垃圾回收是需要时间的。
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); }
程序输出的结果是:
这说明了什么呢???
最为重要的(个人认为)是:
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); }
看看实验结果:
我们来看看学到了什么?
第一:valueOf(Obj)是静态方法!静态方法的类级别的,不是对象级别的,所以我们调用静态方法需要使用:类名.方法名 的方式去调用
第二:对于那种返回了一个新的对象的引用的方法,我们一定要用一个引用名去接收这个返回值,否则可能会让这个方法失效!!!(切记!)
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算法,就不拿来解读了。
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,这个操作确实值得学习。
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去提高效率,但是这俩的区别也还是蛮大的,我们来学习一下。
最主要的区别就是StringBuffer和StringBuilder的对象不是final的,是可以直接在字符串上进行修改的,不需要开辟新的空间,
相比较之下,StringBuilder的效率要高于StringBuffer!但是StringBuilder不能做同步访问(多线程),所以在要求线程安全(能同步访问)的情况下,就要用效率相对较低的StringBuffer了!但是由于效率最高的还是StringBuilder ,所以推荐在一般情况下(不需要同步访问),就使用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); }
结果:
但是凡事都有例外,这个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"); }
结果:
但是这样初始化和StringBuffer str = new StringBuffer();是完全不一样的,因为后面这个初始化空串其实还分配了16个缓冲区,是有空间的,是可以访问的。
从这个构造函数我们可以知晓,为什么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); }
完全不受影响啊,难道是老师讲错了???我们来看看源码便知晓原因内涵:
看到这个源码我们就知晓了,只要是输出,println();的函数参数可以是任何类型的对象 ,但是在真正输出的print里面,就只能把这个对象变成string类型,才能输出 !!
一般来说,我们要减少字符串连接符’+‘的操作,我们可以来解读一下这个过程:
比如:String str = "Hello" + "World";
这个过程实际上是:先实例化一个StringBuffer的对象Hello,然后调用"Hello".append(“World”);这个方法,去把这个字符串拼接起来,然后调用toString();方法,再最后返回一个构造函数给str,相当于把str实例化成这个整个的字符串,这样操作是很麻烦的,如果’+'的操作过多,是很不好的!效率会很低!
如果需要的容量比较大(大于16),就最好在实例化的时候,自己加上需要的容量,否则用默认容量(16)是会造成比较大的开销的(开销在扩容上),这是来自Java编程思想的内容,我们稍后看看这个扩容算法是如何实现的(猜测可能和c++的vector那样吧,搞位运算,2^k去扩容吧)
三、StringBuffer&StringBuilder源码:子类源码没啥好读的,就是继承和多态而已,不说了,就看看父类的源码算了。
private void ensureCapacityInternal(int minimumCapacity) { // overflow-conscious code if (minimumCapacity - value.length > 0) { value = Arrays.copyOf(value, newCapacity(minimumCapacity)); } }
我们可以看到,如果我们需要的容量非常大却又不自己实现定义容量(也就是采用默认容量16)的时候,就可能经常调用这个方法,我们可以知道,这个copyof这个方法,肯定是一个O(n)的方法,调用的次数多了,就会导致效率很低下!所以如果使用容量比较大,就一定要自己在实例化的时候写明容量。
但是我觉得我真正需要学习的就是这个代码的鲁棒性,这份源码执行效率确实不高,但是鲁棒性做的很到位!另外,这个线程安全与否的问题,等我学习了多线程、线程池之类的相关知识之后再来研究吧!虽然我貌似没在源码中发现相关的代码,稍后再补充吧!