Java I/O回顾

最近在学习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速度仅能体现量级上的趋势,以供参考。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,265评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,078评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,852评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,408评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,445评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,772评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,921评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,688评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,130评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,467评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,617评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,276评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,882评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,740评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,967评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,315评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,486评论 2 348

推荐阅读更多精彩内容