陈同学
微服务
Accelerator
About
# Java基础之String你了解多少? String是Java开发中使用非常频繁的类,本文将对String的源码和设计进行探索。 关键字:java、String、immutable、intern、hash > JDK1.0 ```java public final class String implements java.io.Serializable, Comparable<String>, CharSequence { private final char value[]; private int hash; // Default to 0 } ``` 直观来看,String及成员变量存在以下特性: * String被final修饰,即String不可被继承 * String的实际存储为字符数组`value`,且value被final修饰,即value指向的数组引用不可变更 ## 了解不可变(immutable)对象 ### 什么是不可变对象? 如果一个对象在创建之后,不能再改变它的状态,那么这个对象就是不可变的。 状态指对象的成员变量,有以下情况: * 成员变量是基础数据类型,那么其值不可改变 * 成员变量是引用类型,那么引用指向的对象不可改变(引用不能指向其他对象,但对象本身还是可以改变的) ### 为什么要设计不可变对象? immutable对象存在以下几个优点: * 线程安全,因为不可改变(read-only),因此可以被多个线程安全使用。 * 基于线程安全特性,可以在任何地方重复使用,提高性能。 * 保证了hashcode的唯一性,如果缓存hashcode,在频繁使用对象时可以不用重复计算hashcode ### 怎么创建一个不可变对象? 本质上就是不提供任何方式改变对象的状态,以下是一些细节: * 使用final修饰类,确保没有任何子类可以变更其不可变的特性 * 成员变量使用final修饰 * 不提供任何改变对象状态的方法 * 若对对象进行修改,使用拷贝的方式,不要返回对象本身 ##为什么String要设计成immutable? > [Why do we need immutable class?](https://stackoverflow.com/questions/3769607/why-do-we-need-immutable-class) > > 这个问题应该拓展至一门语言为什么需要设计immutable数据类型?为什么基础数据类型往往都是immutable的? 程序是通过代码逻辑操纵内存中的数据并最终持久化下来。内存中的数据呈现显示为各程序设计语言中的数据类型,而持久化的存储可以是DB、磁盘等。 在程序运行时,数据会在程序中不断进行传递和变更,将基础数据类型设计成immutable类型可以使得软件构建变更容易,因为不用担心数据会在处理过程中发生变更。 Java中为8中基础数据类型和String都设计了缓存池。 ## String对象真的不可变吗? 因为String对象实际存储是字符串数组,虽然无法直接变更引用所指向的对象,但是可以直接变更对象。 也就是说:`String对象的引用不可变,但是引用指向的对象可以被改变` ```java public static void main(String[] args) throws Exception { // 创建字符串"hello", 将引用s指向对象"hello". s用final修饰,无法显示将s指向其他对象 final String s = "hello"; System.out.println(s.hashCode()); // 获取String对象中的value字段 Field valueFiled = String.class.getDeclaredField("value"); // 将value字段设置为可访问的 valueFiled.setAccessible(true); // 通过反射获取s引用指向的对象的"value"字段的值 char[] value = (char[]) valueFiled.get(s); // 变更对象"hello"中的第一个字符为"H", s引用还是指向这个对象 value[0] = 'H'; System.out.println(s); System.out.println(s.hashCode()); } ``` 输入的结果: 通过反射变更了实际的对象,但是没有改变引用值,引用还是指向这个对象。由于String缓存了hashcode,所以即使值变了,但是hashcode却没变。 ``` 99162322 Hello 99162322 ``` ## 设计字符串常量池的意义在哪? > [Guide to Java String Pool](http://www.baeldung.com/java-string-pool) > > [What are the benefits of string pool in Java?](https://www.quora.com/What-are-the-benefits-of-string-pool-in-Java) 程序开发中涉及到许多池的概念,如:线程池、数据库连接池、字符串池、Spring容器管理的单例对象以及其它的一些Buffer设计等。设计这些概念并实现,无非是出于提高性能、节约资源(如内存)之类的考虑。 由于String是immutable对象,天然的具备线程安全特性,因为可以作为全局共享对象。 最为重要的是,String是Java中使用最为广泛的类型,与8大基础数据类型并列。实际应用中,往往String的使用更为频繁。因此设计常量池缓存String对象,可以带来如下几个好处: * 节约内存,遇到大量重复String时节约内存 * 提高效率,遇到大量重复String时不用频繁创建对象 ## 字符串常量池在不同JDK版本中如何存储? * JDK1.6以前 常量池位于方法区中的Perm区 * jdk1.7以后 常量池位于堆中,JDK1.8时JVM内存模型已经移除了Perm区,由Metaspace代替 ## String、StringBuilder、StringBuffer的区别? ### StringBuilder * 利用char[]作为buffer存储数据,每次append时都是通过System.arraycopy做数据拷贝 * char[]的长度默认16,不够时将会自动扩容 * 非线程安全,适合单线程下字符串拼接场景 ### StringBuffer 特性和StringBuilder一样,但是每个操作方法中都用了`synchronized`做同步处理 适合多线程下字符串拼接场景 ## String其他的有趣点 ### intern()函数的作用? > JDK源码注释:When the intern method is invoked, if the pool already contains a string equal to this object as determined by the method,then the string from the pool is returned;Otherwise, this object is added to the pool and a reference to this object is returned. > > 当调用intern方法时时,如果常量池已存在该字符串,则返回其引用;否则先将该字符串加入常量池,再返回其引用。 `特别注意:JDK1.6及以下和JDK及以上版本对于intern的处理逻辑有变化。` 假设:str为指向堆中字符串实例的引用,且String Pool中不存在值相同的字符串实例 * JDK1.6:调用str.intern()时,若str在String Pool不存在,将拷贝一份到String Pool * JDK1.7:调用str.intern()时,若str在String Pool不存在,String Pool将存储堆中对象的引用 下面通过一段代码加两张图来解释下: ```java String s1 = new String("H") + new String("i"); String s2 = s1.intern(); String s3 = "Hi"; System.out.println(s1 == s2); System.out.println(s1 == s3); System.out.println(s2 == s3); ``` ![jdk1.6](https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2018/04/09/2abf4ce84b1e4e9c9b9b5efb5a58d73b.png) ![jdk1.7](https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2018/04/09/6c2fe3aca1434d60a15b4449c946d529.png) 执行s1.intern()时,String Pool也只会保存一个指向堆中对象的引用,不再像1.6那样复制一个对象实例到String Pool中 ### 理解String的加法运算 开发中经常会碰到String变量之间的加法操作,那JVM实际上是如何处理的呢? 下面看一个例子: ```java String s1 = "Hello"; String s2 = "Kitty"; String s3 = s1 + s2; System.out.println(s3 == "HelloKitty"); // false ``` `s3 = s1 + s2` 到底会怎么处理?我们看下这段代码对应的字节码指令: ``` 0: ldc #2 // String Hello 2: astore_1 3: ldc #3 // String Kitty 5: astore_2 6: new #4 // class java/lang/StringBuilder 9: dup 10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V 13: aload_1 14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 17: aload_2 18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 24: astore_3 25: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream; 28: aload_3 29: ldc #9 // String HelloKitty 31: if_acmpne 38 34: iconst_1 35: goto 39 38: iconst_0 39: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V ``` 本质上,String变量的加法运算是通过StringBuilder来处理。s3 = s1 + s2实际对应的代码应该是: ```java new StringBuilder().append("Hello").append("Kitty").toString(); ``` 而StringBuilder的toString()源码为: ```java public String toString() { // Create a copy, don't share the array return new String(value, 0, count); } ``` 因此String变量的加法操作实际上是在堆中创建了一个String对象,同时返回对象在堆中的引用。 ### String的hashCode函数为何要设计缓存? String作为使用最频繁的类型,其很多细微的设计都非常有趣。先回顾下String的属性: * value[] 字符数组用于存储数据 * hash用来缓存hash code ```java public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; /** Cache the hash code for the string */ private int hash; // Default to 0 } ``` hashCode的源码: 如果hash码已计算好,将不再进行再次计算,直接返回cache的hash. 这样设计主要是考虑到一方面String使用非常频繁,另一方面String经常所为一些数据结构的检索字段,例如:Map. 缓存hash可以提升性能 ```java public int hashCode() { int h = hash; 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; } ``` ### String的immutable特性 String是不可变对象,前面我们也探讨了怎么样设计一个不可变对象。这里来看看String的情况: * 类命、实际存储数据的字符数组都用final修饰 * 没有提供任何setter方法 * 操作性的函数都是拷贝新的对象 针对第三点,我们看几个实际的函数: replace:返回的是new String ```java public String replace(char oldChar, char newChar) { if (oldChar != newChar) { ... return new String(buf, true); ... } return this; } ``` substring: 返回的是new String ```java public String substring(int beginIndex, int endIndex) { ... return ((beginIndex == 0) && (endIndex == value.length)) ? this : new String(value, beginIndex, subLen); } ``` ### 编译器对于String的优化 现在编译器会对String的操作做些基本的优化,下面用代码举例: **编译前的Java代码** ```java String s1 = "H" + "i"; final String s2 = "H"; String s3 = s2 + "i"; String s4 = s1 + s2; ``` **编译后的class进行反编译** ```java String s1 = "Hi"; String s2 = "H"; String s3 = "Hi"; String s4 = s1 + "H"; ```
本文由
cyj
创作,可自由转载、引用,但需署名作者且注明文章出处。
文章标题:
Java基础之String你了解多少?
文章链接:
https://chenyongjun.vip/articles/12
扫码或搜索 cyjrun 关注微信公众号, 结伴学习, 一起努力