初探String

平时只知道使用String,却不知道String有这么多奥秘,今天就来好好探讨一下。

String概述

我们从源码下手

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

首先看到String是一个final的类,甚至所有的变量都是final修饰,final有什么用呢?

  • final:final 可以用来修饰类、方法、变量,分别有不同的意义,final 修饰的 class 代表不可以继承扩展,final 的变量是不可以修改的,而 final 的方法也是不可以重写的。
    在Java.lang包中的很多类都是被声明为final,目的是为了保证基础功能不被修改,保证平台的安全。
    很多人看到上面的说法会认为final修饰的类或者变量时不可变的,这是错误的,final不等于immutable,举个简单的例子:final List strList =new ArrayList.....只能保证list这个引用不可以被修改,但不能保证list的对象行为(关于对象行为,面向对象我在这里面有说过,不在赘述)
不可变性
image.png
  • 实现原理:
    1. final修饰类和成员变量
      我们知道包括String是一个典型的immutable类,显然final是为了实现这个目标而来的,但是仅仅是final是不够的,就像我们上面说的List一样,String内部维护了一个char数组用来存储字符串中的字符,我可以对数组元素做修改啊。
    2. private成员变量
      /** The value is used for character storage. */
      private final char value[];
      String类没有暴露任何内部成员字段
    3. 没有任何setter方法
      String提供的方法中没有任何一个可以让我们修改value字段
    4. 构造器实现了深拷贝
 /**
     * Allocates a new {@code String} so that it represents the sequence of
     * characters currently contained in the character array argument. The
     * contents of the character array are copied; subsequent modification of
     * the character array does not affect the newly created string.
     *
     * @param  value
     *         The initial value of the string
     */
    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }

以上所说的是String成为了一个immutable类,这也是我们以后设计不可变类的一些方式

  • 不可变的好处
    其实简单来说就两个字:安全
    举个简单的例子:在hashmap或者hashset中,使用String做键,如果使用可变类做键,那么可容易就破坏键值的唯一性

public class Test {


  public static void main(String[] args) {
    HashSet<String> strings = new HashSet<>();
    String s = "1";
    strings.add(s);
    String s1 = s;
    s1 += "123";
    System.out.println(strings);
    HashSet<StringBuilder> stringBuilders = new HashSet<>();
    StringBuilder sb = new StringBuilder("1");
    stringBuilders.add(sb);
    StringBuilder sb1 = sb;
    sb1.append("123");
    System.out.println(stringBuilders);
  }
}

image.png

使用可变类的时候键值被修改,这是很大的问题
第二个例子:多并发的场景下,使用可变类需要额外的同步并且很容易出问题,但是不可变类就不存在这些问题(为啥?不可变类的状态只有一种,并且由构造函数来控制,所以是无法修改的,也就不存在线程安全问题)

  • 性能:
    在我们的应用程序中,String是大量被使用的,如果重复的创建大量的对象,必然会造成性能的下降,这里也是String使用final的目的之一,对于重复的字符串对象,使用常量池来避免大量的对象创建和销毁。
String常量池

在上一篇关于Java内存区域管理的文章中大概的说了一下方法区,在Java8中,String常量池移入了堆中
image.png

大概是这个样子,详细的我就不再画了。
常量池是干啥的?
很明显,用来缓存String对象的呗,复用。
String常量可能会在两种时机进入常量池:

  • 编译期:通过双引号声明的常量(包括显示声明、静态编译优化后的常量,如”1”+”2”优化为常量”12”),在前端编译期将被静态的写入class文件中的“常量池”。该“常量池”会在类加载后被载入“内存中的常量池”,也就是我们平时所说的常量池。同时,JIT优化也可能产生类似的常量。
    引用网上的一些例子:
使用 ” ” 双引号创建 : String s1 = “first”;
使用字符串连接符拼接 : String s2=”se”+”cond”;
使用字符串加引用拼接 : String s12=”first”+s2;
使用new String(“”)创建 : String s3 = new String(“three”);
使用new String(“”)拼接 : String s4 = new String(“fo”)+”ur”;
使用new String(“”)拼接 : String s5 = new String(“fo”)+new String(“ur”);

image.png

我们看到所有的字面量被放进了常量池,这里要注意的是下面这个例子:

 String result = "hello" + "world" ;
