JAVA IO分析三:IO总结&文件分割与合并实例
时间飞逝,马上就要到2018年了,今天我们将要学习的是IO流学习的最后一节,即总结回顾前面所学,并学习一个案例用于前面所学的实际操作,下面我们就开始本节的学习:
一、原理与概念
一、概念
流:流动 、流向 从一端移动到另一端 源头与目的地
程序 与 文件|数组|网络连接|数据库 ,以程序为中心
二、IO流分类
1、流向: 输入流与输出流
2、数据:字节流:二进制,可以一切文件 包括 纯文本 doc 音频、视频等等
字符流:文本文件,只能处理纯文本
3、功能:节点:包裹源头
处理:增强功能,提供性能
三、字符流与字节流 (重点) 与文件
1、字节流
输入流:InputStream read(byte[] b) 、read(byte[] b, int off, int len) +close()
FileInputStream()
输出流:OutputStream write(byte[] b) write(byte[] b, int off, int len) +flush() +close()
FileOutputStream
2、字符流
输入流:Reader read(char[] cbuf) read(char[] cbuf, int off, int len) +close()
FileReader()
输出流:Writer write(char[] cbuf) write(char[] cbuf, int off, int len) +flush() +close()
write(String str, int off, int len)
FileWriter()
四、操作
1、举例:搬家 -->读取文件
1)、关联房子 --->建立与文件联系
2)、选择搬家 -->选择对应流
3)、搬家 -->读取|写出
a)、卡车大小 --->数组大小
b)、运输 -->读取、写出
4)、打发over -->释放资源
2、操作
1)建立联系
2)选择流
3)操作 数组大小+read 、write
4)释放资源
二、字节流
字节流:可以处理一切文件,包括二进制 音频、视频 、doc等
节点流: InputStream FileInputStream
OutputStream FileOutputStream
一、读取文件
1、建立联系 File对象 源头
2、选择流 文件输入流 InputStream FileInputStream
3、操作 : byte[] car =new byte[1024]; +read+读取大小
输出
4、释放资源 :关闭
二、写出文件
1、建立联系 File对象 目的地
2、选择流 文件输出流 OutputStream FileOutputStream
3、操作 : write() +flush
4、释放资源 :关闭
三、文件拷贝 程序为桥梁
1、建立联系 File对象 源头 目的地
2、选择流
文件输入流 InputStream FileInputStream
文件输出流 OutputStream FileOutputStream
3、操作 : 拷贝
byte[] flush =new byte[1024];
int len =0;
while(-1!=(len=输入流.read(flush))){
输出流.write(flush,0,len)
}
输出流.flush
4、释放资源 :关闭 两个流
四、文件夹拷贝
1、递归查找子孙级文件|文件夹
2、文件 复制(IO流复制)
文件夹 创建
3、 A
/ \
a.txt b
|
b.txt
AA
|
A
/ \
a.txt b
|
b.txt
4、不能将父目录拷贝到子目录中
删除超长目录
三、字符流
字符流:只能处理 纯文本,全部为可见字符 .txt .html
节点流 Reader FileReader
Writer FileWriter
一、纯文本读取
1、建立联系
2、选择流 Reader FileReader
3、读取 char[] flush =new char[1024];
4、关闭
二、纯文本写出
1、建立联系
2、选择流 Writer FileWriter
3、读取 write(字符数组,0,长度)+flush
write(字符串)
append(字符|字符串)
4、关闭
四、处理流
处理流:增强功能、提供性能,节点流之上
一、缓冲流
1)、字节缓冲流
BufferedInputStream
BufferedOutputStream
2)、字符缓冲流
BufferedReader readLine()
BufferedWriter newLine()
二、转换流: 字节流 转为字符流 处理乱码(编码集、解码集)
1、编码与解码概念
编码: 字符 ---编码字符集>二进制
解码 : 二进制 --解码字符集-> 字符
2、乱码:
1)编码与解码的字符集不统一
2)字节缺少,长度丢失
3、文件乱码
InputStreamReader(字节输入流,"解码集")
OutputStreamWriter(字符输出流,"编码集")
五、其它流
一、节点流
1、字节数组 字节 节点流
输入流:ByteArrayInputStream read(byte[] b, int off, int len) + close()
输出流:ByteArrayOutputStream write(byte[] b, int off, int len) +toByteArray() 不要使用多态
二、处理流
1、基本类型+String 保留数据+类型
输入流:DataInputStream readXxx
输出流:DataOutputStream writeXxx
2、引用类型 (对象) 保留数据+类型
反序列化 输入流:ObjectInputStream readObject()
序列化 输出流:ObjectOutputStream writeObject()
注意:
1)、先序列化后反序列化;反序列化顺序必须与序列化一致
2)、不是所有的对象都可以序列化, java.io.Serializable
不是所有的属性都需要序列化,transient
3、打印流 PrintStream println() print()
4、三个常量 : System.in /out/err System.setIn() setOut() setErr()
六、小节
一、步骤: 创建源 选择流 操作(读取|写出) 释放
二、流
三、重点
四、操作
0、打印文件|目录
1、文件拷贝
2、关闭流方法
3、文件分割与合并(自学)
七、实例:文件分割与合并
目的:将文件分割成数个部分,然后再将它们合并起来
首先文件的分割,有下面几个要点
1.先要确定的两个因素就是,分成多少块,每块多大,那么最后一块的大小不一定刚好能是你规定的每小块的大小,那么最后一块的大小就比较特殊,它等于文件总大小(块数-1)乘以每块大小
2.在操作源文件到目的文件,即被分割文件到分割文件时,实际上就是文件的拷贝过程
3.最关键的问题是如何控制文件输入流,它必须按照指定的位置读取每一个分块
比如,我有142k大小的文件,要将他们分割成3块,规定每块大小为50,那么我将第一块内容读取的时候,是从0-50K
当读第二快内容时就变成了50-100K,那么如何控制读取范围?
这里就需要用到RandomAccessFile类,它提供了一个seek方法,可以指定读取的开始位置
文件的合并就是将那些分块重新组合在一起,比文件分割考虑的因素少,这里提供了两种不同的方式进行文件合并,其实也就是关于输入流做出的改变,详细见下文,合并从小块文件到大文件,其实也就是文件追加输出
本着OOP的设计思想,将对文件的信息和操作方法封装成一个类 SplitFile
这个类包含了关于文件的一些基本信息,文件的路径,大小,名称等,以及为了分割需要的一些信息,分割块数,每块的大小,分割文件所在的目录文件,各自的名称等,下面开始一步一步实现代码
1.类属性+构造方法
在构造方法运行结束后,初始化也结束,关于每块大小可以在创建对象的时候设置,也可以使用默认的1024
public class SplitFile { //文件路径 private String filePath; //文件名称 private String fileName; //文件大小 private long length; //块数 private int size; //每块大小 private long blockSize; //每块名称 private List<String> blockPath; //分割后的目录 private String destPath; public SplitFile() { this.blockPath=new ArrayList<String>(); } public SplitFile(String filePath,String destPath){ this(filePath,1024,destPath); } public SplitFile(String filePath, long blockSize,String destPath) { this(); this.filePath = filePath; this.blockSize = blockSize; this.destPath=destPath; init(); } }
2.初始化分割信息
确定分割块数,先得到文件长度length
size=(int)(Math.ceil(length*1.0/this.blockSize));
确定部分文件的名称
由于将所有小文件的名称存放在List中,所以这里用一个for循环,往List中添加元素
其中destPath是存放部分文件的目录
for(int i=0;i<this.size;i++){
this.blockPath.add(destPath+"/"+this.fileName+".part"+i);
}
3.文件的分割
这里需要两个很关键的值,beginPos和actualBlockSize,每个部分文件开始的位置,以及它的大小
由于每一个分块这些信息都不完全相同,所以需要分别处理
使用for循环遍历所有的块,需要注意的是,我们的size是向下取整,也就是说当计算的值为3.6时,size的值为3,但是这里的i从0开始,所以一切正常
long beginPos=0;
long actualBlockSize=blockSize;
for(int i=0;i<size;i++){
//计算最后一块大小
if(i==size-1){
actualBlockSize=this.length-beginPos;
}
spiltDetail(i,beginPos,actualBlockSize);
beginPos+=actualBlockSize;
}
spiltDetail方法其实就是真正文件分割的过程,其实也就是文件拷贝,4个步骤
选择目标文件和源文件;选择输入输出流;进行拷贝;关闭流
在拷贝过程中,需要注意不能一直都以固定的缓冲长度来写出数据,需要进行判断
while (-1 != (len = raf.read(flush))) {
if(actualBlockSize-len>=0){
bos.write(flush,0,len);
actualBlockSize-=len;
}else{
bos.write(flush,0,(int)actualBlockSize);
break;
}
}
文件的合并就比较简单了,第二种方法中用到了SequenceInputStream类,将很多个输入流集中在一起,只所有有很多个输入流是由于每一个部分文件对应一个输入流
要使用这个类就要先有一个集合,这里用Vector<InputStream>创建一个带泛型的Vector对象
使用for循环将文件输入流添加到容器中,然后构建SequenceInputStream对象
SequenceInputStream sis = new SequenceInputStream(vi.elements());
为了增强代码的健壮性,还添加了一些判断,下面是全部代码
package com.bjsxt.io.others; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.io.SequenceInputStream; import java.util.ArrayList; import java.util.List; import java.util.Vector; import com.bjsxt.io.util.FileUtil; public class SplitFile { //文件的路径 private String filePath; //文件名 private String fileName; //文件大小 private long length; //块数 private int size; //每块的大小 private long blockSize; //分割后的存放目录 private String destBlockPath; //每块的名称 private List<String> blockPath; public SplitFile(){ blockPath = new ArrayList<String>(); } public SplitFile(String filePath,String destBlockPath){ this(filePath,destBlockPath,1024); } public SplitFile(String filePath,String destBlockPath,long blockSize){ this(); this.filePath= filePath; this.destBlockPath =destBlockPath; this.blockSize=blockSize; init(); } /** * 初始化操作 计算 块数、确定文件名 */ public void init(){ File src =null; //健壮性 if(null==filePath ||!(((src=new File(filePath)).exists()))){ return; } if(src.isDirectory()){ return ; } //文件名 this.fileName =src.getName(); //计算块数 实际大小 与每块大小 this.length = src.length(); //修正 每块大小 if(this.blockSize>length){ this.blockSize =length; } //确定块数 size= (int)(Math.ceil(length*1.0/this.blockSize)); //确定文件的路径 initPathName(); } private void initPathName(){ for(int i=0;i<size;i++){ this.blockPath.add(destBlockPath+"/"+this.fileName+".part"+i); } } /** * 文件的分割 * 0)、第几块 * 1、起始位置 * 2、实际大小 * @param destPath 分割文件存放目录 */ public void split(){ long beginPos =0; //起始点 long actualBlockSize =blockSize; //实际大小 //计算所有块的大小、位置、索引 for(int i=0;i<size;i++){ if(i==size-1){ //最后一块 actualBlockSize =this.length-beginPos; } spiltDetail(i,beginPos,actualBlockSize); beginPos+=actualBlockSize; //本次的终点,下一次的起点 } } /** * 文件的分割 输入 输出 * 文件拷贝 * @param idx 第几块 * @param beginPos 起始点 * @param actualBlockSize 实际大小 */ private void spiltDetail(int idx,long beginPos,long actualBlockSize){ //1、创建源 File src = new File(this.filePath); //源文件 File dest = new File(this.blockPath.get(idx)); //目标文件 //2、选择流 RandomAccessFile raf = null; //输入流 BufferedOutputStream bos=null; //输出流 try { raf=new RandomAccessFile(src,"r"); bos =new BufferedOutputStream(new FileOutputStream(dest)); //读取文件 raf.seek(beginPos); //缓冲区 byte[] flush = new byte[1024]; //接收长度 int len =0; while(-1!=(len=raf.read(flush))){ if(actualBlockSize-len>=0){ //查看是否足够 //写出 bos.write(flush, 0, len); actualBlockSize-=len; //剩余量 }else{ //写出最后一次的剩余量 bos.write(flush, 0, (int)actualBlockSize); break; } } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally{ FileUtil.close(bos,raf); } } /** * 文件的合并 */ public void merge(String destPath){ //创建源 File dest =new File(destPath); //选择流 BufferedOutputStream bos=null; //输出流 SequenceInputStream sis =null ;//输入流 //创建一个容器 Vector<InputStream> vi = new Vector<InputStream>(); try { for (int i = 0; i < this.blockPath.size(); i++) { vi.add(new BufferedInputStream(new FileInputStream(new File(this.blockPath.get(i))))); } bos =new BufferedOutputStream(new FileOutputStream(dest,true)); //追加 sis=new SequenceInputStream(vi.elements()); //缓冲区 byte[] flush = new byte[1024]; //接收长度 int len =0; while(-1!=(len=sis.read(flush))){ bos.write(flush, 0, len); } bos.flush(); FileUtil.close(sis); } catch (Exception e) { }finally{ FileUtil.close(bos); } } /** * 文件的合并 */ public void merge1(String destPath){ //创建源 File dest =new File(destPath); //选择流 BufferedOutputStream bos=null; //输出流 try { bos =new BufferedOutputStream(new FileOutputStream(dest,true)); //追加 BufferedInputStream bis = null; for (int i = 0; i < this.blockPath.size(); i++) { bis = new BufferedInputStream(new FileInputStream(new File(this.blockPath.get(i)))); //缓冲区 byte[] flush = new byte[1024]; //接收长度 int len =0; while(-1!=(len=bis.read(flush))){ bos.write(flush, 0, len); } bos.flush(); FileUtil.close(bis); } } catch (Exception e) { }finally{ FileUtil.close(bos); } } /** * @param args */ public static void main(String[] args) { SplitFile split = new SplitFile("E:/xp/20130502/test/学员设置(20130502).xls","E:/xp/20130502",51); //System.out.println(split.size); //split.split(); split.merge("E:/xp/20130502/test1.xls"); } }
八、实例:思考如何多线程读取大文件?
参考:https://www.cnblogs.com/metoy/p/4470418.html
一、对文件分区
为了充分利用多线程读取,就需要把文件划分成多个区域,供每个线程读取。那么就需要有一个算法来计算出每个线程读取的开始位置和结束位置。那么首先根据配置的线程数和文件的总长度计,算出每个线程平均分配的读取长度。但是有一点,由于文件是纯文本文件,必须按行来处理,如果分割点在某一行中间,那么这一行数据就会被分成两部分,分别由两个线程同时处理,这种情况是不能出现的。所以各个区域的结束点上的字符必须是换行符。第一个区域的开始位置是0,结束位置首先设为(文件长度/线程数),如果结束点位置不是换行符,就只能加1,直到是换行符位置。第一个区域的结束位置有了,自然我们就能求出第二个区域的开始位置了,同理根据上边算法求出第二个区域的结束位置,然后依次类推第三个、第四个......
上边的算法中,第一个区域的结束位置定了,才能有第二个区域的开始位置,第二个区域的结束位置定了,才能有第三个区域的开始位置,依次这么下去。照这种规律,自然地想到的是用递归来解决。(详情看源码)
二、内存文件映射
简单说一下内存文件映射:
内存文件映射,简单地说就是将文件映射到内存的某个地址上。 要理解内存文件映射,首先得明白普通方式读取文件的流程: 首先内存空间分为内核空间和用户空间,在应用程序读取文件时,底层会发起系统调用,由系统调用将数据先读入到内核空间,然后再将数据拷贝到应用程序的用户空间供应用程序使用。这个过程多了一个从内核空间到用户空间拷贝的过程。 如果使用内存文件映射,文件会被映射到物理内存的某个地址上(不是数据加载到内存),此时应用程序读取文件的地址就是一个内存地址,而这个内存地址会被映射到了前面说到的物理内存的地址上。应用程序发起读之后,如果数据没有加载,系统调用就会负责把数据从文件加载到这块物理地址。应用程序便可以读取到文件的数据。省去了数据从内核空间到用户空间的拷贝过程。所以速度上也会有所提高。 |
读取大文件的实现中,就是用了Java的内存映射API,这样我们就可以在要读取某个地址时再将内容加载到内存。不需要一下子全部将内容加载进来。
源码样例如下:
import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.io.UnsupportedEncodingException; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel.MapMode; import java.util.HashSet; import java.util.Set; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicLong; public class BigFileReader { private int threadSize; private String charset; private int bufferSize; private IHandle handle; private ExecutorService executorService; private long fileLength; private RandomAccessFile rAccessFile; private Set<StartEndPair> startEndPairs; private CyclicBarrier cyclicBarrier; private AtomicLong counter = new AtomicLong(0); private BigFileReader(File file,IHandle handle,String charset,int bufferSize,int threadSize){ this.fileLength = file.length(); this.handle = handle; this.charset = charset; this.bufferSize = bufferSize; this.threadSize = threadSize; try { this.rAccessFile = new RandomAccessFile(file,"r"); } catch (FileNotFoundException e) { e.printStackTrace(); } this.executorService = Executors.newFixedThreadPool(threadSize); startEndPairs = new HashSet<BigFileReader.StartEndPair>(); } public void start(){ long everySize = this.fileLength/this.threadSize; try { calculateStartEnd(0, everySize); } catch (IOException e) { e.printStackTrace(); return; } final long startTime = System.currentTimeMillis(); cyclicBarrier = new CyclicBarrier(startEndPairs.size(),new Runnable() { @Override public void run() { System.out.println("use time: "+(System.currentTimeMillis()-startTime)); System.out.println("all line: "+counter.get()); } }); for(StartEndPair pair:startEndPairs){ System.out.println("分配分片:"+pair); this.executorService.execute(new SliceReaderTask(pair)); } //需要再此处加上判断,上述线程是否完毕,然后结束操作shutdown } private void calculateStartEnd(long start,long size) throws IOException{ if(start>fileLength-1){ return; } StartEndPair pair = new StartEndPair(); pair.start=start; long endPosition = start+size-1; if(endPosition>=fileLength-1){ pair.end=fileLength-1; startEndPairs.add(pair); return; } rAccessFile.seek(endPosition); byte tmp =(byte) rAccessFile.read(); while(tmp!='\n' && tmp!='\r'){ endPosition++; if(endPosition>=fileLength-1){ endPosition=fileLength-1; break; } rAccessFile.seek(endPosition); tmp =(byte) rAccessFile.read(); } pair.end=endPosition; startEndPairs.add(pair); calculateStartEnd(endPosition+1, size); } public void shutdown(){ try { this.rAccessFile.close(); } catch (IOException e) { e.printStackTrace(); } this.executorService.shutdown(); } private void handle(byte[] bytes) throws UnsupportedEncodingException{ String line = null; if(this.charset==null){ line = new String(bytes); }else{ line = new String(bytes,charset); } if(line!=null && !"".equals(line)){ this.handle.handle(line); counter.incrementAndGet(); } } private static class StartEndPair{ public long start; public long end; @Override public String toString() { return "star="+start+";end="+end; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (int) (end ^ (end >>> 32)); result = prime * result + (int) (start ^ (start >>> 32)); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; StartEndPair other = (StartEndPair) obj; if (end != other.end) return false; if (start != other.start) return false; return true; } } private class SliceReaderTask implements Runnable{ private long start; private long sliceSize; private byte[] readBuff; /** * @param start read position (include) * @param end the position read to(include) */ public SliceReaderTask(StartEndPair pair) { this.start = pair.start; this.sliceSize = pair.end-pair.start+1; this.readBuff = new byte[bufferSize]; } @Override public void run() { try { MappedByteBuffer mapBuffer = rAccessFile.getChannel().map(MapMode.READ_ONLY,start, this.sliceSize); ByteArrayOutputStream bos = new ByteArrayOutputStream(); for(int offset=0;offset<sliceSize;offset+=bufferSize){ int readLength; if(offset+bufferSize<=sliceSize){ readLength = bufferSize; }else{ readLength = (int) (sliceSize-offset); } mapBuffer.get(readBuff, 0, readLength); for(int i=0;i<readLength;i++){ byte tmp = readBuff[i]; if(tmp=='\n' || tmp=='\r'){ handle(bos.toByteArray()); bos.reset(); }else{ bos.write(tmp); } } } if(bos.size()>0){ handle(bos.toByteArray()); } cyclicBarrier.await();//测试性能用 }catch (Exception e) { e.printStackTrace(); } } } public static class Builder{ private int threadSize=1; private String charset=null; private int bufferSize=1024*1024; private IHandle handle; private File file; public Builder(String file,IHandle handle){ this.file = new File(file); if(!this.file.exists()) throw new IllegalArgumentException("文件不存在!"); this.handle = handle; } public Builder withTreahdSize(int size){ this.threadSize = size; return this; } public Builder withCharset(String charset){ this.charset= charset; return this; } public Builder withBufferSize(int bufferSize){ this.bufferSize = bufferSize; return this; } public BigFileReader build(){ return new BigFileReader(this.file,this.handle,this.charset,this.bufferSize,this.threadSize); } } }
public interface IHandle { public void handle(String line); }
public class Main { public static void main(String[] args) { BigFileReader.Builder builder = new BigFileReader.Builder("d:/reliability.txt",new IHandle() { @Override public void handle(String line) { //System.out.println(line); //increat(); } }); builder.withTreahdSize(10) .withCharset("gbk") .withBufferSize(1024*1024); BigFileReader bigFileReader = builder.build(); bigFileReader.start(); } }
IO学习视频资料:尚学堂_裴新老师