java.lang.String (JDK1.8)
String类实现了java.io.Serializable, Comparable<String>, CharSequence这三个interface。
看了下这三个interface中的方法,发现CharSequence中在1.8版本jdk中新增了两个方法:
1 public default IntStream chars(){...} 2 public default IntStream codePoints() {...}
注意这两个方法是在interface内定义的,并且有方法实现的,interesting!而且还可以看到String并没有实现这两个方法。
这一系列的不可思议其实是Java 8 的新特性。default是Java 8 的新关键字。
摘抄一段网上的翻译(未找到英文原文~~)
因为接口有这个语法限制,所以要直接改变/扩展接口内的方法变得非常困难。我们在尝试强化Java 8 Collections API,让其支持lambda表达式的时候,就面临了这样的挑战。为了克服这个困难,Java 8中引入了一个新的概念,叫做default方法,也可以称为Defender方法,或者虚拟扩展方法(Virtual extension methods)。 Default方法是指,在接口内部包含了一些默认的方法实现(也就是接口中可以包含方法体,这打破了Java之前版本对接口的语法限制),从而使得接口在进行扩展的时候,不会破坏与接口相关的实现类代码。
来源:Java 8新特性——default方法(defender方法)介绍
有了default关键字,interface内部也可以定义方法了,于是便引入了一个C++ 中的菱形继承问题(多继承问题)。
1 interface A {} 2 interface B {}
3 class C implements A,B {}
如果A和B接口中都实现了一个同名的default方法,如果C类的对象不调用这个方法,则不会出问题,一旦调用了,则会出现Conflicting Exception异常,因为系统无法判断该使用那个interface中的default方法。
回到CharSequence接口中定义的这两个default方法,这两个方法的定义都是为了支持Java 8 的stream新特性的。chars方法返回一个char的intstream,codepoint方法返回unicode codepoint的intstream。之后在研究下Java 8 stream特性再来理解此处。
String类拥有四个成员
1 private final char value[]; 2 private int hash; // Default to 0 3 private static final long serialVersionUID = -6849794470754667710L; 4 private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
从这四个成员不难看出String的本质其实是char[] 字符数组,需要注意的是这个字符数组是final的。final可以修饰类、方法和变量,含义各不相同。当修饰变量的时候意味着该变量一旦被初始化后将不能修改它的值,该变量只能创建和删除,不能修改。
其次是一个int型的hash,表示的是该字符串的hash code。后面serialVersionUID用于序列化指定UID的。剩下的ObjectStreamField数组暂时不明白啥意思,官方解释是Serializable类的Serializable字段的描述。 ObjectStreamFields的数组用于声明一个类的Serializable字段。
接下来是String的构造函数。
默认构造函数构造出一个空的字符串,不是null。
其中有个构造函数如下:
1 public String(byte bytes[], int offset, int length, String charsetName)
这个构造函数是将byte数组按照charsetName进行解码得到字符串,通常会用到UTF-8编码,然而UTF-8编码也有很多种写法,比如:UTF-8, UTF8,UTF_8, utf-8, utf8, utf_8, uTf-8, Utf8等等,这些到底是否能被支持呢?写了个例子发现UTF_8和utf_8不支持,其余都支持(如果使用Intellij idea则对于不支持的UTF_8这种格式会显示为红色),但是为啥能支持这么多乱七八糟的写法呢?跟进去最关键的函数是Charset类中的lookup2函数:
1 private static Charset lookup(String charsetName) { 2 if (charsetName == null) 3 throw new IllegalArgumentException("Null charset name"); 4 Object[] a; 5 // 先从一级缓存中寻找 6 if ((a = cache1) != null && charsetName.equals(a[0])) 7 return (Charset)a[1]; 8 // We expect most programs to use one Charset repeatedly. 9 // We convey a hint to this effect to the VM by putting the 10 // level 1 cache miss code in a separate method. 11 return lookup2(charsetName); 12 } 13 14 private static Charset lookup2(String charsetName) { 15 Object[] a; 16 // 一级缓存未命中,再查找二级缓存 17 if ((a = cache2) != null && charsetName.equals(a[0])) { 18 cache2 = cache1; 19 cache1 = a; 20 return (Charset)a[1]; 21 } 22 // 仍然未命中,则去支持的字符集库中去查找 23 Charset cs; 24 // private static CharsetProvider standardProvider = new StandardCharsets(); 25 // standardProvider其实是个StandardCharsets,StandardCharsets这个类里面定义了所有支持的标准字符集,并且包含其别名 26 // StandardCharsets继承自FastCharsetProvider,因此最终是要调用FastCharsetProvider类的lookup函数,该函数第一句就是toLower(var1),然后再去StandardCharsets中定义的几个hashmap中去寻找。 27 if ((cs = standardProvider.charsetForName(charsetName)) != null || 28 (cs = lookupExtendedCharset(charsetName)) != null || 29 (cs = lookupViaProviders(charsetName)) != null) 30 { 31 cache(charsetName, cs); 32 return cs; 33 } 34 35 /* Only need to check the name if we didn't find a charset for it */ 36 checkName(charsetName); 37 return null; 38 }
字符集默认的是ISO-8859-1,UTF-8兼容ISO-8859-1字符集。谈到字符集又是一堆内容,本文略过。
1 public boolean equals(Object anObject)
equals函数是比较两个字符串内容是否相同的一个函数,归根到底比较的是char[],而"=="则比较的是两个String的引用地址是否相同。
1 public boolean contentEquals(StringBuffer sb) 2 private boolean nonSyncContentEquals(AbstractStringBuilder sb) 3 public boolean contentEquals(CharSequence cs)
这三个函数中前两个函数是用于比较当前字符串与给出的StringBuffer、StringBuilder的内容是否一致。因为StringBuffer是线程安全的,因此比较过程中也是加了同步synchronized, 而StringBuilder则是非线程安全的,比较过程就没有同步锁了。
1 public boolean equalsIgnoreCase(String anotherString)
equalsIgnoreCase也是很奇怪,字符比较过程中,先直接比较,再将两个字符都转为大写进行比较,然后为了支持Georgian alphabet,再将两个字符都转为小写进行比较,真是神奇啊。
1 public int hashCode() { 2 int h = hash; 3 if (h == 0 && value.length > 0) { 4 char val[] = value; 5 6 for (int i = 0; i < value.length; i++) { 7 h = 31 * h + val[i]; 8 } 9 hash = h; 10 } 11 return h; 12 }
此为hashCode计算方法:s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1],为什么用31作为乘数,可以去查看Why does Java's hashCode() in String use 31 as a multiplier? 其解释如下
The value 31 was chosen because it is an odd prime. If it were even and the multiplication overflowed, information would be lost, as multiplication by 2 is equivalent to shifting. The advantage of using a prime is less clear, but it is traditional. A nice property of 31 is that the multiplication can be replaced by a shift and a subtraction for better performance: 31 * i == (i << 5) - i. Modern VMs do this sort of optimization automatically. (from Chapter 3, Item 9: Always override hashcode when you override equals, page 48)
1 public int indexOf(int ch, int fromIndex) 2 private int indexOfSupplementary(int ch, int fromIndex)
indexof用于在String中查找ch,实现过程中区分待查找的字符的code point是一个字节还是两个字节,如果字符的code point 小于 0x010000则是一个字节的字符了,只需要比较单个字节就好;否则需要比较两个字节。
补充code point概念(来源:代码点(Code Point)和代码单元(Code Unit)):
代码点(Code Point):Unicode是属于编码字符集(CCS)的范围。Unicode所做的事情就是将我们需要表示的字符表中的每个字符映射成一个数字,这个数字被称为相应字符的码点(code point)。例如“严”字在Unicode中对应的码点是U+0x4E25。 代码点是字符集被编码后出现的概念。字符集(Code Set)是一个集合,集合中的元素就是字符,比如ASCII字符集,其中的字符就是'A'、'B'等字符。为了在计算机中处理字符集,必须把字符集数字化,就是给字符集中的每一个字符一个编号,计算机程序中要用字符,直接用这个编号就可以了。于是就出现了编码后的字符集,叫做编码字符集(Coded Code Set)。编码字符集中每一个字符都和一个编号对应。那么这个编号就是代码点(Code Point)。 码元(Code Unit)是指一个已编码的文本中具有最短的比特组合的单元。对于 UTF-8 来说,码元是 8 比特长;对于 UTF-16 来说,码元是 16 比特长。换一种说法就是 UTF-8 的是以一个字节为最小单位的,UTF-16 是以两个字节为最小单位的。换一种说法就是UTF-8的是以一个字节为最小单位的,UTF-16是以两个字节为最小单位的。 代码单元是把代码点存放到计算机后出现的概念。一个字符集,比如有10个字符,每一个字符从0到9依次编码。那么代码点就是0、1、。。。、9。为了在计算机中存储这10个代代码点,一个代码点给一个字节,那么这里的一个字节就是一个代码单元。比如Unicode是一个编码字符集,其中有65536个字符,代码点依次为0、1、2、。。。、65535,为了在计算机中表示这些代码点就出现了代码单元,65536个代码点为了统一表示每个代码点必须要有两个字节表示才行。但是为了节省空间0-127的ASCII码就可以不用两个字节来表示,只需要一个字节,于是不同的表示方案就形成了不同的编码方案,比如utf-8、utf-16等。对utf-8而言代码单元就是一个字节,对utf-16而言代码单元就是两个字节。
1 public int lastIndexOf(int ch, int fromIndex) 2 private int lastIndexOfSupplementary(int ch, int fromIndex)
这两个函数和上面的indexOf、indexOfSupplementary有点类似,一个是从前往后找,找到第一个返回;这两个函数则是从后往前找,找到第一个返回。
1 int i = Math.min(fromIndex, value.length - 1)
这里面有个问题就是起始查找的位置是取fromIndex和value.length -1 (或者value.length - 2),这样可以避免fromIndex越界问题。
1 static int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)
这个函数一开始就对fromIndex做了两次容错,考虑的比较全面。算法复杂度O(sourceCount * targetCount)
1 static int lastIndexOf(char[] source, int sourceOffset, int sourceCount, 2 char[] target, int targetOffset, int targetCount, 3 int fromIndex) { 4 // ...... 5 6 startSearchForLastChar: 7 while (true) { 8 while (i >= min && source[i] != strLastChar) { 9 i--; 10 } 11 if (i < min) { 12 return -1; 13 } 14 int j = i - 1; 15 int start = j - (targetCount - 1); 16 int k = strLastIndex - 1; 17 18 while (j > start) { 19 if (source[j--] != target[k--]) { 20 i--; 21 continue startSearchForLastChar; 22 } 23 } 24 return start - sourceOffset + 1; 25 } 26 }
在这个函数中使用到了label语法,continue label终止当前循环,继续上层循环。lastIndexOf中查找也是从后往前查找的。
1 public String substring(int beginIndex) 2 public String substring(int beginIndex, int endIndex)
因为String的本质是final char[],因此substring中都是通过拷贝字符串中的字符创建出新的字符串的方式实现的
1 public String replace(char oldChar, char newChar) 2 public String replaceAll(String regex, String replacement)
replace是将字符串中所有的oldChar替换为newChar。replaceAll也具备这个功能,同时它还支持正则表达式。
1 public String[] split(String regex, int limit) { 2 /* fastpath if the regex is a 3 (1)one-char String and this character is not one of the 4 RegEx's meta characters ".$|()[{^?*+\\", or 5 (2)two-char String and the first char is the backslash and 6 the second is not the ascii digit or ascii letter. 7 */ 8 char ch = 0; 9 if (((regex.value.length == 1 && 10 ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) || 11 (regex.length() == 2 && 12 regex.charAt(0) == '\\' && 13 (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 && 14 ((ch-'a')|('z'-ch)) < 0 && 15 ((ch-'A')|('Z'-ch)) < 0)) && 16 (ch < Character.MIN_HIGH_SURROGATE || 17 ch > Character.MAX_LOW_SURROGATE)) 18 { 19 int off = 0; 20 int next = 0; 21 boolean limited = limit > 0; 22 ArrayList<String> list = new ArrayList<>(); 23 while ((next = indexOf(ch, off)) != -1) { 24 if (!limited || list.size() < limit - 1) { 25 list.add(substring(off, next)); 26 off = next + 1; 27 } else { // last one 28 //assert (list.size() == limit - 1); 29 list.add(substring(off, value.length)); 30 off = value.length; 31 break; 32 } 33 } 34 // If no match was found, return this 35 if (off == 0) 36 return new String[]{this}; 37 38 // Add remaining segment 39 if (!limited || list.size() < limit) 40 list.add(substring(off, value.length)); 41 42 // 当limit = 0的时候,结尾的空字符串将不会包含在返回数组中 43 // Construct result 44 int resultSize = list.size(); 45 if (limit == 0) { 46 while (resultSize > 0 && list.get(resultSize - 1).length() == 0) { 47 resultSize--; 48 } 49 } 50 String[] result = new String[resultSize]; 51 return list.subList(0, resultSize).toArray(result); 52 } 53 return Pattern.compile(regex).split(this, limit); 54 }
这个split函数也是比较有意思,下面四行测试代码返回结果是不同的,从代码上来看,如果limit参数为0,则会将结尾的空字符串排除在返回的数组之外,因此",,a,b,c,d,,,,".split(",").length结果是6。而",,a,b,c,d,,,,".split(",", -1).length中因为limit参数为-1,则不会进行重整返回数组操作,结果就是我们通常理解的10了。如果limit > 0, 比如为5,则最终的返回数组长度必定是不大于5的,split次数为5-1=4次。
1 ",,a,b,c,d,,,,".split(",").length = 6 2 ",,a,b,c,d,,,,".split(",", -1).length = 10 3 ",,a,b,c,d,,,,".split(",", 5).length = 5 4 ",,a,b,c,d,,,,".split(",", 20).length = 10
1 public static String join(CharSequence delimiter, CharSequence... elements) 2 public static String join(CharSequence delimiter, Iterable<? extends CharSequence> elements)
1.8版本jdk增加了两个join函数,用于将多个字符串按照分隔符delimiter进行重组。
1 public String trim() { 2 int len = value.length; 3 int st = 0; 4 char[] val = value; /* avoid getfield opcode */ 5 6 while ((st < len) && (val[st] <= ' ')) { 7 st++; 8 } 9 while ((st < len) && (val[len - 1] <= ' ')) { 10 len--; 11 } 12 return ((st > 0) || (len < value.length)) ? substring(st, len) : this; 13 }
trim用于将字符串开头结尾的空白字符都去掉,注意在源码中采用的是小于或等于' '字符的都去掉,查了下ASCII码表,空格字符以下的字符包括\n \r \t \f \b \0等空白字符。
1 public native String intern();
最后还有个intern函数,这个函数是个native函数,测试用例:
1 String str1 = "a"; 2 String str2 = "b"; 3 String str3 = "ab"; 4 String str4 = "a" + "b"; 5 String str5 = str1 + str2; 6 String str6 = new String("ab"); 7 8 System.out.println(str4 == str3); // true 9 System.out.println(str5 == str3); // false 10 System.out.println(str6 == str3); // false 11 System.out.println(str4.intern() == str3); // true 12 System.out.println(str5.intern() == str3); // true 13 System.out.println(str6.intern() == str3); // true
String.intern()方法是一种手动将字符串加入常量池中的方法,当调用该方法时str.intern(),JVM就会在当前类的常量池中查找是否存在与str等值的String,若存在则直接返回常量池中相应Strnig的引用;若不存在,则会在常量池中创建一个等值的String,然后返回这个String在常量池中的引用(Java7, 8中会直接在常量池中保存当前字符串的引用)。因此,只要是等值的String对象,使用intern()方法返回的都是常量池中同一个String引用,所以,这些等值的String对象通过intern()后使用==是可以匹配的。(Java7中会直接在常量池中保存当前字符串的引用)。
另外要注意一点,str.intern()并不会改变str的地址,只会返回该字符串在常量池中的地址,如果不存在则jdk6拷贝一份放到常量池返回常量池中该字符串引用;jdk7和jdk8因为常量池就在堆内,因此是将该字符串的引用放入到常量池内。
再来看下面的两段代码:
1 public static void test3() { 2 String s3 = new String("1") + new String("1"); 3 String s4 = "11"; 4 s3.intern(); 5 System.out.println(s3 == s4); 6 System.out.println(s3.intern() == s4); 7 } 8 9 public static void test4() { 10 String s3 = new String("1") + new String("1"); 11 s3.intern(); 12 String s4 = "11"; 13 System.out.println(s3 == s4); 14 System.out.println(s3.intern() == s4); 15 }
这两段代码输出结果都是false、true。因为str.intern()并不会改变str的地址,因此s3==s4是不可能相等的。
上面一段代码常量池中因为声明了变量s4,导致"11"这个字符串被存储到常量池中,s3.intern()返回的也是常量池中"11"这个字符串的引用。
下面一段代码则不同,因为在常量池中还没有"11"这个字符串的时候就调用了s3.intern(),对于jdk7和jdk8而言,此时会将"11"存入到常量池中,但是s3地址未变(存入常量池中的“11”和s3地址是不同的),之后声明的变量s4是常量池"11"的引用,因此s4和s3不等。s3.intern()指向的同样是常量池中“11”,所以s3.intern()==s4。
参考连接:
https://www.zybuluo.com/pastqing/note/55097
http://www.importnew.com/7302.html
http://www.codeceo.com/article/java-8-default-method.html
http://www.cnblogs.com/zhangzl419/archive/2013/05/21/3090601.html
http://www.jianshu.com/p/95f516cb75ef
https://tech.meituan.com/in_depth_understanding_string_intern.html
https://stackoverflow.com/questions/299304/why-does-javas-hashcode-in-string-use-31-as-a-multiplier/299748