? ? 今年终于重新回到开发岗位继续我的程序员生涯,但时隔一年半,很多都有些生疏了,仔细读了一遍<Thinking In Java>,发现很多基本技术和思想其实从一开始就没研究明白。决定重新对照着书本,jdk和源码把Java中基本的类库研读总结下。
? ? 第一篇就先从最简单最基本的String开始吧!
? ? 博客的对代码的排版似乎不好,附件里附上txt格式的内容,可读性会好些。
?
1.String的基本特性
? ? 我们在class中找到java.lang.String,先来看String的基本特性。
?
? ? String类的定义是这样的:public final class String implements java.io.Serializable, Comparable<String>, CharSequence
? ? 从以上定义,我们可以看出String的几个基本特性:
? ? ·String是一个类。(似乎是废话。主要与其他基本类型分开,Java中的类型似乎只有String与Enum是类形式定义的。)
? ? ·String是只读的常量,具有不可变性;不可被继承。(修饰为final)
? ? ·String类型是可序列化的,可比较的字符序列。(implements java.io.Serializable, Comparable<String>, CharSequence)
? ? 再来看一下Sting类中的定义的几个基本field
? ? private final char value[];//String的值,String的本质就是char型数组
? ? private final int offset;//子数组第一个字符的索引
? ? private final int count;//字符串中的字符数量
? ? private int hash; //对象的hash code
? ? 经过上面的分析,其实我们已经能看出String对象的本质是一个经过封装的char型数组,封装后提供了一些方便操作的方法。
?
? ??
2.Sting常用方法及实现(大部分注释选自JavaSE5中文API说明)
? ? 2.1.equals(Object anObject)
?
?
/** * 重写equals方法 比较此字符串与指定的对象。 * 当且仅当该参数不为 null,并且是表示与此对象相同的字符序列的 String对象时,结果才为true。 * @param anObject 与此 String 进行比较的对象。 * @return 如果 String 相等,则返回 true;否则返回 false。 * @see java.lang.Object#equals(java.lang.Object) */ public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) {// 先判断类型 String anotherString = (String) anObject; int n = count; if (n == anotherString.count) {// 再判断数组容量是否相同 char v1[] = value; char v2[] = anotherString.value; int i = offset; int j = anotherString.offset; while (n-- != 0) {// 再遍历判断每一个字符 if (v1[i++] != v2[j++]) return false; } return true; } } return false; }
? ? 2.2.compareTo(String anotherString)方法
?
/** * 重写compreTo方法 按字典顺序比较两个字符串。 该比较基于字符串中各个字符的 Unicode 值。 将此 String * 对象表示的字符序列与参数字符串所表示的字符序列进行比较。如果按字典顺序此 String 对象在参数字符串之前,则比较结果为一个负整数。 * 如果按字典顺序此 String 对象位于参数字符串之后,则比较结果为一个正整数。 如果这两个字符串相等,则结果为 0;compareTo * 只有在方法 equals(Object) 返回 true 时才返回 0。 这是字典排序的定义。 * 如果这两个字符串不同,则要么它们在某个索引处具有不同的字符,该索引对二者均为有效索引;要么它们的长度不同;或者同时具备上述两种情况。 * 如果它们在一个或多个索引位置上具有不同的字符, * 假设 k 是这类索引的最小值;则按照 < 运算符确定的那个字符串在位置 k上具有较小的值,其字典顺序在其他字符串之前。 * 这种情况下,compareTo 返回这两个字符串在位置 k 处的两个不同的 char 值,即值: * this.charAt(k)-anotherString.charAt(k) * 如果它们没有不同的索引位置,则较短字符串在字典顺序上位于较长字符串的前面。 这种情况下,compareTo 返回这两个字符串长度的不同,即值: * this.length()-anotherString.length() * @param anotherString 要比较的 String。 * @return 如果参数字符串等于此字符串,则返回 0 值; * 如果按字典顺序此字符串小于字符串参数,则返回一个小于 0 的值; * 如果按字典顺序此字符串大于字符串参数,则返回一个大于 0 的值。 */ public int compareTo(String anotherString) { int len1 = count; int len2 = anotherString.count; int n = Math.min(len1, len2);// 获得两个字符串中较短的长度 char v1[] = value; char v2[] = anotherString.value; int i = offset; int j = anotherString.offset; /* 在一个或多个索引位置上具有不同的字符 */ if (i == j) {// 偏移量相同 int k = i; int lim = n + i; while (k < lim) { char c1 = v1[k]; char c2 = v2[k]; if (c1 != c2) { return c1 - c2; } k++; } } else { while (n-- != 0) { char c1 = v1[i++]; char c2 = v2[j++]; if (c1 != c2) { return c1 - c2; } } } /* 没有不同的索引位置 */ return len1 - len2; }
? ? 前面的注释中,已经把String的排序规则说的很明白了。简单来说就是:1.先判断每个索引处的字符都相同,找到第一个字符不同处的索引,返回此索引处两个字符的差;2.如果每个索引处的字符都相同,判断两个数组的长度,返回两个字符串的长度差。
?
? ??2.3.startsWith(String prefix, int toffset)
?
/** * 测试此字符串是否以指定前缀开始,该前缀以指定索引开始。 * @param prefix 前缀 * @param toffset 在字符串中开始查找的位置。 * @return 如果该参数表示的字符序列是此对象从索引 toffset 处开始的子字符串,则返回 true;否则返回 false。 * 如果 toffset 为负或大于此 String 对象的长度,则结果为 false;否则结果与该表达式的结果相同。 */ 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; }?
? ? 这个方法的实现很简单,需要注意的就是toffset的值不要为负或者大于String对象的长度。
? ? 2.4.String substring(int beginIndex, int endIndex)
?
public String substring(int beginIndex, int endIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } if (endIndex > count) { throw new StringIndexOutOfBoundsException(endIndex); } if (beginIndex > endIndex) { throw new StringIndexOutOfBoundsException(endIndex - beginIndex); } return ((beginIndex == 0) && (endIndex == count)) ? this : new String(offset + beginIndex, endIndex - beginIndex, value); }
? ? 很常用的一个方法,会截取从beginIndex处到endIndex处的字符串,生成一个新的字符串。从方法实现可以看出,要注意的有两点:1.beginIndex和endIndex两个参数要符合要求,否则会抛出StringIndexOutOfBoundsException。2.截取的字符串包括beginIndex处的字符,不包括endIndex处的字符。
?
? ? ?2.5.String trim()
?
/** * 返回字符串的副本,忽略前导空白和尾部空白。 * 如果此 String 对象表示一个空字符序列,或者此 String 对象表示的字符序列的第一个和最后一个字符的代码都大于 '\u0020'(空格字符), * 则返回对此 String 对象的引用。 * 否则,若字符串中没有代码大于 '\u0020' 的字符,则创建并返回一个表示空字符串的新的 String 对象。 * 否则,假定 k 为代码大于 '\u0020' 的第一个字符的索引,m 为代码大于 '\u0020' 的最后一个字符的索引。 * 创建一个新的 String 对象,它表示此字符串中从索引 k 处的字符开始,到索引 m 处的字符结束的子字符串,也就是 this.substring(k, m+1) 的结果。 * 此方法用于截去字符串从头到尾的空白(如上面所定义)。 * @return 此字符串移除了前导和尾部空白的副本,如果没有前导和尾部空白,则返回此字符串。 */ public String trim() { int len = count; int st = 0; int off = offset; char[] val = value; while ((st < len) && (val[off + st] <= ' ')) { st++; } while ((st < len) && (val[off + len - 1] <= ' ')) { len--; } return ((st > 0) || (len < count)) ? substring(st, len) : this; }? ? ? 另一个常用的方法,去除空字符串。可以看到,这个方法的实现就是定位到非空字符串的索引处,然后调用substring方法截取字符串中的非空字符串。(Sun的工程师写代码真是简洁,但读起来有点累啊~)
public static String valueOf(float f) { return Float.toString(f); }? 3.String的几点使用须知 ? ?? ? ? 3.1.String的不变性。String对象是不可变的,只读的,任何对他的引用都无法改变他的值。我们从上面看的几个String内置方法的实现也可以看出,任何需要改变字符串内容的方法,实际在实现时都是创建了新的String对象作为返回值,而不是改变原有的String对象。(比如substring,trim) ? ? 3.2.String和StringBuilder(StringBuffer)的区别。这是个老生常谈的问题了,在频繁调用的循环中,StringBuilder无疑是有优势的(tips:如果能预先估算字符串的上限,那么预先为StringBuilder分配空间大小是提高效率的好办法),但是正常的简单拼接中,使用String的"+"和"+="真的没有想象的那么“效率低下”,因为编译器会自动调用StringBuilder来帮你实现。如果只是String newString = oldString + "test"这样的语句的话,那么真的没有必要使用StringBuilder实现,在大多数情况下,让“人”舒服比让“机器”舒服更为重要。 ? ? 3.3.String的对象引用机制。每个字符串都会在内存中分配地址,String对象实际上是对内存中字符串的引用。比如我们现在有个String a = "a";那么当我们创建一个新的String对象String b = new String("a");时,并没有在内存中分配新的空间来再存储"a",而是将b的引用也指向了原来"a"在内存中的位置。同样的,当没有对象引用指向"a"时,垃圾回收器会对他进行接管。