CharBuffer与char[]的区别

30

下面这种情况,是否有理由更喜欢使用 CharBuffer 而不是 char[]

CharBuffer buf = CharBuffer.allocate(DEFAULT_BUFFER_SIZE);
while( in.read(buf) >= 0 ) {
  out.append( buf.flip() );
  buf.clear();
}

对比。

char[] buf = new char[DEFAULT_BUFFER_SIZE];
int n;
while( (n = in.read(buf)) >= 0 ) {
  out.write( buf, 0, n );
}
(其中in是一个Reader,而out是一个Writer)?
7个回答

19

在这种情况下,真的没有理由优先选择CharBuffer

一般来说,CharBuffer(以及ByteBuffer)可以真正简化API并鼓励正确处理。如果您正在设计公共API,则绝对值得考虑使用面向缓冲区的API。


7

我想进行这个比较的小型基准测试。

下面是我编写的类。

问题在于,我无法相信CharBuffer表现得如此糟糕。我做错了什么?

编辑:自从下面的第11条评论以来,我已经编辑了代码和输出时间,整体性能更好,但仍存在显着的时间差异。我还尝试了在评论中提到的out2.append((CharBuffer)buff.flip())选项,但它比下面代码中使用的写入选项慢得多。

结果:(时间以毫秒为单位)
char[] : 3411
CharBuffer: 5653

public class CharBufferScratchBox
{
    public static void main(String[] args) throws Exception
    {
        // Some Setup Stuff
        String smallString =
                "1111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000";

        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < 1000; i++)
        {
            stringBuilder.append(smallString);
        }
        String string = stringBuilder.toString();
        int DEFAULT_BUFFER_SIZE = 1000;
        int ITTERATIONS = 10000;

        // char[]
        StringReader in1 = null;
        StringWriter out1 = null;
        Date start = new Date();
        for (int i = 0; i < ITTERATIONS; i++)
        {
            in1 = new StringReader(string);
            out1 = new StringWriter(string.length());

            char[] buf = new char[DEFAULT_BUFFER_SIZE];
            int n;
            while ((n = in1.read(buf)) >= 0)
            {
                out1.write(
                        buf,
                        0,
                        n);
            }
        }
        Date done = new Date();
        System.out.println("char[]    : " + (done.getTime() - start.getTime()));

        // CharBuffer
        StringReader in2 = null;
        StringWriter out2 = null;
        start = new Date();
        CharBuffer buff = CharBuffer.allocate(DEFAULT_BUFFER_SIZE);
        for (int i = 0; i < ITTERATIONS; i++)
        {
            in2 = new StringReader(string);
            out2 = new StringWriter(string.length());
            int n;
            while ((n = in2.read(buff)) >= 0)
            {
                out2.write(
                        buff.array(),
                        0,
                        n);
                buff.clear();
            }
        }
        done = new Date();
        System.out.println("CharBuffer: " + (done.getTime() - start.getTime()));
    }
}

是的。我之前使用的时间是1.5倍,但在1.6中获得的时间更快。正如你(@Alnitak)所报告的那样,大约快了35%。 - Ron Tuffin
为什么要写成write(buf.array(),0,n)而不是write(buf.flip())? - Chris Conway
因为Write.append()也不存在 - .append()方法只存在于StringWriter子类中。 - Alnitak
1
好的,所以我用out2.append((CharBuffer)buff.flip());替换了out2.write(buff.array(),0,n);,结果时间比较变差了,增加了135% - 哎呀!在这种情况下使用char[]显然更快。 :) - Ron Tuffin
1
你的微基准测试测量了太多的东西。StringWriter在没有参数的情况下被分配,因此它必须重新调整大小。StringWriter由StringBuffer支持,并默认为16。尝试使用参数string.length()进行分配。 - James Schek
显示剩余6条评论

4

如果您只是在使用缓冲区时做这一件事情,那么在这种情况下数组可能是更好的选择。

CharBuffer有很多额外的功能,但在这种情况下都不相关,并且只会稍微减慢速度。

如果需要使事情变得更加复杂,您可以随时进行重构。


1
这符合当前的需求。需求会发生变化。 - Ed S.
1
使用标准实现(敢说模式 :-))可以减少出错的代码。这并不意味着使用数组是错误或有缺陷的,只是更容易出现这种情况。 - Michael Rutherfurd
6
记录一下,我不同意“第一次就把它做对”。对于这个需求,第一次就把它做对,同时有信心在需求改变时可以进行修改,这样做要比第一次就做对好得多,前提是你能让环境支持这种理念。 - Bill Michell

4

实际上,实践中的差异不到10%,而不是其他人所报道的30%。

使用分析器读写一个5MB文件24次,我的数据平均如下:

char[] = 4139 ms
CharBuffer = 4466 ms
ByteBuffer = 938 (direct) ms

个人测试多次支持CharBuffer。

我还尝试将基于文件的IO替换为内存中的IO,性能类似。如果您正在尝试从一个本地流传输到另一个本地流,则最好使用“直接”ByteBuffer。

