Java I/O回顾

作者: 思与学 | 来源:发表于2017-11-10 22:59 被阅读46次

    最近在学习netty,其中有对比bio、nio、netty使用上的不同,也趁此机会回顾了相关知识,加深下理解,主要涉及的有FileInputStream、FileOutputStream、BufferedInputStream、BufferedOutputStream、FileChannel、MappedByteBuffer等知识点。

    一、FileIn(Out)putStream(单字节)

    public static void fileCopy(String src,String dest) throws IOException{
        long t = System.currentTimeMillis();
        File file = new File(src);
        try(
            FileInputStream fis = new FileInputStream(file);
            FileOutputStream fos = new FileOutputStream(new File(dest));
        ){
            int result = 0;
            while((result = fis.read()) != -1){
                fos.write(result);
            }
        }
        System.err.println("file copy use time="+(System.currentTimeMillis()-t)+" file.size="+(file.length()/1024/1024)+"MB");
    }
    

    控制台输出:
    file copy use time=62065 file.size=20MB
    从输出接口可以看到,20MB文件copy用时一分钟多,可见不使用缓冲区,单字节读写时执行慢的无法接受。

    这里使用了try-with-resources的方式来进行流的自动关闭,对该知识点不熟悉的可参考Java 7中的Try-with-resources;

    二、FileIn(Out)putStream(字节数组)

    public static void fileCopyWithBuffer(String src,String dest) throws IOException{
        long t = System.currentTimeMillis();
        File file = new File(src);
        try(
            FileInputStream fis = new FileInputStream(file);
            FileOutputStream fos = new FileOutputStream(new File(dest));
        ){
            byte[] buffer = new byte[1024];
            int byteRead = 0;
            while((byteRead = fis.read(buffer)) != -1){
                fos.write(buffer,0,byteRead);
            }
        }
        System.err.println("fileCopyWithBuffer use time="+(System.currentTimeMillis()-t)+" file.size="+(file.length()/1024/1024)+"MB");
    }
    

    控制台输出:
    fileCopyWithBuffer use time=140 file.size=20MB
    可见使用buffer进行批量读写后,性能有了质的提高.

    下面我们来调整buffer数组的长度为10240,看下输出:
    fileCopyWithBuffer use time=45 file.size=20MB
    经过多次尝试,发现适当的增加buffer的长度,可以明显提升处理速度。

    三、BufferedIn(Out)putStream(单字节)

    public static void fileCopyWithBufferStream(String src,String dest) throws IOException{
        long t = System.currentTimeMillis();
        File file = new File(src);
        try(
            FileInputStream fis = new FileInputStream(file);
            FileOutputStream fos = new FileOutputStream(new File(dest));
            
            BufferedInputStream bis = new BufferedInputStream(fis);
            BufferedOutputStream bos = new BufferedOutputStream(fos);
        ){
            int result = 0;
            while((result =bis.read()) != -1){
                bos.write(result);
            }
        }
        System.err.println("fileCopyWithBufferStream use time="+(System.currentTimeMillis()-t)+" file.size="+(file.length()/1024/1024)+"MB");
    }
    

    控制台输出:
    fileCopyWithBufferStream use time=1199 file.size=20MB
    相对于单字节的流读写方式,处理时间有62s提升至1.2s,提升还是很大的。

    BufferedInputStream创建时内部会默认创建一个长度8192的byte数组,read方法实际上是在这个内存数组里面读取数据,下面我们看下read方法的源码:

    public synchronized int read() throws IOException {
        if (pos >= count) { // 当数据已读完时
            fill(); // 调用上层inputStream内read方法读取数据填充buf
            if (pos >= count) // 如果仍无可读数据,说明数据已读完
                return -1;
        }
        //从此处可看到read操作实际读取的是buf数组的数据
        //& 0xff的目的是消除后八位之前的位数据,保证读出的字节数据无变化,防止首位为1的情况转为int时高位为填充为1
        return getBufIfOpen()[pos++] & 0xff; 
    }
    
    • 当缓存区无可读数据时,调用fill方法,fill方法内部会考虑mark和reset的逻辑,设置新读入数据再数组内的存放位置
    • 调用InputStream内的read(byte b[], int off, int len)方法进行数据的“批量”读取;
    • 调用bis.read()时,从buffer内返回数据

    “批量”的读数据,从而提高了整体的执行速度。fill()方法的实现此处不再展开,可参考BufferedInputStream源码分析.

    BufferedOutputStream的write方法,也是先将数据存入buf,当buf存满时,将数据刷出;

    public synchronized void write(int b) throws IOException {
        if (count >= buf.length) {
            flushBuffer();
        }
        buf[count++] = (byte)b;
    }
    

    四、BufferedIn(Out)putStream(多字节)

    public static void fileCopyWithBufferStreamAndBuffer(String src,String dest) throws IOException{
        long t = System.currentTimeMillis();
        File file = new File(src);
        try(
            FileInputStream fis = new FileInputStream(file);
            FileOutputStream fos = new FileOutputStream(new File(dest));
            
            BufferedInputStream bis = new BufferedInputStream(fis);
            BufferedOutputStream bos = new BufferedOutputStream(fos);
        ){
            byte[] buffer = new byte[1024];
            int byteRead = 0;
            while((byteRead = bis.read(buffer)) != -1){
                bos.write(buffer,0,byteRead);// 1
                //bos.write(buffer); // 2
                // 1和2的区别是,最后一次读取的数据可能不能放满buffer,那样上一次留存的数据就会
                // 被一起写出,可以通过定义一个15长度的byte数组,写出时使用长度10的byte数组,就能看到结果了
            }
        }
        System.err.println("fileCopyWithBufferStreamAndBuffer use time="+(System.currentTimeMillis()-t)+" file.size="+(file.length()/1024/1024)+"MB");
    }
    

    控制台输出:
    fileCopyWithBufferStreamAndBuffer use time=99 file.size=20MB
    可见使用buffer进行批量读写后,性能有了质的提高.

    下面我们来调整buffer数组的长度为10240,看下输出:
    fileCopyWithBufferStreamAndBuffer use time=46 file.size=20MB
    经过多次尝试,发现适当的增加buffer的长度,可以明显提升处理速度。

    五、FileChannel

    public static void fileCopyWithChannel(String src,String dest) throws IOException{
        long t = System.currentTimeMillis();
        File file = new File(src);
        try(
            FileInputStream fis = new FileInputStream(file);
            FileOutputStream fos = new FileOutputStream(new File(dest));
            
            FileChannel in = fis.getChannel();
            FileChannel out = fos.getChannel();
        ){
            in.transferTo(0, in.size(), out);
        }
        System.err.println("fileCopyWithChannel use time="+(System.currentTimeMillis()-t)+" file.size="+(file.length()/1024/1024)+"MB");
    }
    

    控制台输出:
    fileCopyWithChannel use time=85 file.size=20MB

    从输出结果看,单纯的copy文件上,使用Channel并无明显的性能优势。
    ps:transferTo方法比较耗费内存资源,建议只在小文件或小使用量时使用,避免出现资源不足等异常。

    NIO的关键点是通道和缓冲区,这里不再过多展开,NIO入门推荐Java NIO 系列教程

    六、FileChannel(ByteBuffer)

    public static void nioBufferCopy(String src,String dest) throws IOException {  
        long t = System.currentTimeMillis();
        File file = new File(src);
        try(
                FileInputStream fis = new FileInputStream(file);
            FileOutputStream fos = new FileOutputStream(new File(dest));
            
            FileChannel in = fis.getChannel();
            FileChannel out = fos.getChannel();
            ){
            ByteBuffer buffer = ByteBuffer.allocate(4096);  
            while (in.read(buffer) != -1) {  
                buffer.flip();  
                out.write(buffer);  
                buffer.clear();  
            }  
        } 
        
        System.err.println("nioBufferCopy use time="+(System.currentTimeMillis()-t)+" file.size="+(file.length()/1024/1024)+"MB");
    }  
    

    控制台输出:
    nioBufferCopy use time=182 file.size=20MB

    下面我们来调整buffer数组的长度为40960,看下输出:
    nioBufferCopy use time=60 file.size=20MB

    七、FileChannel(MappedByteBuffer)

    public static void nioMappedByteBufferCopy(String src,String dest) throws IOException {  
        long t = System.currentTimeMillis();
        File file = new File(src);
        try(
                FileInputStream fis = new FileInputStream(file);
            FileOutputStream fos = new FileOutputStream(new File(dest));
            
            FileChannel in = fis.getChannel();
            FileChannel out = fos.getChannel();
            ){  
            MappedByteBuffer  mappedByteBuffer = in.map(MapMode.READ_ONLY, 0, in.size());
            out.write(mappedByteBuffer);  
        } 
        
        System.err.println("nioMappedByteBufferCopy use time="+(System.currentTimeMillis()-t)+" file.size="+(file.length()/1024/1024)+"MB");
    } 
    

    控制台输出:
    nioMappedByteBufferCopy use time=50 file.size=20MB
    使用内存映射文件的方式速度还是比较理想的,更多内容推荐阅读深入浅出MappedByteBuffer,另外,推荐关注占小狼,他写的精品文章很多!

    八、Buffered(Reader)Writer

    字符流的使用相对简单,这里给出一个示例代码。

    public static void bufferReaderWriterTest() throws IOException{
        List<String> list = new ArrayList<String>(20);
        for(int i=0;i<20;i++){
            list.add(""+i);
        }
        File file = new File("brwtest.txt");
        try(
                
            BufferedWriter bw = new BufferedWriter(new FileWriter(file));
            BufferedReader br = new BufferedReader(new FileReader(file));
        ){
            for (String item : list) {
                bw.write(item);
                bw.newLine();
            }
            //此处不调用flush方法,下边读取时内容会为空
            bw.flush();
            
            String line = null;
            while ((line = br.readLine()) != null) {
                System.err.println(line);
            }
            
            //org.apache.commons.io.IOUtils,有许多工具包内提供了IO操作方法,可以优先考虑使用
    //          List<String> lines = IOUtils.readLines(br);
    //          for (String string : lines) {
    //              System.err.println(string);
    //          }
        }
    }
    

    小结

    我个人写这篇文章或者说写这些demo代码的收获有:

    1. 自以为很熟悉的I/O类,动起手来才发现自己离“顺手拈来”还有一段不小的差距;
    2. 从源码层面了解不同流的实现方式,更深一层的提高了自己对java I/O知识的理解;
    3. 发现了过往所学知识的弱点,看别人博客进行学习,大致知其然,未系统梳理,未动手练习,未理解其实现方式和原理,浮于表面,随着时间的流逝,这些知识成了零散模糊的片段

    I/O是java中基础的知识点,你不妨也闭起眼思考下你能使用多少种不同的方式实现文件复制,或许你会有不同的发现!

    ps:本机mac air,测试代码有其随意性,copy速度仅能体现量级上的趋势,以供参考。

    相关文章

      网友评论

      • 知识学者:复制不就是字节流,和字符流 二种嘛。
        对于二进制文件只能字节流,对于文本文件可以采用字符流。。
        思与学:@东风冷雪 我之前也是这样的认识,而且总体来说也是正确的,文章最后只是希望能把理解从字节流和字符流细化为具体的api,了解不同流的使用和实现方式

      本文标题:Java I/O回顾

      本文链接:https://www.haomeiwen.com/subject/ldgimxtx.html