zoukankan      html  css  js  c++  java
  • 第一章 关于String

    1.String的编译时优化方案

      首先看以下代码的输出:

    String a = "a" + "b" + "c";
    String b = "abc";
    System.out.println(a == b);
    View Code

        以上代码的输出为true。原因是当编译器在执行String a = "a" + "b" + "c"时,会将其编译成String a = "abc";编译器认为3个常量相加的会得到固定的常量值,无需等到运行时再进行计算。

       

            String a = "a";
            final String b = "a";
            String aa = a + "c";
            String bb = b + "c";
    
            String c = "ac";
            System.out.println(aa == c);
            System.out.println(bb == c);
    -------------------------------------------------------------------------------------------
    false
    true

      以上的代码在编译时,因为b是final类型,所以在编译String bb = b + "c"时,编译器执行了编译优化,直接编译成String bb ="ac"。而a的值在上述代码中虽然没有发生变化,但编译器并不会先跟踪查看a的值是否发生过变化。

    2.intern()/equals()

            String a = "a";
            String b = a + "b";
            String c = "ab";
            System.out.println(b == c);    //false
            System.out.println(b.intern() == c);      //true

      intern()方法是一个native方法,该方法的底层不是由java语言实现。关于native关键字的详细介绍,将在后续博客中给出。调用intern()方法时,JVM会在常量池中调用equals()方法查看是否有值相等的String,如果存在则直接返回该String对象的地址;如果没有找到,先创建等值的字符串,再返回新建字符串的地址。可以看出intern()方法需要比较多个字符串,而且为了保证唯一性,需要有锁的介入。

    JDK1.7中equals()代码如下:

     
        private final char value[];
        public boolean equals(Object anObject) {
                if (this == anObject) {     //传入对象是否为当前对象
                    return true;
                }
                if (anObject instanceof String) {   //是否为字符串
                    String anotherString = (String) anObject;
                    int n = value.length;
                    if (n == anotherString.value.length) {  //比较两个字符串的长度
                        char v1[] = value;
                        char v2[] = anotherString.value;
                        int i = 0;
                        while (n-- != 0) {
                            if (v1[i] != v2[i])
                                return false;
                            i++;
                        }
                        return true;
                    }
                }
                return false;
        }

      如果两个字符串匹配上,则需要遍历两个char数组。

          

    3.StringBuilder.append()与"+"

      通过"+"进行拼接时,如果拼接的是常量,则会在编译时进行优化,不需要在运行时再分配空间,这一点是append()操作做不到的。

      对于运行时拼接的情况,如将"+"与append()操作放在循环中执行时:

        编译前的代码
        String a = "";
        for(){
            a += "aaa"; //拼接随机字符串
        }
    
        编译后的代码
        String a = "";
        for(){
            StringBuilder tmp = new StrngBuilder();
            tmp.append(a).append("aaa");
            a = tmp.toString();
        }

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

      可以看到,对于"+"的操作,编译器在编译期间将其转成append()来完成字符串拼接,所以如果在循环中使用"+"造作,会在循环体内部创建出许多StringBuilder对象,同时调用toStriing()方法时会创建新的String对象,这些临时对象会占用大量内存,导致频繁的GC。

      循环拼接操作,当字符串a达到一定程度之后会进入old区域。对于"+"操作,当a达到old区域的1/4时会发生OOM;而对于append(),当a达到old区域的1/3时会发生OOM。

          首先来看JDK1.7中StringBuilder类append()的源码:

      public StringBuilder append(String str) {
            super.append(str);
            return this;
        }
    
        public AbstractStringBuilder append(String str) {
            if (str == null) str = "null";
            int len = str.length();
            ensureCapacityInternal(count + len);    //count为当前StringBuilder对象中的有效元素的个数
            str.getChars(0, len, value, count);
            count += len;
            return this;
        }
    
        private void ensureCapacityInternal(int minimumCapacity) {
            if (minimumCapacity - value.length > 0)    
                expandCapacity(minimumCapacity);
        }
    
        void expandCapacity(int minimumCapacity) {
            //以“count + 新字符串长度”和"char[]中长度的2倍"相比较,取较大值进行扩容
            int newCapacity = value.length * 2 + 2;
            if (newCapacity - minimumCapacity < 0)    
                newCapacity = minimumCapacity;
            if (newCapacity < 0) {
                if (minimumCapacity < 0) // overflow
                    throw new OutOfMemoryError();
                newCapacity = Integer.MAX_VALUE;
            }
            value = Arrays.copyOf(value, newCapacity);
        }

       StringBuilder对象内部先分配16个长度的char[]数组,当发生append()操作时继续向数组后添加元素;若空间不够时则尝试扩容,扩容的规则如上述代码注释中所述。

      对于"+"操作,当a对象达到old区域的1/4时,会先分配一个StringBuilder对象,初试的char[]数组长度为16,首先进行tmp.append(a)操作,空间不够大需要进行扩容,此时count=0,扩容的长度为"a的长度+count",扩容完成后存储拼接a,而且当a拼接完成时,StringBuilder已经没有空余的char[]空间了,而且此时原本的a对象还没有释放,所以此时占用了old区域的1/4+1/4=1/2的空间。

      然后开始进行append("aaa")的操作,如上所述,此时StringBuilder已经没有空余的char[]空间了,需要进行再次扩容,"3(aaa的长度)+count"<char[]总长度的2倍,所以此次扩容是2倍扩容。扩容后的StringBuilder的char[]数组的空间是1/4*2=1/2,所以old空间的剩余一半也被用掉了。扩容操作完成后,扩容前的char[]数组空间才会被释放,如果append()的随机字符串是""空字符串,则拼接前的对象长度达到old空间的1/3时发生OOM。在执行toString()方法时,前面的扩容已经成功,所以扩容前的char[]数组已经被释放。

      再看看在循环中直接使用append()操作的情况:

        StringBuilder tmp = new StrngBuilder();
        for(){     
            tmp.append("随机字符串");
        }

      在这段代码中,在第一次进行拼接时,StringBuilder首先也是分配16个长度的char[]数组,在扩容时,因为"count+随机字符串的长度(值较小)"一般都小于char[]数组的长度*2,所以都是进行2倍扩容,而且进行二倍扩容之后,存储随机字符串之后仍有一般左右的空余空间,所以能使用一段时间之后才会再次扩容,而且不会发生申请一个大的StringBuilder对象并很快将它当成垃圾的情况。这种擦偶偶在字符串达到old区域的1/3时会发生OOM。

      与String的"+"不同的是,在差几个字节导致OOM时,String拼接在下一次拼接随机字符串时必然会发生OOM,但append()操作扩容后会经历很长一段时间才发生OOM。另外append()操作产生的垃圾都是小块的内存,主要是拼接的对象以及扩容时原来的空间。所以在这种场景下StringBuilder对象拼接字符串的效率会高出很多倍。

      关于String的"+"的补充说明:String str = a + b + c + d;这行代码只会申请一个StringBuilder并执行多个append()操作。

  • 相关阅读:
    2016"百度之星"
    codeforces 55 div2 C.Title 模拟
    codeforces 98 div2 C.History 水题
    codeforces 97 div2 C.Replacement 水题
    codeforces 200 div2 C. Rational Resistance 思路题
    bzoj 2226 LCMSum 欧拉函数
    hdu 1163 九余数定理
    51nod 1225 余数的和 数学
    bzoj 2818 gcd 线性欧拉函数
    Codeforces Round #332 (Div. 2)D. Spongebob and Squares 数学
  • 原文地址:https://www.cnblogs.com/jian-xiao/p/5621353.html
Copyright © 2011-2022 走看看