目录
一、Java-IO流的概述
Java输入/输出(Input/Output)系统简称IO,又称为输入/输出流,它是程序设计语言中最基础的部分。输入/输出是指应用程序与外部设备及其他计算机进行数据交流的操作,可以在程序设计中根据需要实现不同类型的输入/输出功能。
Java中IO是通过“流”的形式进行数据的输入和输出,所有数据被串行化写入输出流或从输入流读入。Java的核心库java.io提供了全面的IO接口,包括文件的读写和标准设备输出等。
二、流的概念和作用
Java中将输入/输出抽象称为流,可以理解为输入/输入的途径。就好像水管,将两个容器连接起来。当程序需要读取或者写入数据时,就会开启一个通向数据源或者目的地的通道,而流是数据在数据源和目的地之间有序运动着的数据序列,该数据序列是有顺序、有起点和终点的,整个过程就好像数据在其中“流”动一样。
个人观点:Java输入/输出流就是在数据源和目的地建立传输通道(作用),根据数据的类型有序的运输到目的地中(本质)。
三、流的分类
3.1 输入流和输出流
流可以分为两类:输入流和输出流。输入流不关心数据源自何种设备,输出流也不关心数据的目的是何种设备。
输入流:只能从中读取数据,而不能向其写入数据。(只能读)
输出流:只能向其写入数据,而不能从中读取数据。(只能写)
相对于程序来说,输出流是往存储介质或数据通道写入数据,而输入流是从存储介质或数据通道中读取数据,一般来说关于流的特性有下面几点:
a. 先进先出。最先写入输出流的数据最先被输入流读取到。
b. 顺序存取。一个接一个地往流中写入一串字节,读出时也将按写入顺序读取一串字节,不能随机访问中间的数据(RandomAccessFile可以从文件的任意位置进行存取操作)。
c. 只读或只写。每个流只能是输入流或输出流的一种,不能同时具备两个功能,输入流只能进行读操作,对输出流只能进行写操作。在一个数据传输通道中,如果既要写入数据,又要读取数据,则要分别提供两个流。
注意的是,Java的输入流主要由InputStream和Reader作为基类,而输出流则主要由OutputStream和Writer作为基类。他们都是一些抽象基类,无法直接创建实例,但是可以通过他们的子类来创建流。例如针对存储在磁盘、光盘或其他存储设备上的文件的字节流,可以通过FileInputStream和FileOutputStream的子类来创建。
3.2 字节流和字符流
字节流和字符流的用法几乎完全一样,区别在于两者所操作的数据单元不同。流序列中的数据可以是没有进行加工的原始数据(二进制字节数据),也可以是经过编码的符合某种编码格式的数据。其中,字节流以字节为基本单位来处理数据的输入/输出,一般用于对二进制数据的读写,例如声音、图像等数据;字符流以字符为基本单位来处理数据的输入和输出,一般用于文本类型数据的读写,例如文本文件、网络中发送的文本信息等。
字节流和字符流主要由4个抽象类来表示:InputStream、OutputStream、Reader、Writer,输入和输出各两种。其中InputStream和OutputStream表示字节流,Reader和Writer表示字符流,其他各种各样的流均是继承这4个抽象类而来的。
个人观点:字节流以字节为单位,通过InputStream和OutputStream抽象类进行读写,而字符流是以字符为单位,通过Reader和Writer抽象类进行读写。
3.3 节点流和处理流
按照流的角色来分,可以分为节点流和处理流。
可以从/向一个特定的IO设备(如磁盘、网络)读/写数据的流,称为节点流。节点流也被称为低级流。
处理流则用于对一个已存在的流进行连接或封装,通过封装后的流来实现数据读/写功能。处理流也被称为高级流。
从图15.4中可以看出,当使用处理流进行输入/输出时,程序并不会直接连接到实际的数据源,没有和实际的输入/输出节点连接。使用处理流的一个明显好处时,只要使用相同的处理流,程序就可以采用相同的输入/输出代码来访问不同的数据源,随着处理流所包装节点流的变化,程序实际所访问的数据源也相应地发生变化。
四、字节流和字符流
InputStream和Reader是所有输入流的抽象基类,本身并不能创建实例来执行输入,但它们将称为所有输入流的模板,所以他们的方法是所有输入流都可以使用的方法。
4.1 字节输入流InputStream
1.InputStream主要包含如下四个方法
* int read() 从输入流读取一个字节,并转换为0~255的整数,最后返回这个整数。为了提高I/O操作的效率,建议使用下面两种形式。
* int read(byte[] b) 从输入流中最多读取b.length个字节的数据,并将其存储在数组b中,返回实际读取的字节数。
* int read(byte[] b,int int off,int length) 从输入流中的off位置开始,最多读取length个字节的数据,并将其存储在数组b中,该方法返回实际读取的字节数。
* void close() 关闭输入流。在读操作完成后,应该关闭输入流,系统将会释放与这个输入流相关的资源。注意,InputStream类本身的close()方法不执行任何操作,但是它的子类重写了close()方法。
InputStream类的子类
* ByteArrayInputStream类将字节数组转换为字节输入流,从中读取字节。
* FileInputStream类从文件中读取数据。
* PipedInputStream类连接到一个PipedOutputStream(管道输出流)。
* SequenceInputStream类将多个字节输入流串联成一个字节输入流。
* ObjectInputStream类将对象反序列化
2. 在Reader里包含如下三个方法
* int read() 从输入流读取单个字符,返回所读取的字符数据。
* int read(char[] cbuf) 从输入流中最多读取cbuf.length个字符的数据,并将其存储在字符数组cbuf中,返回实际读取的字符数。
* int read(char[] cbuf,int int off,int length) 从输入流中的off位置开始,最多读取length个字符的数据,并将其存储在字符数组cbuf中,该方法返回实际读取的字符数。
Reader类的子类
* CharArrayReader类将字符数组转换为字符输入流,从中读取字符。
* StringReader类字符串转换为字符输入流,从中读取字符。
* BufferedReader类为其他字符输入流提供读缓冲区。
* PipedReader类连接到一个PipedWriter。
* InputStreamReader类将字节输入流转换为字符输入流,可指定字符编码。
InputStream()的基本方法时没有参数的read()方法。这个方法从输入流的源中读取1字节数据,作为一个0~255的int返回,流的结束通过返回-1来表示。read()方法会等待并阻塞其后任何代码的执行,直到有1字节的数据可供读取。输入和输出很慢,所以如果程序在做其他重要的工作,要尽量将I/O放在单独的线程中,下面是一个简单的例子。
//创建文本字节输入流
FileInputStream fis = new FileInputStream("文件路径");
//创建一个长度为1024的“竹筒”
byte[] bbuf = new byte[1024];
//用于保存实际读取字节数
for(int i=0;i<bbuf.length;i++){
int b = fis.read();
if(b == -1){
System.out.println("关闭");
break;
}else{
bbuf[i] = (byte)b;
System.out.println(b);
}
}
//关闭文件输入流,放在finally块里更安全
fis.close();
read()方法都用返回-1来表示流的结束。如果流已经结束,而又没有读取的数据,多字节read()方法会返回这些数据,直到缓冲区清空。-1永远不会放进数组中,因为数组只包含实际的数据,所以如果获取数据的字节数小于数据的存储长度,一定要判断流是否读取完毕,如果读取完毕则结束读取并关闭输入流。例如上面的例子那样,存储数组的长度大于实际的数据长度,下面我们看看存储长度小于实数据长度的情况:
从上图可以看到,循环中没有输出关闭的字符串。和输出流一样,当结束对输入流的操作时,通过调用close()方法将其关闭,释放与这个流关联的所有数据。一旦输入流已经关闭,进一步读取这个流会抛出IOException异常。
4.2 输出流OutputStream和Writer
1.OutputStream主要包含如下五个方法
* void write(int b) 向输出流写入一个字节,其中c既可以代表字节,也可以代表字符。这里的参数是int类型,但是它允许使用表达式,而不用强制转换成byte类型。
* void write(byte[] b) 将b字节数组/字符数组的所有数据写入到输出流中。
* void write(byte[] b,int off,int length) 从b字节数组中的off位置开始,长度为length的子字节写入输出流中。
* void flush() 将数据缓冲区中全部数据写入到输出流,并清空缓冲区。(为提高效率,在向输出流中写入数据时,数据一般会保存到内存缓冲区,只有当缓冲区中的数据达到一定程度时,缓冲区的数据才会被写入到输出流中。而flush方法则可以强制将缓冲区的数据输入到输出流中)
* void close() 关闭输出流并释放与流相关的系统资源。
OutputStream类的子类
* ByteArrayOutputStream类向内存缓冲区中的字节数组中写入数据
* FileOutputStream类向文件中写入数据
* PipedOutputStream类连接到一个PipedIntputStream
* ObjectOutputStream类将对象序列化
2. Writer主要包含如下几个方法
因为字符流可以直接以字符作为操作单位,所以Writer可以用字符串来代替字符数组,即以String对象作为参数
* void write(int b) 向输出流中写入一个字符
* void write(char[] buf) 将buf字符数组中的所有字符写入到输出流中
* void write(char[] buf,int off,int length) 从buf字符数组中的off位置开始,长度为length的子字符写入输出流。
* void write(String Str) 向输出流中写入一个字符串
* void write(String str,int off,int length) 从str字符串中的off起始偏移量开始,长度为length的子字符写入输出流。
Writer类的子类
* CharArrayWriter类向内存缓冲区中的字符数组写数据
* StringWriter类向内存缓冲区中的字符串写数据
* BufferedWriter类为其他字符输出流提供写缓冲区
* PipedWriter类连接到一个PipedReader
* OutputStreamWriter类将字节输出流转换为字符输出流,可指定字符编码
下面程序使用FileInputStream来执行输出,并使用FileOutputSteam来执行输出,实现复制指定文件的功能。
try{
//创建字节输入流
FileInputStream fis = new FileInputStream("文件路径");
//创建字节输出流
FileOutputStream fos = new FileOutputStream("newFile.java");
//创建字节存储数据,并定义长度
byte[] bbuf = new byte[32];
//创建读取结果的存储变量
int hasRead = 0;
while((hasRead = fis.read(bbuf)) > 0){
//如果读取返回的值不为-1,则将每次读取到的32个字节数据写入到输出流
fos.write(bbuf,0,hasRead);
}
}catch(IOException ex){
System.err.println(ex.getMessage());
}
注意:每执行一次fis.read(bbuf)函数,流中的数据都会往前移动32位字节。
和网络硬件中缓存一样,流还可以在软件中得到缓冲。一般说来,可以通过把BufferedOutputStream或BufferedWriter串联到底层流上来实现。因此,在写入数据之后,刷新(flush)输出流非常重要。例如,如果输出流有一个1024字节的缓冲区,那么这个流在发送缓冲区中的数据之前会等待更多的数据到达。在服务器响应到达之前不会向流写入更多的数据,但是响应也不会到来,因为请求还没有发送,而flush()方法可以强迫缓冲的流发送数据,即使缓冲区还没有满,以此来打破这种死锁状态。
个人观点:在缓冲区的数据没有充满之前,请求和数据是不会发送到服务器中,通过flush()方法可以释放缓冲区的数据。
最后,当结束一个流的操作时,通过调用close()方法将其关闭,释放与这个流关联的所有数据。如果流来自一个网络,那么关闭这个流也会终止这个连接。一旦输出流关闭,继续写入时就会抛出IOException异常。不过,有些流仍允许对这个对象做一些处理,例如:ByteArrayOutputStream仍转换为实际的字节数组,关闭的DigestOutputStream仍然可以返回其摘要。
为了得到正确的变量作用域,必须在try块之外声明流变量,但是必须在try块内完成初始化。另外,为了避免NullPointerExcepiton异常,在关闭流之前需要检查流变量是否为null。最后,通常都希望希望忽略关闭流时出现的异常,或者最多只是将这些异常写入日志。
OutputStream Out = null;
try{
Out = new FileOutputStream(“/test/data.txt”);
//处理输出流...
}catch(IOException ex){
System.err.println(ex.getMessage());
}finally{
if(out != null){
try{
Out.close();
}catch(IOException ex){
//忽略
}
}
}
这个技术有时称为释放模式(dispose pattern),这对于需要在垃圾回收前先进行清理的对象很常见,这个技术不仅用于流,还可以用于Socket、通道、JDBC连接等。
Java 7引入了“带资源的try”构造,不需要在try块之外声明流变量,完全可以在try块的参数表中声明,所以也就不需要在Finally字句。例如:
try(OutputStream Out = new FileOutputStream(“/test/data.txt”)){
//处理输出流...
}catch(IOException ex){
System.err.println(ex.getMessage());
}
Java会对try块参数表中声明的所有AutoCloseable对象自动调用close();