String高效编程2,3事(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可能导致的问题。

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的源代码去分析)

时间: 2024-10-13 12:38:27

String高效编程2,3事(Java)的相关文章

JAVA编程“性能说”(java编程需要做的26件事)

转载于 http://www.csdn.net/article/2012-06-01/2806249 最近的机器内存又爆满了,除了新增机器内存外,还应该好好review一下我们的代码,有很多代码编写过于随意化,这些不好的习惯或对程序语言的不了解是应该好好打压打压了. 下面是参考网络资源总结的一些在Java编程中尽可能要做到的一些地方. 1.尽量在合适的场合使用单例 使用单例可以减轻加载的负担,缩短加载的时间,提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面: 控

高效 告别996,开启java高效编程之门 3-2传统方式处理业务逻辑

1 重点 1.1 对sort方法使用的理解 2 代码演练 需求: 根据第一章需求,女盆友提出需求* 1 打印所有商品* 2 图书类的商品一定给买* 3 最贵的买两件* 4 打印最贵的两件商品的名称和总价 测试类: package com.imooc.zhangxiaoxi.stream; import com.alibaba.fastjson.JSON; import com.imooc.zhangxiaoxi.lambda.cart.CartService; import com.imooc.

高效 告别996,开启java高效编程之门 3-7实战:常用中间操作演示之:过滤/映射/扁平化 filter/map/flagMap

1 重点 filter方法的使用 map方法的使用 flatMap方法的使用 forEach方法的使用 2 map和flatMap的区别: map的作用很容易理解就是对rdd之中的元素进行逐一进行函数操作映射为另外一个rdd. flatMap的操作是将函数应用于rdd之中的每一个元素,将返回的迭代器的所有内容构成新的rdd.通常用来切分单词,可用来单词计数 3 实战演示之过滤(filter): package com.imooc.zhangxiaoxi.stream; import com.al

我的高效编程秘诀

高效编程.高效学习.高效工作.高效生活 高效的意思是指在相同或更短的时间里完毕比其她人很多其它的任务,并且质量与其她人一样或者更好.用英文来说就是write less.do more.这是程序永远的主题,在我们日常编程中一段代码常常复用的时候我们会进行代码封装,借助一些工具来高速定位错误这些都有利于提高我们编程的效率.今天和小伙伴们聊聊高效编程的秘诀,事实上不仅仅是高效编程的秘诀,更是高效学习.生活和工作的秘诀,由于小编感觉编程就如同人生一样,一样的丰富多彩,一样的璀璨夺目.指导小编高效编程.生

39.JAVA编程思想之外篇——JAVA图形化设计精简大全一文覆盖

39.JAVA编程思想之外篇--JAVA图形化设计精简大全一文覆盖 欢迎转载,转载请标明出处:http://blog.csdn.net/notbaron/article/details/51204948 目录 Java图形化界面设计--容器(JFrame)...1 Java基本类(JFC)...1 l     AWTAbstract Window Toolkit(AWT)抽象窗口工具包... 2 l     Swing包... 2 l     AWT和Swing的区别... 6 Swing基本框

我的高效编程秘笈

准确来说是2012年11月的时候开始学编程的,那个时候我上大二,到现在学习编程快三年了,期间大部分时间处于自学状态,今天就跟大家聊聊我的高效编程秘笈. 一.活学活用office 首先请大家看清楚标题,我说的是office,不是wps,这两个差异还是非常大的,特别是excel中的差异非常明显.如果你能够熟练掌握Word和Excel的使用,这对你的编程效率会有质的提高. 说说我自己身上的一个事吧. 前些天公司产品要升级2.0,手机上有个数据表要根据服务端返回的字段重新设计,我把服务端返回的数据打印出

【编程之美】java实现重建二叉树

package com.cn.binarytree.utils; /** * @author 刘利娟 [email protected] * @version 创建时间:2014年7月20日 下午2:03:30 类说明: */ class Node { Node left; Node right; char chValue; Node(char chValue) { left = null; right = null; this.chValue = chValue; } } public cla

java 编程思想 22.11: java bean 案例代码

java 编程思想  22.11:   java bean 案例代码 thinking in java 4免费下载:http://download.csdn.net/detail/liangrui1988/7580155 package org.rui.swing.bean; import java.awt.Color; import java.awt.event.ActionListener; import java.awt.event.KeyListener; import org.rui.

[.NET] 《Effective C#》快速笔记 - C# 高效编程要点补充

<Effective C#>快速笔记 - C# 高效编程要点补充 目录 四十五.尽量减少装箱拆箱 四十六.为应用程序创建专门的异常类 四十七.使用强异常安全保证 四十八.尽量使用安全的代码 四十九.实现与 CLS 兼容的程序集 五十.实现小尺寸.高内聚的程序集 这是这一系列的最后一篇. 四十五.尽量减少装箱拆箱 值类型是数据的容器,不支持多态. 装箱把一个值类型放在一个未确定类型的引用对象中,让该值作为引用类型所使用.拆箱指从引用类型的位置取出值的一个副本. 装箱拆箱都是比较影响性能的手段,应