开始写 Java 一年来,一直都是遇到什么问题再去解决,尚未主动的深刻的去学习过 Java 语言的特性和深刻阅读 JDK 的源码。既然决定从此靠 Java 吃饭,仍是得花些心思在上面,放弃一些打游戏的时间,系统深刻的去学习。html
Java String 是 Java 编程中最经常使用的类之一,也是 JDK 提供的最基础的类。因此我决定先从 String 类入手,深刻的研究一番来开个好头。java
打开 JDK 中的 String 源码,最早应当关注 String 类的定义。git
public final class String implements java.io.Serializable, Comparable<String>, CharSequence 复制代码
写过 Java 的人都知道, 当 final
关键字修饰类时,表明此类不可继承。因此 String 类是不能被外部继承。这时候咱们可能会好奇,String 的设计者 为何要把它设计成不可继承的呢。我在知乎上找到了相关的问题和讨论, 我以为首位的回答已经说的很明白了。String 作为 Java 的最基础的引用数据类型,最重要的一点就是不可变性,因此使用 final
就是为了禁止继承 破坏了 String 的不可变的性质。github
实现类的不可变性,不光是用 final
修饰类这么简单,从源码中能够看到,String 其实是对一个字符数组的封装,而字符数组是私有的,而且没有提供 任何能够修改字符数组的方法,因此一旦初始化完成, String 对象便没法被修改。express
从上面的类定义中咱们看到了 String 实现了序列化的接口 Serializable
,因此 String 是支持序列化和反序列化的。 什么是Java对象的序列化?相信不少和我同样的 Java 菜鸟都有这样疑问。深刻分析Java的序列化与反序列化这篇文章中的这一段话 解释的很好。编程
Java平台容许咱们在内存中建立可复用的Java对象,但通常状况下, 只有当JVM处于运行时,这些对象才可能存在, 即,这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中, 就可能要求在JVM中止运行以后可以保存(持久化)指定的对象,并在未来从新读取被保存的对象。 Java对象序列化就可以帮助咱们实现该功能。 使用Java对象序列化,在保存对象时,会把其状态保存为一组字节,在将来,再将这些字节组装成对象。 必须注意地是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量。 除了在持久化对象时会用到对象序列化以外,当使用RMI(远程方法调用),或在网络中传递对象时,都会用到对象序列化。 Java序列化API为处理对象序列化提供了一个标准机制,该API简单易用。数组
在 String 源码中,咱们也能够看到支持序列化的类成员定义。网络
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
/** * Class String is special cased within the Serialization Stream Protocol. * * A String instance is written into an ObjectOutputStream according to * <a href="{@docRoot}/../platform/serialization/spec/output.html"> * Object Serialization Specification, Section 6.2, "Stream Elements"</a> */
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
复制代码
serialVersionUID
是一个序列化版本号,Java 经过这个 UID 来断定反序列化时的字节流与本地类的一致性,若是相同则认为一致, 能够进行反序列化,若是不一样就会抛出异常。app
serialPersistentFields
这个定义则比上一个少见许多,大概猜到是与序列化时的类成员有关系。为了弄懂这个字段的意义,我 google 百度齐上,也 仅仅只找到了 JDK 文档对类 ObjectStreamField
的一丁点描述, A description of a Serializable field from a Serializable class. An array of ObjectStreamFields is used to declare the Serializable fields of a class.
大意是这个类用来描述序列化类的一个序列化字段, 若是定义一个此类的数组则能够声明类须要被序列化的字段。可是仍是没有找到这个类的具体用法和做用是怎样的。后来我仔细看了一下这个字段的定义, 与 serialVersionUID
应该是一样经过具体字段名来定义各类规则的,而后我直接搜索了关键字 serialPersistentFields
,终于找到了它的具体做用。 即,默认序列化自定义包括关键字 transient
和静态字段名 serialPersistentFields
,transient
用于指定哪一个字段不被默认序列化, serialPersistentFields
用于指定哪些字段须要被默认序列化。若是同时定义了 serialPersistentFields
与 transient
,transient
会被忽略。 我本身也测试了一下,确实是这个效果。性能
知道了 serialPersistentFields
的做用之后,问题又来了,既然这个静态字段是用来定义参与序列化的类成员的,那为何在 String 中这个数组的长度定义为0? 通过一番搜索查找资料之后,仍是没有找到一个明确的解释,期待若是有大佬看到能解答一下。
String 类还实现了 Comparable
接口,Comparable<T>
接口只有一个方法 public int compareTo(T o)
,实现了这个接口就意味着该类支持排序, 便可用 Collections.sort
或 Arrays.sort
等方法对该类的对象列表或数组进行排序。
在 String 中咱们还能够看到这样一个静态变量,
public static final Comparator<String> CASE_INSENSITIVE_ORDER
= new CaseInsensitiveComparator();
private static class CaseInsensitiveComparator implements Comparator<String>, java.io.Serializable {
// use serialVersionUID from JDK 1.2.2 for interoperability
private static final long serialVersionUID = 8575799808933029326L;
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;
}
/** Replaces the de-serialized object. */
private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
}
复制代码
从上面的源码中能够看出,这个静态成员是一个实现了 Comparator
接口的类的实例,而实现这个类的做用是比较两个忽略大小写的 String 的大小。
那么 Comparable
和 Comparator
有什么区别和联系呢?同时 String 又为何要两个都实现一遍呢?
第一个问题这里就不展开了,总结一下就是,Comparable
是类的内部实现,一个类能且只能实现一次,而 Comparator
则是外部实现,能够经过不改变 类自己的状况下,为类增长更多的排序功能。 因此咱们也能够为 String 实现一个 Comparator
使用,具体能够参考Comparable与Comparator的区别这篇文章。
String 实现了两种比较方法的意图,其实是一目了然的。实现 Comparable
接口为类提供了标准的排序方案,同时为了知足大多数排序需求的忽略大小写排序的状况, String 再提供一个 Comparator
到公共静态类成员中。若是还有其余的需求,那就只能咱们本身实现了。
String 的方法大体能够分为如下几类。
关于 String 的方法的解析,这篇文章已经解析的够好了,因此我这里也再也不重复的说一遍了。不过 最后的 intern 方法值得咱们去研究。
String 作为 Java 的基础类型之一,可使用字面量的形式去建立对象,例如 String s = "hello"
。固然也可使用 new
去建立 String 的对象, 可是几乎不多看到这样的写法,长此以往我便习惯了第一种写法,可是殊不知道背后大有学问。下面一段代码能够看出他们的区别。
public class StringConstPool {
public static void main(String[] args) {
String s1 = "hello world";
String s2 = new String("hello world");
String s3 = "hello world";
String s4 = new String("hello world");
String s5 = "hello " + "world";
String s6 = "hel" + "lo world";
String s7 = "hello";
String s8 = s7 + " world";
System.out.println("s1 == s2: " + String.valueOf(s1 == s2) );
System.out.println("s1.equals(s2): " + String.valueOf(s1.equals(s2)));
System.out.println("s1 == s3: " + String.valueOf(s1 == s3));
System.out.println("s1.equals(s3): " + String.valueOf(s1.equals(s3)));
System.out.println("s2 == s4: " + String.valueOf(s2 == s4));
System.out.println("s2.equals(s4): " + String.valueOf(s2.equals(s4)));
System.out.println("s5 == s6: " + String.valueOf(s5 == s6));
System.out.println("s1 == s8: " + String.valueOf(s1 == s8));
}
}
/* output s1 == s2: false s1.equals(s2): true s1 == s3: true s1.equals(s3): true s2 == s4: false s2.equls(s4): true s5 == s6: true s1 == s8: false */
复制代码
从这段代码的输出能够看到,equals
比较的结果都是 true,这是由于 String 的 equals
比较的值( Object 对象的默认 equals
实现是比较引用, String 对此方法进行了重写)。==
比较的是两个对象的引用,若是引用相同则返回 true
,不然返回 false
。s1==s2: false
和 s2==s4: false
说明了 new
一个对象必定会生成一个新的引用返回。s1==s3: true
则证实了使用字面量建立对象一样的字面量会获得一样的引用。
s5 == s6
实际上和 s1 == s3
在 JVM 眼里是同样的状况,由于早在编译阶段,这种常量的简单运算就已经完成了。咱们可使用 javap
反编译一下 class 文件去查看 编译后的状况。
➜ ~ javap -c StringConstPool.class
Compiled from "StringConstPool.java"
public class io.github.jshanet.thinkinginjava.constpool.StringConstPool {
public io.github.jshanet.thinkinginjava.constpool.StringConstPool();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String hello world
2: astore_1
3: return
}
复制代码
看不懂汇编也不要紧,由于注释已经很清楚了......
s1 == s8
的状况就略复杂,s8 是经过变量的运算而得,因此没法在编译时直接算出其值。而 Java 又不能重载运算符,因此咱们在 JDK 的源码里也 找不到相关的线索。万事不绝反编译,咱们再经过反编译看看实际上编译器对此是否有影响。
public class io.github.jshanet.thinkinginjava.constpool.StringConstPool {
public io.github.jshanet.thinkinginjava.constpool.StringConstPool();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String hello
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #6 // String world
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_2
23: return
}
复制代码
经过反编译的结果能够发现,String 的变量运算实际上在编译后是由 StringBuilder
实现的,s8 = s7 + " world"
的代码等价于 (new StringBuilder(s7)).append(" world").toString()
。Stringbuilder
是可变的类,经过 append
方法 和 toString
将两个 String 对象聚合 成一个新的 String 对象,因此到这里就不难理解为何 s1 == s8 : false
了。
之因此会有以上的效果,是由于有字符串常量池的存在。字符串对象的分配和其余对象同样是要付出时间和空间代价,而字符串又是程序中最经常使用的对象,JVM 为了提升性能和减小内存占用,引入了字符串的常量池,在使用字面量建立对象时, JVM 首先会去检查常量池,若是池中有现成的对象就直接返回它的引用,若是 没有就建立一个对象,并放到池里。由于字符串不可变的特性,因此 JVM 不用担忧多个变量引用同一个对象会改变对象的状态。同时运行时实例建立的全局 字符串常量池中有一个表,老是为池中的每一个字符串对象维护一个引用,因此这些对象不会被 GC 。
上面说了不少都没有涉及到主题 intern
方法,那么 intern
方法到做用究竟是什么呢?首先查看一下源码。
/** * Returns a canonical representation for the string object. * <p> * A pool of strings, initially empty, is maintained privately by the * class {@code String}. * <p> * When the intern method is invoked, if the pool already contains a * string equal to this {@code String} object as determined by * the {@link #equals(Object)} method, then the string from the pool is * returned. Otherwise, this {@code String} object is added to the * pool and a reference to this {@code String} object is returned. * <p> * It follows that for any two strings {@code s} and {@code t}, * {@code s.intern() == t.intern()} is {@code true} * if and only if {@code s.equals(t)} is {@code true}. * <p> * All literal strings and string-valued constant expressions are * interned. String literals are defined in section 3.10.5 of the * <cite>The Java™ Language Specification</cite>. * * @return a string that has the same contents as this string, but is * guaranteed to be from a pool of unique strings. */
public native String intern();
复制代码
Oracle JDK 中,intern
方法被 native
关键字修饰而且没有实现,这意味着这部分到实现是隐藏起来了。从注释中看到,这个方法的做用是若是常量池 中存在当前字符串,就会直接返回当前字符串,若是常量池中没有此字符串,会将此字符串放入常量池中后再返回。经过注释的介绍已经能够明白这个方法的做用了, 再用几个例子证实一下。
public class StringConstPool {
public static void main(String[] args) {
String s1 = "hello";
String s2 = new String("hello");
String s3 = s2.intern();
System.out.println("s1 == s2: " + String.valueOf(s1 == s2));
System.out.println("s1 == s3: " + String.valueOf(s1 == s3));
}
}
/* output s1 == s2: false s1 == s3: true */
复制代码
这里就很容易的了解 intern
实际上就是把普通的字符串对象也关联到常量池中。
固然 intern
的实现原理和最佳实践等也是须要理解学习的,美团技术团队的这篇深刻解析String#intern 很深刻也很详细,推荐阅读。