博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
深入研究Java String
阅读量:6371 次
发布时间:2019-06-23

本文共 10286 字,大约阅读时间需要 34 分钟。

开始写 Java 一年来,一直都是遇到什么问题再去解决,还没有主动的深入的去学习过 Java 语言的特性和深入阅读 JDK 的源码。既然决定今后靠 Java

吃饭,还是得花些心思在上面,放弃一些打游戏的时间,系统深入的去学习。

Java String 是 Java 编程中最常用的类之一,也是 JDK 提供的最基础的类。所以我决定先从 String 类入手,深入的研究一番来开个好头。

类定义与类成员

打开 JDK 中的 String 源码,最先应当关注 String 类的定义。

public final class String    implements java.io.Serializable, Comparable
, CharSequence

不可继承与不可变

写过 Java 的人都知道, 当 final 关键字修饰类时,代表此类不可继承。所以 String 类是不能被外部继承。这时候我们可能会好奇,String 的设计者

为什么要把它设计成不可继承的呢。我在知乎上找到了相关的,
我觉得首位的回答已经说的很明白了。String 做为 Java 的最基础的引用数据类型,最重要的一点就是不可变性,所以使用 final 就是为了**禁止继承
破坏了 String 的不可变的性质**。

实现类的不可变性,不光是用 final 修饰类这么简单,从源码中可以看到,String 实际上是对一个字符数组的封装,而字符数组是私有的,并且没有提供

任何可以修改字符数组的方法,所以一旦初始化完成, String 对象便无法被修改。

序列化

从上面的类定义中我们看到了 String 实现了序列化的接口 Serializable,所以 String 是支持序列化和反序列化的。

什么是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     *      * Object Serialization Specification, Section 6.2, "Stream Elements"     */    private static final ObjectStreamField[] serialPersistentFields =        new ObjectStreamField[0];

serialVersionUID 是一个序列化版本号,Java 通过这个 UID 来判定反序列化时的字节流与本地类的一致性,如果相同则认为一致,

可以进行反序列化,如果不同就会抛出异常。

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 和静态字段名 serialPersistentFieldstransient 用于指定哪个字段不被默认序列化,
serialPersistentFields 用于指定哪些字段需要被默认序列化。如果同时定义了 serialPersistentFieldstransienttransient 会被忽略。**
我自己也测试了一下,确实是这个效果。

知道了 serialPersistentFields 的作用以后,问题又来了,既然这个静态字段是用来定义参与序列化的类成员的,那为什么在 String 中这个数组的长度定义为0?

经过一番搜索查找资料以后,还是没有找到一个明确的解释,期待如果有大佬看到能解答一下。

可排序

String 类还实现了 Comparable 接口,Comparable<T>接口只有一个方法 public int compareTo(T o),实现了这个接口就意味着该类支持排序,

即可用 Collections.sortArrays.sort 等方法对该类的对象列表或数组进行排序。

在 String 中我们还可以看到这样一个静态变量,

public static final Comparator
CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator();private static class CaseInsensitiveComparator implements Comparator
, 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 的大小。

那么 ComparableComparator 有什么区别和联系呢?同时 String 又为什么要两个都实现一遍呢?

第一个问题这里就不展开了,总结一下就是,Comparable 是类的内部实现,一个类能且只能实现一次,而 Comparator 则是外部实现,可以通过不改变

类本身的情况下,为类增加更多的排序功能。
所以我们也可以为 String 实现一个 Comparator使用,具体可以参考这篇文章。

String 实现了两种比较方法的意图,实际上是一目了然的。实现 Comparable 接口为类提供了标准的排序方案,同时为了满足大多数排序需求的忽略大小写排序的情况,

String 再提供一个 Comparator 到公共静态类成员中。如果还有其他的需求,那就只能我们自己实现了。

类方法

String 的方法大致可以分为以下几类。

  • 构造方法
  • 功能方法
  • 工厂方法
  • intern方法

关于 String 的方法的解析,已经解析的够好了,所以我这里也不再重复的说一遍了。不过

最后的 intern 方法值得我们去研究。

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));    }}/* outputs1 == s2: falses1.equals(s2): trues1 == s3: trues1.equals(s3): trues2 == s4: falses2.equls(s4): trues5 == s6: trues1 == s8: false */

从这段代码的输出可以看到,equals 比较的结果都是 true,这是因为 String 的 equals 比较的值( Object 对象的默认 equals 实现是比较引用,

String 对此方法进行了重写)。== 比较的是两个对象的引用,如果引用相同则返回 true,否则返回 falses1==s2: falses2==s4: false
说明了 new 一个对象一定会生成一个新的引用返回。s1==s3: true 则证明了使用字面量创建对象同样的字面量会得到同样的引用。

s5 == s6 实际上和 s1 == s3 在 JVM 眼里是一样的情况,因为早在编译阶段,这种常量的简单运算就已经完成了。我们可以使用 javap 反编译一下 class 文件去查看

编译后的情况。

➜ ~ javap -c StringConstPool.classCompiled 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."
":()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."
":()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."
":()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 方法,那么 intern 方法到作用到底是什么呢?首先查看一下源码。

/**     * Returns a canonical representation for the string object.     * 

* A pool of strings, initially empty, is maintained privately by the * class {@code String}. *

* 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. *

* 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}. *

* All literal strings and string-valued constant expressions are * interned. String literals are defined in section 3.10.5 of the * The Java™ Language Specification. * * @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));    }}/* outputs1 == s2: falses1 == s3: true*/

这里就很容易的了解 intern 实际上就是把普通的字符串对象也关联到常量池中。

当然 intern 的实现原理和最佳实践等也是需要理解学习的,美团技术团队的这篇

很深入也很详细,推荐阅读。

转载地址:http://biyqa.baihongyu.com/

你可能感兴趣的文章
上传一张照片,让算法告诉你是否患有抑郁症
查看>>
VR厂商唯晶科技获2800万C+轮融资,曾开发过游戏《圣女之歌》
查看>>
Countly 19.02.1 发布,实时移动和 web 分析报告平台
查看>>
Elasticsearch 公司上市,市值近 50 亿美元
查看>>
TCP连接中time_wait在开发中的影响-搜人以鱼不如授之以渔
查看>>
Oracle数据库机出新帮助不同规模企业迈向云端
查看>>
前端通信:ajax设计方案(六)--- 全局配置、请求格式拓展和优化、请求二进制类型、浏览器错误搜集以及npm打包发布...
查看>>
Android捕获监听Home键、最近任务列表键
查看>>
微服务分布式企业框架 Springmvc+mybatis+shiro+Dubbo+ZooKeeper+Redis+KafKa
查看>>
word2vec原理(三) 基于Negative Sampling的模型
查看>>
被《时代周刊》选为年度最佳发明,PS VR靠的竟然是价格
查看>>
通用唯一标识码UUID的介绍及使用。
查看>>
spring笔记--依赖注入之针对不同类型变量的几种注入方式
查看>>
Java爬虫——网易云热评爬取
查看>>
Ajax的简单学习
查看>>
机器人会消灭人类统治世界?你TM是在逗我么
查看>>
无华为,不智慧:智慧城市建设为何少不了华为?
查看>>
易到网约车许可证到手,终于能卖个好价钱了
查看>>
java是值传递还是引用传递
查看>>
阿里云推出“政务云” 助力政务机构推进互联网+
查看>>