zoukankan      html  css  js  c++  java
  • String源码浅析

    如果问你,开发过程中用的最多的类是哪个?你可能回答是HashMap,一个原因就是HashMap的使用量的确很多,还有就是HashMap的内容在面试中经常被问起。

    但是在开发过程中使用最多的类其实并不是HashMap类,而是“默默无闻”的String类。假如现在问你String类是怎么实现的?这个类为什么是不可变类?这个类为什么不能被继承?这些问题你都能回答么。本文就从String源代码出发,来看下String到底是怎么实现的,并详细介绍下String类的API的用法。

    String源码结构

    首先要说明的是本文的源码是以JDK11为基准,选择JDK11的原因是JDK11是一个LTS版本(长期支持版本),没选择现阶段还在广泛使用的JDK8的原因是想在看源码的过程中学习下JDK的新特性。

    还有要说下的就是:大家在看源码时一定要注意JDK的版本,因为不同版本的实现有较大的差异。比如说String的实现在高低版本中就差异比较大。如果你是一个博客主,更加要注明代码的版本了,不然读者可能会很疑惑,为什么和自己之前看的不一样。

    好了,下面就言归正传来看下String在JDK11中的实现代码。

     public final class String implements Serializable, Comparable<String>, CharSequence {
       @Stable
       //字节数组,存放String的内容,如果你看的是较低版本的源代码,这个变量可能是char[]类型,这个其实是JDK9开始对String做的一个优化
       //具体是做了什么优化我们下面再讲,这边先卖个关子
       private final byte[] value;
       //也是和String压缩优化有关,指定当前的LATIN1码还是UTF16码
       private final byte coder;
       //哈希值
       private int hash;
       //序列化Id
       private static final long serialVersionUID = -6849794470754667710L;
       //优化压缩开关,默认开启
       static final boolean COMPACT_STRINGS = true;
       private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
       public static final Comparator<String> CASE_INSENSITIVE_ORDER = new String.CaseInsensitiveComparator();
       static final byte LATIN1 = 0;
       static final byte UTF16 = 1;
       
       //... 下面部分代码省略
     }
    

    从实现的接口看,String类有如下特点:

    • String类被final关键字修饰,因此不能被继承。
    • String的成员变量value使用final修饰,因此是不可变的,线程安全;
    • String类实现了Serializable接口,可以实现序列化。
    • String类实现了Comparable,可以比较大小。
    • String类实现了CharSequence接口,String本质是个数组,低版本中是char数组,JDK9以后优化成byte数组,从String的成员变量value就可以看出来。

    这边说一个看源代码的小技巧:看一个类的源代码时,我们先看下这个类实现了哪些接口,就可以大概知道这个类的主要作用功能是什么了。

    JDK9对String的优化

    这边首先要讲下JDK 9中对String的优化,如果你不了解这块优化点的话,看String的代码时会感到非常疑惑。

    背景知识

    在Java中,一个字节char占用两个字节的内存空间。在低版本的JDK中,String的内部默认维护的是一个char[]数组,也就是说一个字符串中包含一个字符,这个字符串内部就包含一个相应长度的字符数组。这样就会出现下面这种情况:

     String s = "ddd";
     String s1 = "自由之路";
    

    上面两个字符串内部的情况实际上是:

     char[] value = ['d','d','d'];
     char[] value1 = ['自','由','之','路'];
    

    对于字符串s,我们发现其中每个字符其实都是可以用一个字节表示的,而现在使用两个字符的char类型来表示,明显就浪费了一倍的内存空间。

    而且根据统计,在实际程序运行中,字符串中包含的字符大多都是可以用一个字节表示的字符,所以优化的空间很大。优化的方式就是在String内部使用byte[]数组来表示字符串,而不是使用char[]数组。当检测到,字符串中的所有字符在Unicode码集中的码值可以使用一个字节表示时,就可以节省一半的空间。

    时间换空间的方式

    JDK6 中的Compressed Strings

    其实在JDK6中就对String类做过类似的优化:在Java 6引入了Compressed Strings,对于one byte per character的字符串使用byte[],对于two bytes per character的字符串继续使用char[]。

    使用-XX:+UseCompressedStrings来开启上面的优化。不过由于开启这个特性后会造成一些不可知的异常,这个特性在java7中被废弃了,然后在java8被移除。

    JDK9中的Compact String

    Java 9 重新采纳字符串压缩这一概念。

    和JDK6不同的是:无论何时我们创建一个所有字符都能用一个字节的 LATIN-1 编码来描述的字符串,都将在内部使用字节数组的形式存储,且每个字符都只占用一个字节。另一方面,如果字符串中任一字符需要多于 8 比特位来表示时,该字符串的所有字符都统统使用两个字节的 UTF-16 编码来描述。因此基本上能如果可能,都将使用单字节来表示一个字符。

     //占用3个字节
     String ss = new String("ddd");
     //占用14个字节
     String s = "自由之路ddd";
    

    现在的问题是:所有的字符串操作如何执行? 怎样才能区分字符串是由 LATIN-1 还是 UTF-16 来编码?为了处理这些问题,字符串的内部实现进行了一些调整。引入了一个 final 修饰的成员变量 coder, 由它来保存当前字符串的编码信息。

     //所有的字符串都用byte数组存储
     private final byte[] value; 
     //用coder标示字符串中所有的字符是不是都可以用一个字节表示,它的值只有两个LATIN1:1,标示所有字符都可以用一个字节表示,UTF16:标示字符串中部分字符需要两个字节表示。
     private final byte coder;
     //下面是两个常量
     static final byte LATIN1 = 0;
     static final byte UTF16 = 1;
    

    现在,大多数的字符串操作都将检查 coder 变量,从而采取特定的实现:

     public int indexOf(int ch, int fromIndex) {
       return isLatin1() 
        ? StringLatin1.indexOf(value, ch, fromIndex) 
        : StringUTF16.indexOf(value, ch, fromIndex);
     } 
     
     private boolean isLatin1() {
       return COMPACT_STRINGS && coder == LATIN1;
     } 
    

    我们再看下String的一个常用方法:

     public int length() {
       return value.length >> coder;
     }
    

    这个方法是要计算字符串的长度,含义也很清楚。根据coder字段判断当前的字符串中一个字符使用几个字节表示,如果是coder等于0,也是LATIN1模式,那么所有字符都是用一个字节表示,直接返回byte[]数组的长度就可以。

    如果coder等于1,那么标示字符串中所有字符都是用两个字节表示的,计算字符串的长度需要将byte[]数组除以2。value.length >> coder就是这个意思。

    因为对String做了上面的优化,所以String的很多方法在操作时都需要判断现在的模式是LATIN1还是UTF16模式,具体的方法这边就不一一举例了。但是这些判断对使用String的开发者时无感的。

    当然,String的这个优化特性可以关闭,使用下面的启动参数就可以。

     +XX:-CompactStrings
    

    String的常用构造方法

     //构建空字符串
     public String() {
      this.value = "".value;
      this.coder = "".coder;
     }
    
    //根据已有的字符串,创建一个新的字符串
     @HotSpotIntrinsicCandidate
     public String(String original) {
      this.value = original.value;
      this.coder = original.coder;
      this.hash = original.hash;
     }
    
    //根据字符数组,创建字符串,创建的过程中有压缩优化的逻辑,具体见下面的方法
     public String(char[] value) {
      this((char[])value, 0, value.length, (Void)null);
     }
    
    String(char[] value, int off, int len, Void sig) {
      if (len == 0) {
       this.value = "".value;
       this.coder = "".coder;
      } else {
       if (COMPACT_STRINGS) {
        //如果发现这个字符数组可以压缩,就使用LATIN1方式
        byte[] val = StringUTF16.compress(value, off, len);
        if (val != null) {
         this.value = val;
         this.coder = 0;
         return;
        }
       }
       //不能进行压缩优化,还是使用UTF16的方式
       this.coder = 1;
       this.value = StringUTF16.toBytes(value, off, len);
      }
     }
    

    String中还有很多构造方法,但是都会大同小异,大家可以自己看源代码。

    String常用方法总结

    这边总结下String的常用方法,一些比较简单的方法就不具体讲了。我们挑选一些比较重要的方法,具体讲下他们的使用方法。

    • codePointAt(int index):返回下标是index的字符在Unicode码集中的码点值;
    • codePoints():返回字符串中每个字符在Unicode码集中的码点值;
    • compareToIgnoreCase(String other):忽略大小写比较字符大小;
    • concat(String other):字符串拼接函数;
    • equalsIgnoreCase(String other):忽略大小写比较字符串;
    • format:字符串格式化函数,比较有用;
    • getBytes(String charSet):获取字符串在特定编码下的字节数组;
    • indexOf(String s):返回字符串s的下标,不存在返回-1;
    • intren():作用是检测常量池中是否有当前字符串,有的话就返回常量池中的对像,没有的话就将当前对像放入常量池。
    • isBlank():如果字符串为空或只包含空白字符,则返回true,否则返回false,JDK11新加的API;
    • length():返回字符长度;
    • lines():从字符串返回按行分割的Stream,行分割福包括:n ,r 和rn,stream包含了按顺序分割的行,行分隔符被移除了,这个方法会类似split(),但性能更好;这个也是JDK11新加的API
    • matchs(String regex):和某个正则是否匹配;
    • regionMatches(int firstStart, String other, int otherStart, int len):当某个字符串调用该方法时,表示从当前字符串的firstStart位置开始,取一个长度为len的子串;然后从另一个字符串other的otherStart位置开始也取一个长度为len的子串,然后比较这两个子串是否相同,如果这两个子串相同则返回true,否则返回false。
    • repeat():返回一个字符串,其内容是字符串重复n次后的结果,JDK11新加入的函数;
    • String[] split(String regex, int limit):分割字符串,注意limit参数的使用,下面会详细讲;
    • startsWith(String prefix, int toffset):判断字符串是否以prefix打头;
    • replace(char oldChar, char newChar):使用newChar替换所有的oldChar,不是基于正则表达式的;
    • replace(CharSequence target, CharSequence replacement):替换所有,基于正则表达式的;
    • replaceFirst(String regex, String replacement):替换regex匹配的第一个字符串,基于正则表达式;
    • replaceAll(String regex, String replacement):替换regex匹配的所有字符串,基于正则表达式;
    • strip() :去除字符串前后的“全角和半角”空白字符,这个函数在JDK中11才引入,注意和trim的区别,关于全角和半角的区别,可以参考这篇文章,还提供了stripLeading()和stripTrailing(),可以分别去掉头部或尾部的空格;
    • subString(int fromIndex):从指定位置开始截取到字符串结尾部分的子串;
    • subString(int fromIndex,int endIndex):截取字符串指定下标的子串;
    • toCharArray():转换成字符数组;
    • toUpperCase(Locale locale) :小写转换成大写;
    • toLowerCase(Locale locale):大写转换成小写;
    • trim():去除字符串前后的空白字符(空格、tab键、换行符等,具体的话是去除ascll码小于32的字符),注意trim和strip的区别;
    • valueof系列方法:将其他类型的数据转换成String类型,比如将bool、int和long等类型转换成String类型。

    concat字符串拼接函数

    concat函数是字符串拼接函数,介绍这个函数并不是因为这个函数比较重要或者实现比较复杂。而是因为通过这个函数的源代码我们可以看出很多String的特性。

     public String concat(String str) {
      //如果被拼接的字符串的长度是0,直接返回自己
      int olen = str.length();
      if (olen == 0) {
       return this;
      } else {
       byte[] buf;
       //如果当前字符串和被拼接的字符串的编码模式相同,都是LATIN1或者都是UTF16
       if (this.coder() == str.coder()) {
        byte[] val = this.value;
        buf = str.value;
        //计算出新字符串所需字节的长度
        int len = val.length + buf.length;
        byte[] buf = Arrays.copyOf(val, len);
        //使用系统函数拷贝
        System.arraycopy(buf, 0, buf, val.length, buf.length);
        //根据新的字节数组生成一个新的字符串
        return new String(buf, this.coder);
       } else {
        //当前字符串和被拼接的字符串的编码模式不同,那么必须使用UTF16的编码模式
        int len = this.length();
        buf = StringUTF16.newBytesFor(len + olen);
        this.getBytes(buf, 0, (byte)1);
        str.getBytes(buf, len, (byte)1);
        return new String(buf, (byte)1);
       }
      }
     }
    

    format函数

    String的format方法是一个很有用的方法,可以用来对字符串、数字、日期和时间等进行格式化。

    //对整数格式化,4位显示,不足4位补0
    //超过4位,还是原样显示
    int num = 999;
    String str = String.format("%04d", num);
    System.out.println(str);
    
    //对日期进行格式化
    String format = String.format("%tF", new Date());
    System.out.println(format);
    

    format方法还有很多用法,大家可以自己查询使用。

    regionMatches

    该方法的定义如下:

    regionMatches(int firstStart, String other, int otherStart, int len)
    
    

    当某个字符串调用该方法时,表示从当前字符串的firstStart位置开始,取一个长度为len的子串;然后从另一个字符串other的otherStart位置开始也取一个长度为len的子串,然后比较这两个子串是否相同,如果这两个子串相同则返回true,否则返回false。

    该方法还有另一种重载:

    str.regionMatches(boolean ignoreCase, int firstStart, String other, int otherStart, int len)
    
    

    可以看到只是多了一个boolean类型的参数,用来确定比较时是否忽略大小写,当ignoreCase为true表示忽略大小写。

    split函数

    String的split函数我们平时也经常使用,但是估计很多人都没有注意这个函数的第二个参数:limit

    public String[] split(String regex, int limit)
    

    首先,split方法的作用是根据给定的regex去分割字符串,将分割完成的字符数组返回。其中limit参数的作用是:

    • 当limit>0时,limit代表最后的数组长度,同时一共会分割limit-1次,最后没有切割完成的直接放在一起;

    • 当limit=0时(默认值),会尽量多去分割,并且如果分割完的字符数组末尾是空字符串,会去除这个空字符串;

    • 当limit<0时,会尽量多去分割,但不会去掉末尾的空字符串。

    下面举个列子:

    String s1 = "博客园|CSDN||";
    
    String[] split1 = s1.split("\|", 2);
    System.out.println("split1 length:" + split1.length);
    System.out.println("split1 content:" + Arrays.toString(split1));
    String[] split2 = s1.split("\|", 0);
    System.out.println("split2 length:" + split2.length);
    System.out.println("split2 content:" + Arrays.toString(split2));
    String[] split3 = s1.split("\|", -1);
    System.out.println("split3 length:" + split3.length);
    System.out.println("split3 content:" + Arrays.toString(split3));
    
    System.out.println("---换一个复杂点的字符串---");
    s1 = "|博客园||CSDN|自由之路ddd|";
    
    split1 = s1.split("\|", 2);
    System.out.println("split1 length:" + split1.length);
    System.out.println("split1 content:" + Arrays.toString(split1));
    split2 = s1.split("\|", 0);
    System.out.println("split2 length:" + split2.length);
    System.out.println("split2 content:" + Arrays.toString(split2));
    split3 = s1.split("\|", -1);
    System.out.println("split3 length:" + split3.length);
    System.out.println("split3 content:" + Arrays.toString(split3));
    

    下面是输出结果,对照着这个结果大家就应该能明白split方法的使用了

    split1 length:2
    split1 content:[博客园, CSDN|自由之路ddd|]
    split2 length:3
    split2 content:[博客园, CSDN, 自由之路ddd]
    split3 length:4
    split3 content:[博客园, CSDN, 自由之路ddd, ]
    ---换一个复杂点的字符串---
    split1 length:2
    split1 content:[, 博客园||CSDN|自由之路ddd|]
    split2 length:5
    split2 content:[, 博客园, , CSDN, 自由之路ddd]
    split3 length:6
    split3 content:[, 博客园, , CSDN, 自由之路ddd, ]
    

    再举个JDK中的列子:

    The input "boo:and:foo", for example, yields the following results with these parameters:

    Regex     Limit     Result    
    : 2 { "boo", "and:foo" }
    : 5 { "boo", "and", "foo" }
    : -2 { "boo", "and", "foo" }
    o 5 { "b", "", ":and:f", "", "" }
    o -2 { "b", "", ":and:f", "", "" }
    o 0 { "b", "", ":and:f" }

    总结

    • String类被final关键字修饰,因此不能被继承;
    • String的成员变量value使用final修饰,因此是不可变的,线程安全;
    • String中的方法对字符串的操作都会生成一个新的String对象,如果你需要一个可修改的字符串,应该使用 StringBuffer 或者 StringBuilder。否则会有大量时间浪费在垃圾回收上,因为每次试图修改都有新的string对象被创建出来;
    • JDK9开始对String进行了优化,内部彻底使用byte[]数组来代替char数组。

    参考

    公众号推荐

    欢迎大家关注我的微信公众号「程序员自由之路」

    人生的主旋律其实是苦难,快乐才是稀缺资源。在困难中寻找快乐,才显得珍贵~
  • 相关阅读:
    HDU 4278 Faulty Odometer 8进制转10进制
    hdu 4740 The Donkey of Gui Zhou bfs
    hdu 4739 Zhuge Liang's Mines 随机化
    hdu 4738 Caocao's Bridges tarjan
    Codeforces Gym 100187M M. Heaviside Function two pointer
    codeforces Gym 100187L L. Ministry of Truth 水题
    Codeforces Gym 100187K K. Perpetuum Mobile 构造
    codeforces Gym 100187J J. Deck Shuffling dfs
    codeforces Gym 100187H H. Mysterious Photos 水题
    windows服务名称不是单个单词的如何启动?
  • 原文地址:https://www.cnblogs.com/54chensongxia/p/13626963.html
Copyright © 2011-2022 走看看