在实践中,不到10%的性能差异,我更喜欢CharBuffer。它的语法更清晰,没有多余的变量,并且可以进行更直接的操作(即任何要求CharSequence的内容)。

以下是基准测试...它略有错误,因为BufferedReader是在测试方法内分配的,而不是外部...但是,下面的示例允许您隔离IO时间并消除像字符串或字节流调整其内部内存缓冲区等因素。

public static void main(String[] args) throws Exception {
    File f = getBytes(5000000);
    System.out.println(f.getAbsolutePath());
    try {
        System.gc();
        List<Main> impls = new java.util.ArrayList<Main>();
        impls.add(new CharArrayImpl());
        //impls.add(new CharArrayNoBuffImpl());
        impls.add(new CharBufferImpl());
        //impls.add(new CharBufferNoBuffImpl());
        impls.add(new ByteBufferDirectImpl());
        //impls.add(new CharBufferDirectImpl());
        for (int i = 0; i < 25; i++) {
            for (Main impl : impls) {
                test(f, impl);
            }
            System.out.println("-----");
            if(i==0)
                continue; //reset profiler
        }
        System.gc();
        System.out.println("Finished");
        return;
    } finally {
        f.delete();
    }
}
static int BUFFER_SIZE = 1000;

static File getBytes(int size) throws IOException {
    File f = File.createTempFile("input", ".txt");
    FileWriter writer = new FileWriter(f);
    Random r = new Random();
    for (int i = 0; i < size; i++) {
        writer.write(Integer.toString(5));
    }
    writer.close();
    return f;
}

static void test(File f, Main impl) throws IOException {
    InputStream in = new FileInputStream(f);
    File fout = File.createTempFile("output", ".txt");
    try {
        OutputStream out = new FileOutputStream(fout, false);
        try {
            long start = System.currentTimeMillis();
            impl.runTest(in, out);
            long end = System.currentTimeMillis();
            System.out.println(impl.getClass().getName() + " = " + (end - start) + "ms");
        } finally {
            out.close();
        }
    } finally {
        fout.delete();
        in.close();
    }
}

public abstract void runTest(InputStream ins, OutputStream outs) throws IOException;

public static class CharArrayImpl extends Main {

    char[] buff = new char[BUFFER_SIZE];

    public void runTest(InputStream ins, OutputStream outs) throws IOException {
        Reader in = new BufferedReader(new InputStreamReader(ins));
        Writer out = new BufferedWriter(new OutputStreamWriter(outs));
        int n;
        while ((n = in.read(buff)) >= 0) {
            out.write(buff, 0, n);
        }
    }
}

public static class CharBufferImpl extends Main {

    CharBuffer buff = CharBuffer.allocate(BUFFER_SIZE);

    public void runTest(InputStream ins, OutputStream outs) throws IOException {
        Reader in = new BufferedReader(new InputStreamReader(ins));
        Writer out = new BufferedWriter(new OutputStreamWriter(outs));
        int n;
        while ((n = in.read(buff)) >= 0) {
            buff.flip();
            out.append(buff);
            buff.clear();
        }
    }
}

public static class ByteBufferDirectImpl extends Main {

    ByteBuffer buff = ByteBuffer.allocateDirect(BUFFER_SIZE * 2);

    public void runTest(InputStream ins, OutputStream outs) throws IOException {
        ReadableByteChannel in = Channels.newChannel(ins);
        WritableByteChannel out = Channels.newChannel(outs);
        int n;
        while ((n = in.read(buff)) >= 0) {
            buff.flip();
            out.write(buff);
            buff.clear();
        }
    }
}

2
我认为CharBuffer和ByteBuffer(以及任何其他xBuffer)都是为了可重用性而设计的,因此您可以使用buf.clear()而不是每次重新分配内存。
如果您不重复使用它们,则无法充分利用它们的潜力,这将增加额外的开销。但是,如果您计划扩展此功能,则将它们保留在那里可能是一个好主意。

2
缓冲区的全部潜力包括直接缓冲区和轻松转换数据表示的能力。您可以使用 ByteArray.asTYPE() 将字节即时转换为数字或字符串。您还可以更改字节顺序。 - James Schek

1

CharBuffer版本稍微简单一些(少了一个变量),封装了缓冲区大小处理,并使用标准API。通常我更喜欢这个版本。

然而,在某些情况下,仍有一个很好的理由选择数组版本。CharBuffer只在Java 1.4中引入,因此如果您部署到早期版本,则无法使用Charbuffer(除非您自己编写/使用后移版)。

P.S 如果您使用后移版,请记得在追赶包含后移代码“真正”版本的版本后将其删除。


1
你应该避免在最新的Java版本中使用CharBuffer,因为在subsequence()中存在一个bug。由于实现混淆了容量和剩余量,因此无法从缓冲区的后半部分获取子序列。我观察到这个问题出现在Java 6-0-11和6-0-12中。

3
Java 8已经发布了,你能否更新一下这个答案? - james.garriss

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接