转载:http://blog.csdn.net/u012250875/article/details/78341874
1.IO相关概念一览
1.1 什么是IO
所谓IO即input和output的缩写,是对数据的流入和流出的一种抽象,编程中很常见的一个概念。
1.2 什么是流
体会一下这几个词:水流(静止的水想必没人会叫水流),物流,人流(此人流非彼人流 = =!),可以发现流的特点:动态的,可转移的,从一处到另一处的
1.3 java io
java为了我们调用方便,而屏蔽输入/输出源和流动细节,抽象出的用于解决数据流动问题的类体系,这就是java的io流
1.4 输入流和输出流
用于读取的流称为输入流(输入流只能用来读),用于写入的流称为输出流(输出流只能用来写)。输入输出的概念一般是针对内存来说的,流(写)入内存,从内存流(读)出。
1.5 字节流和字符流
输入输出流可操作最小单位来区分字节流和字符流,最小操作单位是一个字节(8bit)的为字节流,最小操作单位为一个字符(16bit)的为字符流,java io体系中字节操作流以stream结尾,字符操作流以reader和writer结尾
1.6 节点流和包装(处理)流
1)节点流偏向实现细节,直接与细节打交道,比如FileInputStream,而包装(处理)流偏功能,以目标功能为抽象,比如PrintStream。
2)区分节点流和包装(处理)流最简单的一个方式:处理流的构造方法中需要另一个流作为参数,而节点流构造方法则是具体的物理节点,如上FileInputStream构造法中需要一个文件路径或者File对象,而PrintStream构造方法中则需要一个流对象
3)包装流使用了装饰器模式(什么是装饰器模式),包装流对节点流进行了一系列功能的强化包装,让包装后的流拥有了更多的操作手段或更高的操作效率,而隐藏节点流底层的复杂性。
1.7 低级流和高级流
低级流和高级流对应的概念即对应上面的节点流和包装(处理)流概念
1.8 普通流和缓冲流
普通流和缓冲流主要是针对读写性能上提出的相对概念。普通流与缓冲流的区别在于一个一个数据的流动还是一堆一堆数据的流动。
1.9 bio,nio,aio
bio:b有两说,一为base,jdk中最早抽象出的io体系;一为block,jdk 1.0 中的io体系是阻塞的。所以两说皆有道理,一般我们认为b取block之意
nio:n也有两说,一为new,针对base而言;一为non-block,针对block而言。
aio:a为asynchronous,异步的,异步io,aio还有个名字叫:nio2
发展历程:bio(jdk1.0) -> nio(jdk1.4) -> aio(jdk1.7)
2.BIO类体系
2.1 体系图
3.BIO体系中的类该怎么使用?
要回答这个问题,我们需要将这个问题进行拆分为下面四个问题
3.1 什么时候该用输入流,什么时候用输出流?
从流中读取信息使用输入流(xxxInputStream/xxxReader),写入信息使用输出流(xxxOutputStream/xxxWriter)
3.2 什么时候该用字节流,什么时候用字符流?
处理纯文本数据时使用字符流(xxxReader/xxxWriter),处理非纯文本时使用字节流(xxxStream)。最后其实不管什么类型文件都可以用字节流处理,包括纯文本,但会增加一些额外的工作量。所以还是按原则选择最合适的流来处理
3.3 什么时候该用节点流,什么时候用包装(处理)流?
不管你用什么包装(处理)流,都需要先使用节点流获取对应节点的数据流,然后根据具体需求来选择相应的包装(处理)流来对节点流进行包装修饰,从而获取相应的功能
3.4 什么时候该用普通流,什么时候用缓冲流?
一般如果对数据流不做加工处理,而是单纯的读写,如数据转移(拷贝,上传,下载),则需要使用缓冲流来提高性能,当然你也可以自己使用buff数组来提高读写效率。
3.5 使用小结
1)判断操作的数据类型
纯文本数据:读用Reader系,写用Writer系
非纯文本数据:读用InputStream系,写用OutputStream系
如果纯文本数据只是简单的复制,下载,上传,不对数据内容本身做处理,那么使用Stream系
2)判断操作的物理节点
内存:ByteArrayXXX
硬盘:FileXXX
网络:http中的request和response均可获取流对象,tcp中socket对象可获取流对象
键盘(输入设备):System.in
显示器(输出设备):System.out
3)搞清读写顺序,一般是先获取输入流,从输入流中读取数据,然后再写到输出流中。
4)是否需增加特殊功能,如需要用缓冲提高读写效率则使用BufferedXXX,如果需要获取文本行号,则使用LineNumberXXX,如果需要转换流则使用InputStreamReader和OutputStreamWriter,如果需要写入和读取对象则使用ObjectOutputStream和ObjectInputStream
4.使用IO流一些注意点
4.1 关于流的read、write方法的使用
下面列出read和write的方法原型(这里以字节流为例,字符流道理相同):
1 | // stream的read |
如果不考虑性能会有如下两种形式来读写:读入一个字节,写入一个字节;使用缓冲区读入一堆字节,然后将缓冲区数据进行写入输出流。如下:
1 | //读一个字节,写一个字节 |
上面第一种方式读一个字节写一个字节没什么说的;第二个方式则需要注意,调用write(buff,0,b)而不是write(buff,0,buff.length),为什么写入[0,b]而不是[0,buff.length]区间的数据呢?因为假如只剩余3个字节没有读取,而缓冲数组定义的大小是8个字节,那么使用[0,buff.length]则会造成多写入5个字节的脏数据
4.2 关于流读写性能问题
流的读写是比较耗时的操作,因此为了提高性能,便有缓冲的这个概念(什么是缓冲?假如你是个搬砖工,你工头让你把1000块砖从A点运到B点,你可以一次拿一块砖从A点运到B点放下砖,这样你要来回跑1000次,大多数的时间开销在路上了;你还可以使用一辆小车,在A点装满一车的砖,然后运到B点放下砖,如果一车最多可以装500块,那么你来回两次便可以把这些砖运完。这里的小车便是那个缓冲),在java bio中使用缓冲一般有两种方式。一种是自己申明一个缓冲数组,利用这个数组来提高读写效率;另一种方式是使用jdk提供的处理流BufferedXXX类。下面我们分别演示不使用缓冲读写,使用自定义的缓冲读写,使用BufferedXXX缓冲读写一个文件。
4.21 无缓冲读写文件
1 | /** |
4.22 自定义数组做缓冲读写文件
1 | /** |
4.23 使用BufferedXXX类使用默认大小缓冲来读写文件
1 | /** |
4.24 使用BufferedXXX类自定义大小缓冲来读写文件
1 | /** |
从上面可以看出来,如果不使用缓冲(4.21)拷贝一个八百多k的文件,竟然要5秒钟,这个速度让人捉急啊,所以我们拷贝文件时应该使用缓冲技术。由于这是常用功能,便提供了BuffereXXX类简化这个工作,上面我们使用BufferedOutputStream(4.23),即使调用write(int b)方法没有显示使用缓冲数组为什么性能也大大得到提升?来看看BufferedOutputStream中的部分源码:
1 | public class BufferedOutputStream extends FilterOutputStream { |
方法测试:
1 | public static void main(String[] args) { |
从上面BufferedOutputStream中可以看出,构造实例的时候便创造了一个大小为8192字节的缓冲数组,当我们调用write(int b)方法时,并没有在调用完后直接写入,而是将每一个传入的int值放入到了buf中,只有count >= buf.length时,才调用flushBuffer将缓冲区的数据写入。
4.3 如何判断文件是否读写完毕?
我们一般在处理文件,一般是一边从输出流中读数据,然后将读出的部分进行处理,最后将处理好的数据写入到输出流中。那么要将一个文件完整的处理完,我们必须知道什么时候已经读到文件的末尾了。
一般来说可以根据read方法返回的值,如果返回了-1表示没有可读取的字节了。
另一种是使用available()方法查看还有多少可供读取的,当输入流每读一个字节,available()返回的值便减小1,这种模式很像游标的模式,但要注意的是available的适用场景是非阻塞读取,如本地文件读取,如果是网络io使用该方法,可能你拿到的值就不对了。
总的来说一般输入流提供的读取方法是可以获得文件是否结束的标志,比如流默认的read方法,根据返回值是否非负,比如PrintReader和BufferedReader的readLine()方法,根据返回数据是否非空。
4.4 关于flush()的问题
为什么缓冲输出流写数据结束需要调用flush方法?我们以BufferedOutputStream的write(int b)方法源码为例,源码如下:
1 | public synchronized void write(int b) throws IOException { |
可以得知,BufferedOutputStream在write时候,只有count >= buf.length,即缓冲区数据填满的时候才会自动调用flushBuffer()将缓冲区数据进行写入,也就是说如果缓冲区数据未满则将不会写入,这时我们需人为的调用flush()方法将未满的缓冲区数据进行写入,如下例,可以看出未flush和flush后的区别。
1 | public static void copyByBufferedStream(File src,File dest,int size) { |
从上面测试可以知道,如果未使用flush,带来的后果可能会造成部分数据丢失,为什么说是可能?因为如果文件大小刚好是缓冲区的整倍数,即最后一次写入的数据刚好填满缓冲区,write方法也会自动flushBuffer。另一种原因是,调用close方法后会自动将缓冲区的数据flush,我们看看close方法源码,由于BufferedOutputStream类中并没有重写close方法,因此我们去看看直接父类FilterOutputStream的close,源码如下:
1 | public void close() throws IOException { |
从上可以看出,在调用close后,其实内部调用了flush(),因此我们在调用close后,数据也能保证数据完整写入,我们验证下我们刚才的分析:
1 | public static void copyByBufferedStream(File src,File dest,int size) { |
这样的结果确实符合我们对源码的分析
4.5 关于网络流中使用available()方法的问题
当你在网络io中,比如你用socket编程时获取到的流进行读写时,会发现使用available方法有问题,原因是网络io的特点是:
1.非实时性。你调用available()方法判断剩余流的大小时,远端数据可能还未发送,或者要发送的数据处于队列中,因此通过available()拿到的可用长度可能是0
2.非连续性。由于网络数据传输中,一般会分段多次发送,available仅仅能返回本次的可用长度。
鉴于以上两个特点,使用available判断网络io还有多少数据可读是不合适的,因此解决该问题一般采用自定义协议,比如文件大小,文件名等信息放入流的头几个字节中,接收方根据收到的头信息来解析出对法传送的文件大小,根据大小来判断还剩多少字节需要读取,是否读取完毕。
4.6 关于关闭流的问题
1)为什么需要手动关闭?
参见https://blog.csdn.net/u012250875/article/details/78365112
2) 关闭流的正确写法
先来两个不规范的,上代码:
1 | /** |
案例一写法不规范的原因是fis和fos对象执行一些方法时是可能发生异常的,一旦出现异常,虽然由于进行了try catch,但是执行流程会直接进入到catch中,会跳过流的关闭操作。
案例二写法不规范的原因是,虽然讲close操作放入了finally中,但是一旦fos.close执行出现异常,则fis无法正常关闭,修改方法是在finally块中的同时对每个close都单独try catch。
因此写一个关闭流的工具方法,写法如下:
/**
* 关闭给定的io流
*/
public static void close(Closeable...closes){
for (Closeable closeable : closes) {
try {
if(closeable!=null){
closeable.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 调用close()方法
*/
public static void main(String[] args) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream(new File("E:\\iotest\\1.bmp"));
fos = new FileOutputStream(new File("E:\\iotest\\1_copy.bmp"));
//其他代码
//......
} catch (IOException e) {
e.printStackTrace();
} finally{
close(fos,fis);
}
}
5.java io流到底能干些什么?
java io主要做两类事情:
1)数据传输,实际例子如文件上传,下载,文件本地拷贝等
2)数据处理,如文本内容加密,图片处理,文件压缩,音视频处理等
6.实践一下
下面就来个简单的实践
6.1 文件拷贝
本文中4.2中的几个方法均可进行文件拷贝,拷贝一般都会使用缓冲来提高性能
6.2 文本加密
因为我们通过java io流处理可以获得文本的原始数据,我们在数据上进行加工就可以加密文本,通过相反的方式就可以解密文本,实际生产中当然不会像下面这么简单的加密,下面我们只是做个示意
public static void encrypt(File src, File dest) {
FileReader fr = null;
FileWriter fw = null;
try {
fr = new FileReader(src);
fw = new FileWriter(dest);
int tmp = 0;
while ((tmp = fr.read()) != -1) {
fw.write(tmp+1);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
close(fw, fr);
}
}
public static void decrypt(File src, File dest) {
FileReader fr = null;
FileWriter fw = null;
try {
fr = new FileReader(src);
fw = new FileWriter(dest);
int tmp = 0;
while ((tmp = fr.read()) != -1) {
fw.write(tmp-1);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
close(fw, fr);
}
}
public static void main(String[] args) {
File src = new File("E:\\iotest\\1.txt");
File dest = new File("E:\\iotest\\1_jiami.txt");
File dest2 = new File("E:\\iotest\\1_jiemi.txt");
encrypt(src, dest);
decrypt(dest, dest2);
}
上面代码中处理方式很简单,仅仅做了一个字符加1处理,然后解密在字符上进行减1处理,便能达到一个加密解密的效果,效果如下
原始密文:
1 |
|
解密后的文本:
1 |
|
上面代码中写数据时,如果字节长度小于headSize则原样输出(headSize我这里取的是12个字节,bmp头信息具体多大我并没有去查询。如果取太小头信息被破坏,bmp文件将不是bmp文件。因此取几十个字节只要保证头信息完整即可。),大于headSize的部分进行一个处理,我们对颜色值取反。效果如下:
原图:
通过上面三个的示例可以大致了解java io能做些什么。当然上面只是一个简单的处理,并不能用于实际生产,这里只是说明持有二进制数据,你将可以对文件为所欲为。
————————————————
版权声明:本文为CSDN博主「忧落」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u012250875/article/details/78341874