初探String

作者: 黄金矿工00七 | 来源:发表于2018-07-20 14:29 被阅读3次

    平时只知道使用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...

    相关文章

      网友评论

        本文标题:初探String

        本文链接:https://www.haomeiwen.com/subject/tbozpftx.html