一、String类源码
3.实现CharSequence接口,用于表明char值得一个只读的字符序列。此接口对许多不同种类的char序列提供统一的自读访问。
string的本质是char[ ]字符数组,String类只是封装字符串的一些操作的,真是的字符串就是存在其下value这个字符数组中的。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; //字符数组 //private final char[] value; //hash是String实例化的hashcode的一个缓存。字符串的不变性确保了hashcode的值一直是一样的,在需要hashcode时,就不需要每次都计算,这样会很高效。 private int hash; //Default to 0 private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0]; public String() { this.value = "".value; //或者可以写this.value = new char[0];构造里只能初始化长度,不能赋值如: ={'A'}; } public String(char value[]) { this.value = Arrays.copyOf(value, value.length); } //本质是持有一个静态内部类,用于忽略大小写得比较两个字符串。 public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator(); private static class CaseInsensitiveComparator implements Comparator<String>, java.io.Serializable {
public int compare(String s1, String s2) { int n1 = s1.length(); int n2 = s2.length(); int min = Math.min(n1, n2); for (int i = 0; i < min; i++) { char c1 = s1.charAt(i); char c2 = s2.charAt(i); if (c1 != c2) { c1 = Character.toUpperCase(c1); //比较时忽略大小写,同一索引的下标先都转为大写比较,再转为小写比较一次 c2 = Character.toUpperCase(c2); if (c1 != c2) { c1 = Character.toLowerCase(c1); c2 = Character.toLowerCase(c2); if (c1 != c2) { // No overflow because of numeric promotion return c1 - c2; } } } } return n1 - n2; } private Object readResolve() { return CASE_INSENSITIVE_ORDER; } } }
CASE_INSENSITIVE_ORDER 如果需要忽略大写,比较时传入此比较器即可,因为实现了Comparator比较器接口。【Comparator是比较器,实现Comparable的对象自身可以直接使用比较】
或者直接使用来持有这个内部类的公共的静态变量 CASE_INSENSITIVE_ORDER,可以简单得用它来比较两个String,这样当要比较两个String时可以通过这个变量来调用。
并且String类中提供的compareToIgnoreCase方法其实就是调用这个内部类里面的方法实现的。
public int compareToIgnoreCase(String str) { return CASE_INSENSITIVE_ORDER.compare(this, str); }
通过一个String 内部一个static的内部类实现的,那么为什么还要特地写一个内部类呢,这样其实就是为了代码复用,这样在其他情况下也可以使用这个static内部类。
因为String本质就是通过char[]实现的,可以发现length(),isEmpty(),charAt()这些方法其实就是在内部调用数组的方法。
public int length() { return value.length; } public boolean isEmpty() { return value.length == 0; } public char charAt(int index) { if ((index < 0) || (index >= value.length)) { throw new StringIndexOutOfBoundsException(index); } return value[index]; }
将String转化成二进制:本质上是调用了StringCoding.encode()这个静态方法。
public byte[] getBytes() { return StringCoding.encode(value, 0, value.length); }
equals:
首先进行当前对象和要判断的对象引用的是不是同一个对象(==判断引用地址),如果是则返回true。接着判断要判断的对象是不是String类的实例,如果是,接着转化类型判断两个字符串的长度,如果一样,进行char数组[]的逐一比较。
public boolean equals(Object anObject) { if (this == anObject) { //首先进行当前对象和要判断的对象引用的是不是同一个对象 return true; } if (anObject instanceof String) { String anotherString = (String)anObject; //判断要判断的对象是不是String类的实例 int n = value.length; if (n == anotherString.value.length) { //如果两个字符串长度一样,那么一一比较char[] 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; }
hashCode:
hashCode方法可以保证相同的字符串具有相同的hash值。但是hash值相同并不一定是字符串的value值相同。
public int hashCode() { int h = hash; //初始值为0 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; }
jdk1.7 Switch选择表达式对String支持原理:
其实,jdk1.7并没有新的指令来处理switch string,而是通过调用switch中string.hashCode(),将string转换为int类型的hash值。然后用这个Hash值来唯一标识着这个case
。
当匹配的时候,首先调用这个字符串的hashCode()
方法,获取一个Hash值(int类型),用这个Hash值来匹配所有的case
,如果没有匹配成功,说明不存在;如果匹配成功了,接着会调用字符串的equals()
方法进行匹配。因为hashCode相同,字符串的value不一定相同
contentEqauls:
主要是用来比较String和StringBuffer或者StringBuild的内容是否一样。可以看到传入参数是CharSequence ,这也说明了StringBuffer和StringBuild同样是实现了CharSequence。源码中先判断参数是从哪一个类实例化来的,再根据不同的情况采用不同的方案,不过其实大体都是采用上面那个for循环的方式来进行判断两字符串是否内容相同。
public boolean contentEquals(CharSequence cs) { // Argument is a StringBuffer, StringBuilder if (cs instanceof AbstractStringBuilder) { if (cs instanceof StringBuffer) { synchronized(cs) { //如果是线程安全的StringBuffer,那么会使用同步代码锁 return nonSyncContentEquals((AbstractStringBuilder)cs); } } else { //如果是StringBuilder return nonSyncContentEquals((AbstractStringBuilder)cs); } } // Argument is a String //如果被比较的只是Sting类型,那么直接调用equals方法 if (cs instanceof String) { return equals(cs); } // Argument is a generic CharSequence //如果是一个字符序列CharSequence char v1[] = value; int n = v1.length; if (n != cs.length()) { //先比较长度 return false; } for (int i = 0; i < n; i++) { if (v1[i] != cs.charAt(i)) { //在比较每一个字符 return false; } } return true; }
//字符串与字符串缓冲区比较 private boolean nonSyncContentEquals(AbstractStringBuilder sb) { char v1[] = value; char v2[] = sb.getValue(); int n = v1.length; if (n != sb.length()) { return false; } for (int i = 0; i < n; i++) { if (v1[i] != v2[i]) { return false; } } return true; }
compareTo:
这个就是String对Comparable接口中方法的实现了。
先通过比较两个字符串的长度将最小的长度赋值给lim,接着将字符串赋值给两个字符数组,在0~lim范围内进行字符数组的逐一判断,如果有一个不相等则返回两个字符的ASCII码的差值,如果循环结束都相等则返回两个长度的差值。
其核心就是那个while循环,通过从第一个开始比较每一个字符,当遇到第一个较小的字符时,判定该字符串小。
注意:anotherString.value 不报错是因为在本类中本类对象的引用可以使用private变量
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; }
startswith:判断当前字符串是否以某一段其他字符串开始的,和其他字符串比较方法一样,其实就是通过一个while来循环比较。
public boolean startsWith(String prefix, int toffset) { char ta[] = value; int to = toffset; char pa[] = prefix.value; int po = 0; int pc = prefix.value.length; // Note: toffset might be near -1>>>1. if ((toffset < 0) || (toffset > value.length - pc)) { return false; } while (--pc >= 0) { if (ta[to++] != pa[po++]) { return false; } } return true; }
contact:
concat的作用是将str拼接到当前字符串后面,通过代码也可以看出其实就是建一个新的字符串。
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); }
replace和replaceAll:
都是全部替换匹配的字符,而replaceAll是通过正则表达式的方式,替换所有匹配的字符
如果只替换第一个匹配的字符,使用replaceFirst
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; } public String replaceAll(String regex, String replacement) { return Pattern.compile(regex).matcher(this).replaceAll(replacement); }
trim:
字符的比较其实是比较ASCII码值,而空字符对应的值是32,是最小的,比32小那么就是空字符
public String trim() { int len = value.length; int st = 0; char[] val = value; /* avoid getfield opcode */ while ((st < len) && (val[st] <= ' ')) { //去除前缀空字符 st++; } while ((st < len) && (val[len - 1] <= ' ')) { //取出后缀空字符 len--; } return ((st > 0) || (len < value.length)) ? substring(st, len) : this; //根据新的没有空字符的索引来截取原字符串 }
string.valueOf(int i)与Integer.toString(int i)本质上没什么区别:
String: public static String valueOf(int i) { return Integer.toString(i); } Integer: public static String toString(int i) { if (i == Integer.MIN_VALUE) return "-2147483648"; int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i); char[] buf = new char[size]; getChars(i, size, buf); return new String(buf, true); }
二、jdk1.6的subString内存泄漏问题:
在 JDK 1.6 中,java.lang.String 主要由3 部分组成:代表字符数组的value、偏移量offset和长度count
char[] value offset 偏移 count 长度
字符串的实际内容由value、offset 和count 三者共同决定,而非value 一项。如果字符串value 数组包含100 个字符,而count 长度只有1 个字节,那么这个String 实际上只有1 个字符,却占据了至少100 个字节,那剩余的99 个就属于泄漏的部分,它们不会被使用,不会被释放,却长期占用内存,直到字符串本身被回收。可以看到,str 的count 为1,而它的实际取值为字符串“0”,但是在value 的部分,却包含了上万个字节,在这个极端情况中,原本只应该占用1 个字节的String,却占用了上万个字节,因此,可以判定为内存泄漏。
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); } String(int offset, int count, char value[]) { //构造方法只是改变数组的偏移和长度,不是产生新的String对象,所以造成内存泄露 this.value = value; this.offset = offset; this.count = count; }
构造方法只是改变数组的偏移和长度,不是产生新的String对象,引用的还是原字符串,原字符串永远不会被回收,所以造成内存泄露
而在jdk1.7之后:
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); } public String(char value[], int offset, int count) { if (offset < 0) { throw new StringIndexOutOfBoundsException(offset); } if (count <= 0) { if (count < 0) { throw new StringIndexOutOfBoundsException(count); } if (offset <= value.length) { this.value = "".value; return; } } // 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); //复制创建了一个新的字符数组 }
可以看到,在substring()的实现中,最终是使用了String 的构造函数,生成了一个新的String,不会造成内存泄露。
三、字符串拼接
concat:连接string各个字符串,是创建新的String对象(字符数组),只能接受String
+:默认是java的String类的一种重载,将+后面的对象,转换为String类型,然后再进行字符串拼接,其实都是产生了一个新的对象,+可以接其它类型
字符串拼接几种方式的效率:+ < contact < StringBuffer < StringBuilder
String apple = "Apple,";
String fruit = apple + "Pear," + "Orange"
其实底层编译器在执行上述代码的时候会的自动引入 java.lang.StringBuilder 类,上面这个例子中,编译器会创建一个 StringBuilder 对象,用来构造最终要生成的 String,并为每一个字符串调用一次 StringBuilder 中的 append() 方法,因此上述代码一共执行了三次 append() 方法。最后调用 toString 生成最终的结果,并保存为 fruit。
但能使用StringBuilder最好不要用 +,如再循环里 += 连接会在每次循环自动创建一次StringBuilder对象,降低效率