美文网首页
String解析(一)

String解析(一)

作者: 七喜丶 | 来源:发表于2022-01-23 10:40 被阅读0次

    字符串的特征

    想要了解 String 的特性就必须从它的源码入手,如下所示:

    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
    
        /** 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
         * <a href="{@docRoot}/../platform/serialization/spec/output.html">
         * Object Serialization Specification, Section 6.2, "Stream Elements"</a>
         */
        private static final ObjectStreamField[] serialPersistentFields =
            new ObjectStreamField[0];
    
        /**
         * Initializes a newly created {@code String} object so that it represents
         * an empty character sequence.  Note that use of this constructor is
         * unnecessary since Strings are immutable.
         */
        public String() {
            this.value = "".value;
        }
    
        /**
         * Initializes a newly created {@code String} object so that it represents
         * the same sequence of characters as the argument; in other words, the
         * newly created string is a copy of the argument string. Unless an
         * explicit copy of {@code original} is needed, use of this constructor is
         * unnecessary since Strings are immutable.
         *
         * @param  original
         *         A {@code String}
         */
        public String(String original) {
            this.value = original.value;
            this.hash = original.hash;
        }
      }
    

    从它的源码中我们可以出,String 类以及它的 value[]属性被final修饰了,其中 value[]是实现字符串存储的最终结构,大家都知道被final修饰的类不能被继承,被修饰的方法不能被重写,被修饰的属性不能被修改。这也就是说String一旦被创建,就不能进行修改操作

    String 为什么不能被修改?

    String 的类和属性value[]都被定义为final了,这样做的好处有以下三点:

    一、安全性:当你在调用其他方法时,比如调用一些系统级操作指令之前,可能会有一系列校验,如果是可变类的话,可能在你校验过后,它的内部的值又被改变了,这样有可能会引起严重的系统崩溃问题,所以迫使 String 设计为 final 类的一个重要原因就是出于安全考虑;
    二、高性能:String 不可变之后就保证的 hash 值的唯一性,这样它就更加高效,并且更适合做 HashMap 的 key- value 缓存;
    三、节约内存:String 的不可变性是它实现字符串常量池的基础,字符串常量池指的是字符串在创建时,先去“常量池”查找是否有此“字符串”,如果有,则不会开辟新空间创作字符串,而是直接把常量池中的引用返回给此对象,这样就能更加节省空间。例如,通常情况下 String 创建有两种方式,直接赋值的方式,如 String str="Java";另一种是 new 形式的创建,如 String str = new String("Java")。当代码中使用第一种方式创建字符串对象时,JVM 首先会检查该对象是否在字符串常量池中,如果在,就返回该对象引用,否则新的字符串将在常量池中被创建。这种方式可以减少同一个值的字符串对象的重复创建,节约内存。String str = new String("Java") 这种方式,首先在编译类文件时,“Java”常量字符串将会放入到常量结构中,在类加载时,“Java”将会在常量池中创建;其次,在调用 new 时,JVM 命令将会调用 String 的构造函数,同时引用常量池中的“Java”字符串,在堆内存中创建一个 String 对象,最后 str 将引用 String 对象。

    1.不要直接+=字符串

    通过上面的内容,我们知道了 String 类是不可变的,那么在使用 String 时就不能频繁的 += 字符串了

    优化前代码:

    public static String toAdd() {
        String result = "";
        for (int i = 0; i < 10000; i++) {
            result += (" i:" + i);
        }
        return result;
    }
    

    官方为我们提供了两种字符串拼加的方案:StringBufferStringBuilder,其中 StringBuilder 为非线程安全的,而 StringBuffer则是线程安全的,StringBuffer 的拼加方法使用了关键字 synchronized来保证线程的安全,源码如下:

    @Override
    public synchronized StringBuffer append(CharSequence s) {
        toStringCache = null;
        super.append(s);
        return this;
    }
    


    也因为使用 synchronized修饰,所以 StringBuffer 的拼加性能会比 StringBuilder

    那我们就用 StringBuilder 来实现字符串的拼加,优化后代码:

    public static String toAppend() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 10000; i++) {
            sb.append(" i:" + i);
        }
        return sb.toString();
    }
    

    我们通过代码测试一下,两个方法之间的性能差别:

    public class StringTest {
        public static void main(String[] args) {
            for (int i = 0; i < 5; i++) {
                // String
                long st1 = System.currentTimeMillis(); // 开始时间
                doAdd();
                long et1 = System.currentTimeMillis(); // 开始时间
                System.out.println("String 拼加,执行时间:" + (et1 - st1));
                // StringBuilder
                long st2 = System.currentTimeMillis(); // 开始时间
                doAppend();
                long et2 = System.currentTimeMillis(); // 开始时间
                System.out.println("StringBuilder 拼加,执行时间:" + (et2 - st2));
                System.out.println();
            }
        }
        public static String doAdd() {
            String result = "";
            for (int i = 0; i < 10000; i++) {
                result += ("Java:" + i);
            }
            return result;
        }
        public static String doAppend() {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < 10000; i++) {
                sb.append("Java:" + i);
            }
            return sb.toString();
        }
    }
    

    以上程序的执行结果如下:

    String 拼加,执行时间:429
    StringBuilder 拼加,执行时间:1

    String 拼加,执行时间:325
    StringBuilder 拼加,执行时间:1

    String 拼加,执行时间:287
    StringBuilder 拼加,执行时间:1

    String 拼加,执行时间:265
    StringBuilder 拼加,执行时间:1

    String 拼加,执行时间:249
    StringBuilder 拼加,执行时间:1

    接下来,我们要思考一个问题:为什么 StringBuilder.append() 方法比 += 的性能高?而且拼接的次数越多性能的差距也越大?

    当我们打开 StringBuilder的源码,就可以发现其中的“小秘密”了,StringBuilder父类AbstractStringBuilder 的实现源码如下:

    abstract class AbstractStringBuilder implements Appendable, CharSequence {
        char[] value;
        int count;
        @Override
        public AbstractStringBuilder append(CharSequence s, int start, int end) {
            if (s == null)
                s = "null";
            if ((start < 0) || (start > end) || (end > s.length()))
                throw new IndexOutOfBoundsException(
                    "start " + start + ", end " + end + ", s.length() "
                    + s.length());
            int len = end - start;
            ensureCapacityInternal(count + len);
            for (int i = start, j = count; i < end; i++, j++)
                value[j] = s.charAt(i);
            count += len;
            return this;
        }
        // 忽略其他信息...
    }
    

    StringBuilder 使用了父类提供的 char[]作为自己值的实际存储单元,每次在拼加时会修改char[]数组,StringBuilder toString()源码如下:

    @Override
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }
    

    综合以上源码可以看出:StringBuilder 使用了 char[]作为实际存储单元,每次在拼加时只需要修改char[]数组即可,只是在 toString()时创建了一个字符串;而 String 一旦创建之后就不能被修改,因此在每次拼加时,都需要重新创建新的字符串,所以 StringBuilder.append() 的性能就会比字符串的 += 性能高很多

    2.善用 intern 方法

    善用 String.intern() 方法可以有效的节约内存并提升字符串的运行效率,先来看 intern() 方法的定义与源码:

    /**
    * Returns a canonical representation for the string object.
    * <p>
    * A pool of strings, initially empty, is maintained privately by the
    * class {@code String}.
    * <p>
    * 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.
    * <p>
    * 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}.
    * <p>
    * All literal strings and string-valued constant expressions are
    * interned. String literals are defined in section 3.10.5 of the
    * <cite>The Java&trade; Language Specification</cite>.
    *
    * @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();
    

    可以看出 intern()是一个高效的本地方法,它的定义中说的是,当调用intern方法时,如果字符串常量池中已经包含此字符串,则直接返回此字符串的引用,如果不包含此字符串,先将字符串添加到常量池中,再返回此对象的引用

    Twitter 工程师曾分享过一个String.intern() 的使用示例,Twitter 每次发布消息状态的时候,都会产生一个地址信息,以当时 Twitter 用户的规模预估,服务器需要 32G 的内存来存储地址信息

    public class Location {
        private String city;
        private String region;
        private String countryCode;
        private double longitude;
        private double latitude;
    }
    

    考虑到其中有很多用户在地址信息上是有重合的,比如,国家、省份、城市等,这时就可以将这部分信息单独列出一个类,以减少重复,代码如下:

    public class SharedLocation {
    
      private String city;
      private String region;
      private String countryCode;
    }
    
    public class Location {
    
      private SharedLocation sharedLocation;
      double longitude;
      double latitude;
    }
    

    通过优化,数据存储大小减到了 20G 左右。但对于内存存储这个数据来说,依然很大,怎么办呢?

    Twitter 工程师使用 String.intern()使重复性非常高的地址信息存储大小从 20G 降到几百兆,从而优化了 String 对象的存储

    实现的核心代码如下:

    SharedLocation sharedLocation = new SharedLocation();
    sharedLocation.setCity(messageInfo.getCity().intern());    
    sharedLocation.setCountryCode(messageInfo.getRegion().intern());
    sharedLocation.setRegion(messageInfo.getCountryCode().intern());
    

    从 JDK1.7 版本以后,常量池已经合并到了堆中,所以不会复制字符串副本,只是会把首次遇到的字符串的引用添加到常量池中。此时只会判断常量池中是否已经有此字符串,如果有就返回常量池中的字符串引用

    这就相当于以下代码:

    String s1 = new String("Java中文社群").intern();
    String s2 = new String("Java中文社群").intern();
    System.out.println(s1 == s2);
    

    执行的结果为:true

    此处如果有人问为什么不直接赋值(使用 String s1 = "Java中文社群"),是因为这段代码是简化了上面 Twitter 业务代码的语义而创建的,他使用的是对象的方式,而非直接赋值的方式。更多关于 intern() 的内容可以查看《String解析(二)》这篇文章

    相关文章

      网友评论

          本文标题:String解析(一)

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