• 第一章 关于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()操作。

  • 相关阅读:
    cas 单点登录服务端客户端配置
    POI 导出excel
    关于小米手机刷机亲尝
    C#对本地文件重命名--适用于下载的图片、电视剧等奇怪名字的重命名
    泛型List<T>与非泛型ArrayList
    设置一键启动多文件
    网页显示电子表
    插入sql语句01值时,在数据库中的查询时显示为1
    C#面向对象--继承
    SqlServer数据库查询不同字段-年龄段分析
  • 原文地址:https://www.cnblogs.com/jian-xiao/p/5621353.html
Copyright © 2020-2023  润新知