• String高效编程优化(Java)


    1, substring截取超大字符串可能造成的“内存泄漏”

    2,+ 操作符的优化和局限

    3,StringBuilder和StringBuffer

    4,split和StringTokenizer做简单字符分割效率的比较

    1, substring截取超大字符串可能造成的“内存泄漏”

    我们知道,String对象内保存着一个char数组。但是char数组未必和String所代表的字符集等长,而可能是一个“超集”。String有一个私有的构造函数:

    // Package private constructor which shares value array for speed.
        String(int offset, int count, char value[]) {
            this.value = value;
            this.offset = offset;
            this.count = count;
        }

    这个构造函数允许你只使用value[]的一部分作为String的字符集,它并不会截取value[]的一部分来创建一个新的char数组,而是把它整个保存起来了。

    接着来看substring函数的实现:

         public String substring(int beginIndex, int endIndex) {
            if (beginIndex < 0) {
                throw new StringIndexOutOfBoundsException(beginIndex);
            }
            if (endIndex > count) {
                throw new StringIndexOutOfBoundsException(endIndex);
            }
            if (beginIndex > endIndex) {
                throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
            }
            return ((beginIndex == 0) && (endIndex == count)) ? this :
                
    new String(offset + beginIndex, endIndex -
     beginIndex, value);
        }

    substring正是用我们上面提到的构造函数来构造返回的String的,Java这么做有利有弊:

    1)如果我们要从一个大字符串中截取许多小字符串,那么这些小字符串共享一个大的char[]。那么,这么做是非常高效的,避免了重新分配内存的时间空间开销。

    2)但是,如果我们只从中截取一个或少数几个很小的字符串,原String将丢弃,而这些小字符串却被长期保存,这样我们就造成了某种意义上的内存泄漏 -- 我们以为原String的内存被GC释放了,然而并没有,它的主要部分 — 巨大的char数组仍被他的子String引用着,虽然只有其中很小的一部分被它们使用了。

    对于这种泄漏,解决办法很简单,使用以下语法

    str2 = new String(str1.substring(5,100));

    构造函数String(String)会为新的String创建一个新的char[]。但是前提是,我们意识到了substring可能导致的问题。

    (后记:

    谢谢@╰︶赖床专业户こ 的提醒,JDK7早期的版本还是按照我说的方式实现的,但是后期的版本已经修复了。我没有注意我参考的版本。
    应该说新的实现有利有弊,现在我们需要担心的不是substring的泄露问题,而是效率问题了。
    非常赞赏你的研究精神。

    2,+ 操作符的优化和局限

    我们知道,对于以下语法:

    str1 += "abc";
    str1 = str1 + "abc";

    Java将创建一个新的String对象和字符串数组,把原字符串和”abc”拷贝拼接到新的字符串数组中。如果反复进行这样字符串的累加操作,自然是非常低效的,这种情况按照最佳实践,应该使用StringBuilder。

    但事实上,Java已经对+操作进行了优化。看下面的代码:

    String temp = "ABC" + 200 + 'D';

    编译器已经把该代码优化编译成了:

    String temp = new StringBuilder().append( "ABC" ).append( 200 ).append('D').toString();

    (注:

    另外,如果代码简单的多个字符串相加:

    String temp = "Hello" + “ ” + “World”;

    编译器直接优化为

    String temp = "Hello World”;

    所以,连续累加效率并不比使用StringBuilder效率差,因为它本来就是用一个StringBuilder对象连续的append来实现的。

    但是,如果是:

    for(int i=0; i<100; i++)
    {
        temp+="abc";
    }

    编译器并没有办法把以上for循环里面多次迭代的‘+’操作优化为只使用一个StringBuilder对象的连续append操作。因此,还是非常低效的。

    简而言之,如果所有的字符串拼接可以在一行里面用‘+’完成,那么是没有效率问题的;否则,最好使用StringBuilder。

    3,StringBuilder和StringBuffer

    StringBuilder和StringBuffer用法基本没什么区别,但是StringBuilder不是线程安全的,StringBuffer是线程安全的。StringBuffer在所有用于字符操作的public方法都加了锁--使用了synchronized关键字。

    我们来测试一下单线程下StringBuilder和StringBuffer的效率,以下代码:

    public static void main(String[] args){
            long t1 = System.nanoTime();
            StringBuffer stringBuffer = new StringBuffer();
            
            for(int i=0; i<1000000; i++)
            {
                stringBuffer.append("a");
            }
            stringBuffer.toString();
            long t2 = System.nanoTime();
            System.out.println("StringBuffer :"+ (t2-t1));
            
             t1 = System.nanoTime();
            StringBuilder stringBuilder = new StringBuilder();
            
            for(int i=0; i<1000000; i++)
            {
                stringBuilder.append("a");
            }
            stringBuilder.toString();
             t2 = System.nanoTime();
            System.out.println("StringBuilder:"+ (t2-t1));
        }

    结果:

    StringBuffer :33979818
    StringBuilder:14061978

    单线程情况下,StringBuilder要快一倍多。

    那多线程情况StringBuffer效率如何呢?下面代码测试:

    long t1 = System.nanoTime();
            final StringBuffer stringBuffer = new StringBuffer();
            
            ExecutorService executor = Executors.newFixedThreadPool(3);
            CountDownLatch countDownLatch = new CountDownLatch(3);
            
            for (int i = 0; i < 3; i++) {
                executor.execute(new Runnable() {
    
                    @Override
                    public void run() {
                        for (int i = 0; i < 333333; i++) {
                            stringBuffer.append("a");
                        }
                    }
    
                });
                countDownLatch.countDown();
            }
            stringBuffer.toString();
            countDownLatch.await();
            
            long t2 = System.nanoTime();
            System.out.println("StringBuffer :"+ (t2-t1));

    结果:

    StringBuffer :2603076

    虽然我们使用了3个工作线程,但是效率几乎比单线程没有什么提升,这就是使用锁在多线程的结果--锁在多线程中的协调,导致线程的频繁切换,大大降低效率。

    虽然我实在不知道有什么场景需要用到多线程的字符串拼装。假设有,并且对性能有很严格的要求,我觉得可以考虑使用一些无锁的多线程编程框架,例如Disruptor--一个无锁的RingBuffer框架,使用多个生产者线程往Ring buffer中投递String对象,在消费者中用StringBuilder进行组装。(类似log4j 2的异步日志处理)

    4,split和StringTokenizer做简单字符分割效率的比较。

    很多文章都说split比StringTokenizer效率高很多,开始也深以为然,但是却发现它们的测试代码都存在很严重的问题。自己做了一下测试

            StringBuilder stringBuilder = new StringBuilder();
            for (int i = 0; i < 1000000; i++) {
                stringBuilder.append(i);
                stringBuilder.append(",");
            }
            
            
            String str = stringBuilder.toString();
            long t1 = System.nanoTime();
            String[] strArray = str.split(",");
            long t2 = System.nanoTime();
            System.out.println("split :" + (t2 - t1));
    
            String str1 = stringBuilder.toString();
            t1 = System.nanoTime();
            StringTokenizer stringTokenizer = new StringTokenizer(str1, ",");
            //List<String> strList = new ArrayList<String>(1000000); //或者 String[] strArray1 = new String[stringTokenizer.countTokens()];
            for (int i = 0; i < 1000000; i++) {
                String subStr = stringTokenizer.nextToken();
                //strList.add(subStr); //或者strArray1[i] =subStr;
            }
            t2 = System.nanoTime();
            System.out.println("token :" + (t2 - t1));

    结果:

    split :248539389
    token :53191452

    StringTokenizer 比split快4倍。

    但是上面的比较在某些情况下并不公平,split会返回一个数组,而StringTokenizer 的next方法只能逐个浏览token。如果要求StringTokenizer 也把返回的子字符串保存在List中,那么结果如何呢?把上面代码段中的注释掉的代码打开,使StringTokenizer 也要把tokens保存在List或Array中,再进行测试。

    结果:

    split :254496592
    token :303926083

    这种情况下StringTokenizer 的效率还差一些。因此,不能一概而论split或StringTokenizer 谁的效率高,还要看如果使用。如果需要把结果放在Array或List当中,split更简单还有效率。(可见2种算法效率并没有本质差别,差就差在Array或List的使用上,具体还要从JDK的源代码去分析)

    Binhua Liu原创文章,转载请注明原地址http://www.cnblogs.com/Binhua-Liu/p/5572350.html

  • 相关阅读:
    RabbitMQ官方文档翻译之Simple(一)
    rabbitMq集成Spring后,消费者设置手动ack,并且在业务上控制是否ack
    RabbitMQ消息队列知识点归纳
    理解Java中HashMap的工作原理
    mybatis 主键回显
    quart任务调度框架实战
    springmvc常用注解标签详解
    Java程序员玩Linux学操作系统
    在网页中发起QQ临时对话的方法
    软件测试技术学习总结
  • 原文地址:https://www.cnblogs.com/Binhua-Liu/p/5572350.html
Copyright © 2020-2023  润新知