常量池中是helloworld
String a="hello";
String b="world";
String result =a+b;
常量池中是hello和world
final String a="hello";
final String b="world";
String result =a+b;
常量池中是hello和world 以及helloworld
  • 运行期:调用String#intern()方法,可能将该String对象动态的写入上述“内存中常量池”
    但是,在Java6之后,由于SCP内存位置的变化,intern也发生了变化String#intern 方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。
public static void main(String[] args) {
    String s = new String("1");
    s.intern();
    String s2 = "1";
    System.out.println(s == s2);

    String s3 = new String("1") + new String("1");
    s3.intern();
    String s4 = "11";
    System.out.println(s3 == s4);
    //这段代码更加直观
    //  String s3 = new String("1") + new String("1");
    //  System.out.println(s3 == s3.intern(););
}

这里我用的jdk8做的测试 结果为false和true


image.png

String s = new String("1"); 第一句代码,生成了2个对象。常量池中的“1” 和 JAVA Heap 中的字符串对象。s.intern(); 这一句是 s 对象去常量池中寻找后发现 “1” 已经在常量池里了。接下来String s2 = "1"; 这句代码是生成一个 s2的引用指向常量池中的“1”对象。 结果就是 s 和 s2 的引用地址明显不同。
String s3 = new String("1") + new String("1");,这句代码中现在生成了2最终个对象,是字符串常量池中的“1” 和 JAVA Heap 中的 s3引用指向的对象。中间还有2个匿名的new String("1")我们不去讨论它们。此时s3引用对象内容是"11",但此时常量池中是没有 “11”对象的。
接下来s3.intern();这一句代码,是将 s3中的“11”字符串放入 String 常量池中,因为此时常量池中不存在“11”字符串,关键点是 jdk76以后常量池不在 Perm 区域了,这块做了调整。常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用指向 s3 引用的对象。 也就是说引用地址是相同的。

public static void main(String[] args) {
    //intern下调
    String s = new String("1");
    String s2 = "1";
    s.intern();
    System.out.println(s == s2);

    String s3 = new String("1") + new String("1");
    String s4 = "11";
    s3.intern();
    System.out.println(s3 == s4);
     
}
image.png

运行结果为false,false,为啥呢,String s = new String("1"); 第一句代码,生成了2个对象。常量池中的“1” 和 JAVA Heap 中的字符串对象。。接下来String s2 = "1";这一句是 s2 对象去常量池中寻找后发现 “1” 已经在常量池里了 ,所以s2引用的是常量池的对象,s.intern这句代码已经没有作用了,因为常量池中已经存在1了。 结果就是 s 和 s2 的引用地址明显不同
String s3 = new String("1") + new String("1");,这句代码中现在生成了2最终个对象,是字符串常量池中的“1” 和 JAVA Heap 中的 s3引用指向的对象。中间还有2个匿名的new String("1")我们不去讨论它们。此时s3引用对象内容是"11",但此时常量池中是没有 “11”对象的。然后 String s4 = "11";在常量池中生成了11对象,所以s4这时候是指向常量池中的对象的,s3.intern也没有任何影响,因为常量池中已经存在11对象了

最后总结一下就是:String#intern 方法时,如果常量池中存在对象,则返回常量池中的对象;否则,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。(常量池中既有对象也有引用)

  • intern注意事项
    JAVA 使用 jni 调用c++实现的StringTable的intern方法, StringTable的intern方法跟Java中的HashMap的实现是差不多的, 只是不能自动扩容。默认大小是60013(Java8)。

要注意的是,String的String Pool是一个固定大小的Hashtable,默认值大小长度是60013,如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降(因为要一个一个找)。
可以使用下面命令查看你的StringTable大小
-XX:+PrintStringTableStatistic...

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 从网上复制的,看别人的比较全面,自己搬过来,方便以后查找。原链接:https://www.cnblogs.com/...
    lxtyp阅读 1,357评论 0 9
  • 前言 RTFSC (Read the fucking source code )才是生活中最重要的。我们天天就是要...
    二毛_coder阅读 465评论 1 1
  • String,是Java中除了基本数据类型以外,最为重要的一个类型了。很多人会认为他比较简单。但是和String有...
    拓薪教育阅读 385评论 0 0
  • String是Java基础的重要考点。可问的点多,而且很多点可以横向切到其他考点,或纵向深入JVM。 本文略过了S...
    猴子007阅读 1,408评论 0 8
  • 中午时分,在北海道大快朵顾各种海鲜,我感觉这顿饭够我吹嘘一年了,对于一个屌丝来说,用多天吃土的积累换一次北...
    傅红了雪阅读 726评论 0 0