IO
大多数应用程序都要实现与设备之间的数据传输,例如键盘可以输入数据,显示器可以显示程序的运行结果等。在Java中,将这种通过不同输入输出设备之间的数据传输抽象表述为“流”,程序允许通过流的方式与输入输出设备进行数据传输。Java中的“流”都位于java.io包中,称为IO(输入输出)流。
IO流有很多中,按照操作数据的不同,可以分为字节流和字符流,按照数据传输方向的不同又可以分为输入流和输出流,程序从输入流中读取数据,向输出流中写入数据。在IO包中,字节流的输入输出流分别用java.io.InputStream和java.io.OutputStream表示,字符流的输入输出流分别用java.io.Reader和java.io.Writer表示。
1、字节流
1.1、字节流的概念
在计算机中,无论是文本、图片、音频还是视频,所有的文件都是以二进制(字节)形式存在,IO流中针对字节的输入输出提供了一系列的流,统称为字节流。字节流是程序中最常用的流,根据数据的传输方向可将其分为字节输入流和字节输出流。在JDK中,提供了两个抽象类IuputStream和OutputStream,它们是字节流的顶级父类。
在JDK中,InputStream和OutputStream提供了一系列与读写相关的方法,如表1-1所示。
表1-1 InputStream的常用方法
方法声明 | 功能描述 |
---|---|
int read() | 从输入流中读取一个8位的字节,把它转化位0~255之间的整数,并返回这一整数 |
int read(byte[ ] b) | 从输入流中读取若干字节,把它保存到参数b指定的字节数组中,返回的整数表示读取字节书 |
int read(byte[ ] b,int off,int len) | 从输入流中读取若干字节,把它保存到参数b指定的字节数组中,off表示指定字节数组开始保存数据的起始下标,len表示读取字节的数目 |
void close() | 关闭此输入流并释放与该流关联的所有系统资源 |
表1-2 OutputStream的常用方法
方法名称 | 方法描述 |
---|---|
void write(int b) | 向输出流写入一个字节 |
void write(byte[ ] b) | 把参数指定的字节数组的所有字节写道输出流 |
void write(byte[ ] b,int off,int len) | 将指定byte数组中从偏移量off开始的len个字节写入输入流 |
void flush() | 刷新此输出流并强制写出所有缓冲的输出字节 |
void close() | 关闭此输出流并释放与此流相关的所有系统资源 |
注意:InputStream类和OutputStream类虽然提供了一系列与读写相关的方法,但是这两个类是抽象类,不能被实例化。
1.2、字节流读写文件
由于计算机中的数据基本上都保存在硬盘的文件中,因此操作文件中的数据是一种很常见的操作。在操作文件上,最常见的就是从文件中读取数据并将数据写入文件,即文件的读写。针对文件的读写,JDK专门提供了两个类,FileInputStream和FileOutputStream。
FileInputStream是InputStream类的子类,它是操作文件的字节输入流,专门用于读取文件中的数据。由于读取数据是重复的操作,因此需要通过循环语句来实现数据的持续读取,接下来通过一个案例来实现字节流对文件数据的读取。
首先在目标目录下创建一个文本文件test.txt,在文件中输入内容“test”,具体代码如例一。
例一
package io.example1;
import java.io.File;
import java.io.FileInputStream;
public class Example1 {
public static void main(String[] args) throws Exception{
String logsFile = "E:\\read.txt";
//实例化File对象,读取e盘read.txt文件
File file = new File(logsFile);
//实例化FileInputStream对象,将file作为参数传入
FileInputStream in = new FileInputStream(file);
int b = 0;
while(true){
//变量b记住读取的一个字节
b = in.read();
//如果读取的字节为-1,跳出while循环
if(b == -1){
break;
}
System.out.println(b);
}
in.close();
}
}
例一运行结如下
例一中,创建的字节流FileInputStream 通过read()方法将当前目录文件“test.txt”中的数据读取并打印。通常情况下,读取文件应该输出字符,而运行结果输出数字是因为硬盘上的文件是以字节的形式存在的,在test.txt中,t、e、s、t四个字符各占一个字节,因此,最终结果显示的就是该文件中四个字节所对应的十进制数。
注意:在读取文件数据时,必须保证文件是存在并且可读的,否则会抛出异常FileNotFoundException。
与FileInputStream对应的是FileOutputStream,它是OutputStream的子类,是操作文件的字节输出流,专门用于将数据写入文件,接下来通过例二来演示如何将数据写入文件。
例二
package io.example2;
import java.io.FileOutputStream;
public class Example2 {
public static void main(String[] args) throws Exception{
//创建一个文件字节输出流
FileOutputStream out = new FileOutputStream("E:\\read.txt");
String str = "测试案例";
byte [] b = str.getBytes();
for(int i = 0; i < b.length; i++){
out.write(b[i]);
}
out.close();
}
}
例二运行结果
通过FileOutputStream写入数据时,如果没有read.txt文件,会自动在指定目录创建该文件,并将数据写入。需要注意的是:如果向一个已经存在的文件中写入数据,那么该文件的的数据首先会被清空,再写入新的数据,若希望在已经存在的文件中追加新内容,可以使用FileOutputStream的构造函数FileOutputStream(String fileName,boolean append)来创建文件的输出流对象,并把append的参数值设置为true,接下来我们通过例三来演示该效果。
例三
package io.example3;
import java.io.FileOutputStream;
public class Example3 {
public static void main(String[] args) throws Exception{
//创建一个文件字节输出流
FileOutputStream out = new FileOutputStream("E:\\read.txt",true);
String str = ":测试成功";
byte [] b = str.getBytes();
for(int i = 0; i < b.length; i++){
out.write(b[i]);
}
out.close();
}
}
例三运行结果
从例三的运行结果来看,程序把新写入的数据追加到文件的末尾,达到的预期的效果。
从上面的案例中可以看出,IO流在进行数据读写的操作时会出现异常,为了代码的简洁,在程序中使用throws关键字抛出异常。然而一旦遇到IO异常,IO流的close()方法无法得到执行,流所占用的资源将得不到释放,因此为了保证IO流的close()方法必须执行,通常将关闭流的操作写在finally代码块中,具体代码如下所示。
finally{
try {
if(in != null){ //如果in不为空,关闭输入流
in.close();
}
} catch (Exception e) {
e.printStackTrace();
}
try {
if(out != null){ //如果out不为空,关闭输出流
out.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
1.3、文件的拷贝
在应用程序中,IO流通常都是成对出现的,例如文件的拷贝就需要通过输入流读取文件中的数据,通过输出流将数据写入文件。接下来通过案例四来演示文件的拷贝,使用E盘下的test.txt文件为拷贝模板。
例四
package io.example4;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class Example4 {
public static void main(String[] args) throws Exception{
//创建一个字节输入流,读取木标文件夹中的文件
FileInputStream in = new FileInputStream("E:\\read.txt");
//创建一个字节输出流,用于将读取的数据写入目标文件夹下的文件
FileOutputStream out = new FileOutputStream("E:\\read1.txt");
//定义变量len,记住每次读取的一个字节
int len;
//获取拷贝前的系统时间
long begintime = System.currentTimeMillis();
while((len = in.read()) != -1){
out.write(len);
}
long endtime = System.currentTimeMillis();
System.out.println("拷贝文件所耗的时间是:"+(endtime-begintime)+"毫秒");
in.close();
out.close();
}
}
例四运行结果
例四实现了read.txt文件的拷贝,在拷贝过程中,通过while循环将字节进行拷贝,每循环一次,就通过FileInputStream的read()方法读取一个字节,并通过FileOutputStream的write()方法将该字节写入指定文件,循环往复,直到len的值为-1,表示读取到文件的末尾,结束循环,完成文件的拷贝。程序结束后,会打印出拷贝文件消耗的时间。
1.4、字节流的缓冲区
例四虽然实现了文件的拷贝,但是一个字节一个字节的读写,需要频繁的操作文件,效率非常低。为了提高效率,可以定义一个字节数组作为缓冲区,这样在拷贝文件时,可以一次性地读取多个字节地数据,并保存在字节数组中,然后将字节数组中的数据一次性地写入文件,接下来通过例五来演示如何通过自己缓冲区拷贝文件。
例五
package io.example5;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class Example5 {
public static void main(String[] args) throws Exception{
//创建一个字节输入流,读取木标文件夹中的文件
FileInputStream in = new FileInputStream("E:\\read.txt");
//创建一个字节输出流,用于将读取的数据写入目标文件夹下的文件
FileOutputStream out = new FileOutputStream("E:\\read3.txt");
//以下时用字节缓冲区读写文件
byte [] buff = new byte [1024] ;
//定义变量len,记住每次读取的一个字节
int len;
//获取拷贝前的系统时间
long begintime = System.currentTimeMillis();
//从第一个字节开始,向文件中写入len个字节
while((len = in.read(buff)) != -1){
out.write(buff,0,len);
}
long endtime = System.currentTimeMillis();
System.out.println("拷贝文件所耗的时间是:"+(endtime-begintime)+"毫秒");
in.close();
out.close();
}
}
例五运行结果
例五实现了文件的拷贝,在拷贝过程中,使用while循环语句实现文件的拷贝,每循环一次,就从文件中读取若干字节填充字节数组,并通过变量len记住读入数组的字节数,然后从数组的第一个字节开始,将len个字节依次写入文件,循环往复,当len值为-1时,说明已经读到了文件的末尾,循环会结束,整个拷贝过程也就结束了。通过例四与例五的对比,可以看出例五拷贝文件消耗的时间明显减少了,从而说明缓冲区读写文件有效地提高了效率。这是因为程序中地缓冲区就是一块内存,使用缓冲区减少了对文件的操作次数,所以提高了读写数据的效率。
1.5、装饰设计模式
在程序设计中,可以通过装饰一个类,来增强它的功能。装饰设计模式就是通过包装一个类,动态地为它增加功能地一种设计模式。接下来通过例六来演示这种模式。
例六
package io.example6;
public class Car {
private String carName;
public Car(String carName){
this.carName = carName;
}
public void show(){
System.out.println("我是"+carName+"。具有基本功能");
}
}
public class RedCar {
public Car myCar;
public RedCar(Car myCar){
this.myCar = myCar;
}
public void show(){
myCar.show();
System.out.println("具有倒车雷达功能");
}
}
public class Example6 {
public static void main(String[] args) {
Car benz = new Car("Benz");
System.out.println("----包装前-----");
benz.show();
RedCar dbenz = new RedCar(benz);
System.out.println("---包装后----");
dbenz.show();
}
}
例六运行结果
例六实现了RedCar类对Car类的包装,从运行结果看,被RedCar包装后的对象benz不仅具有车的基本功能,还具备了倒车雷达的功能。
1.6、字节缓冲流
在IO包中,提供了两个带缓冲的字节流,分别是BufferedInputStream和BufferedOutputStream,这两个流都使用了装饰设计模式。它们的构造方法中分别接受InputStream和OutputStream类型的参数作为被包装对象,在读写数据时提供缓冲功能。接下来通过例七来学习BufferedInputStream和BufferedOutputStream这两个流的用法。
例七
package io.example7;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class Example7 {
public static void main(String[] args) throws Exception{
//创建一个带缓冲区的输入流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("E:\\read.txt"));
//创建一个带缓冲区的输出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("E:\\read4.txt"));
int len;
while((len = bis.read()) != -1){
bos.write(len);
}
bis.close();
bos.close();
}
}
例七中,创建了BufferedInputStream和BufferedOutputStream两个缓冲流对象,这两个流内部定义了有一个大小为8192的字节数组,当调用read()或者write()方法读写数据时,首先想到将读写的数据存入定义好的字节数组,然后将字节数组一次性读写到文件中,这种方式与例五将的字节流的缓冲区类似,都对数据进行了缓冲,从而有效地提高了读写效率。
2、字符流
2.1、字符流地定义及基本用法
上面讲的InputStream和OutputStream类在读写文件时操作的都是字节,如果希望在程序中操作字符,使用这两个类就不太方便,为此JDK提供了字符流。同字节流一样,字符流也有两个抽象的顶级父类,分别时Reader和Writer。其中Reader是字符输入流,用于从某个源设备读取字符,Writer是字符输出流,用于向某个目标设备写入字符。Reader和Writer作为字符流的顶级父类,也有许多子类。其中FileReader和FileWriter用于读写文件,BufferedReader和BufferedWriter是具有缓冲功能的流,它们可以提高读写效率。
2.2、字符流操作文件
在程序开发中,如果想从文件中直接读取字符便可以使用字符输入流FileReader,通过此流可以从关联的文件中读取一个或者一组字符。接下来通过例八来学习使用FileReader读取文件中的字符。
例八
package io.example8;
import java.io.FileReader;
public class Example8 {
public static void main(String[] args) throws Exception{
//创建一个FileReader对象用来读取文件中的字符
FileReader reader = new FileReader("E:\\read.txt");
int ch;
//循环判断是否读到文件末尾
while((ch = reader.read()) != -1){
System.out.println((char) ch);
}
//关闭文件读取流,释放资源
reader.close();
}
}
例八运行结果
例八实现了读取文件字符的功能,首先创建一个FileReader对象与文件关联,然后通过while循环每次从文件中读取一个字符并打印,这样就实现了FileReader读文件字符的操作。需要注意的是:字符输入流的read()方法返回的是int类型的值,如果想获取到字符就必须进行强制类型转换,(char)ch就是将变量ch转为char类型再打印。
例八讲解了使用FileReader读取文件中的字符,如果要向文件中写入字符就需要使用FileWriter类。该类是Writer的一个子类,接下来通过例九来学习使用FileWriter将字符写入文件。
例九
package io.example9;
import java.io.FileWriter;
public class Example9 {
public static void main(String[] args) throws Exception{
//创建一个FileWriter对象用于向文件中写入数据
FileWriter fw = new FileWriter("E:\\read001.txt");
String str = "向文件中写入测试数据";
//将字符数据写入到文件中
fw.write(str);
fw.close();
}
}
例九运行结果
程序运行结束后,会在目标文件夹下生成一个“read001.txt”文件,内容如上图所示。FileWtiter和FileOutputStream一样,如果指定的文件不存在,就会先创建文件,再写入数据;如果文件存在,就会先清空文件里的内容,再进行写入,如果想在文件末尾追加数据,同样需要调用重载的构造方法FileWtiter(String fileName,boolean append),并将append的参数值设为true。
字符流和字节流一样提供了带缓冲区的包装流,分别是BufferReader和BufferWriter,其中BufferReader用于对字符输入流进行包装,BufferWriter用于对字符输出流进行包装,需要注意的是,在BufferReader中有一个重要的方法readLine(),该方法用于一次读取一行文本,接下来通过例十来演示使用这两个包装流实现文件的拷贝。
例十
package io.example10;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
public class Example10 {
public static void main(String[] args) throws Exception{
FileReader fd = new FileReader("E:\\read001.txt");
//创建一个BufferedReader缓冲对象
BufferedReader br = new BufferedReader(fd);
FileWriter fw = new FileWriter("E:\\read002.txt");
//创建一个BufferedWriter缓冲对象
BufferedWriter bw = new BufferedWriter(fw);
String str;
while((str = br.readLine()) != null){//每次读取一行文本,判断是否到达文件末尾
bw .write(str);
bw.newLine();//写入一个换行符,该方法会根据不同的操作系统生成相应的换行符
}
br.close();
bw.close();
}
}
例十运行结果
在例十中,首先对输入输出流进行包装,并通过一个while循环实现了文本文件的拷贝。在拷贝的过程中,每次循环都是用readLine()方法读取文件的一行,然后通过writer()方法写入目标文件,其中readLine()方法会逐个读取字符,当读到回车‘\r’或者换行‘\n’时会将读到的字符作为一行的内容返回。
需要注意的是:由于包装流内部使用了缓冲区,在循环中调用了BufferedWriter的write()方法写字符时,这些字符首先会被写入缓冲区,当缓冲区写满或者调用close()方法时,缓冲区中的字符才会被写入目标文件。因此在循环结束后一定要调用close()方法,否则极有可能会导致部分存在缓冲区中的数据没有写入目标文件。
2.3、LineNumberReader
java程序在编译或者运行期间会经常出现一些错误,在报告中通常会报告出错误的行数,为了方便查找错误,需要在代码中加入行号。JDK提供了一个可以跟踪行号的输入流——LineNumberReader,它是BufferedReader的直接子类,接下来通过例十一来掩饰拷贝文件时是如何为文件加上行号的。
例十一
package io.example11;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.LineNumberReader;
public class Example11 {
public static void main(String[] args) throws Exception{
FileReader fr = new FileReader("E:\\read001.txt");//创建字符输入流
FileWriter fw = new FileWriter("E:\\read003.txt");//创建字符输出流
LineNumberReader lr = new LineNumberReader(fr); //包装
lr.setLineNumber(0); //设置读取文件的起始行数
String line = null;
while((line = lr.readLine()) != null){
fw.write(lr.getLineNumber()+":"+line); //将行号写入文件中
fw.write("\r\n"); //写入换行
}
lr.close();
fw.close();
}
}
例十一运行结果
程序运行结束后,会看到拷贝前后不带行号和带行号的效果如图例十一所示。在拷贝过程中,使用LineNumberReader类跟踪行号,首先调用setLineNumber()方法设置行号的初始值,本例中将行号初始值设为0,从运行结果来看,调用getLineNumber()方法读取行号时,行号时从1开始的,这是因为LineNumberReader类在读取到换行符’\n’、回车符‘\r’或者回车后紧跟的换行符时,会将行号自动加1。
转换流
前面提到的IO流可以分为字节流和字符流,有时字节流和字符流之间也需要进行转换,在JDK中提供了两个类将可以将字节流转换为字符流InputStreamReader和OutoutStreamWriter。
转换流也是一种包装流,其中OutputStreamWriter是Writer的子类,它可以将一个字节输出流包装成字符输出流,方便直接写入字符,而InputStreamReader是Reader的子类,它可以将一个字节输入流包装成字符输入流,方便直接读取字符。接下来通过例十二来学习将字节流转换为字符流。
例十二
package io.example12;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
public class Example12 {
public static void main(String[] args) throws Exception{
FileInputStream in = new FileInputStream("E:\\read001.txt");//创建字节输入流
InputStreamReader isr = new InputStreamReader(in); //将字节流输入转换成字符输入流
BufferedReader br = new BufferedReader(isr); //对字符流进行包装
FileOutputStream out = new FileOutputStream("E:\\read004.txt");//将字节输出流转换成字符输出流
OutputStreamWriter osw = new OutputStreamWriter(out);
BufferedWriter bw = new BufferedWriter(osw); //对字符输出流对象进行包装
String line;
while((line = br.readLine()) != null){
bw.write(line);
}
br.close();
bw.close();
}
}
例十二运行结果
例十二实现了字节流和字符流之间的转换,将字节流转换为字符流,从而实现直接对字符的读写。需要注意的是,在使用转换流时,只能针对操作文本文件的字节流进行转换,如果字节流操作的是一张图片,此时转换成字符流就会造成数据丢失。