1 Java IO流的概念,分类
1.1 Java IO流的概念
java的IO是实现输入和输出的基础,可以方便的实现数据的输入和输出操作。在java中把不同的输入/输出源(键盘,文件,网络连接等)抽象表述为“流”(stream)。流的本质是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。
1.2.1按照流的流向分,可以分为输入流和输出流。
- 输入流:只能从中读入数据,而不能向其写出数据。
- 输出流:只能向其写出数据,而不能向其读入数据。
注:为什么叫写出和读入,这里涉及到一个方向问题,因为划分输入/输出流时是从程序运行所在的内存的角度来考虑的;将内存中的数据写出到硬盘中的文件,或者是将硬盘中文件的信息读入到内存。
注:java的输入流主要是InputStream和Reader作为基类,而输出流则是主要由OutputStream和Writer作为基类。它们都是一些抽象基类,无法直接创建实例。
1.2.2 按照操作单元划分,可以划分为字节流和字符流。
字符流的由来: 因为数据编码的不同,而有了对字符进行高效操作的流对象。本质其实就是基于字节流读取时,去查了指定的码表。
字节流和字符流的区别:
- 读写单位不同:字节流以字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可能读多个字节。
- 处理对象不同:字节流能处理所有类型的数据(如图片、avi等),而字符流只能处理字符类型的数据。
- 字节流:一次读入或读出是8位二进制。
- 字符流:一次读入或读出是16位二进制。
1.2.3 按照流的角色划分为节点流和处理流。
- 节点流(低级流):直接与数据源相连,读入或写出。直接使用节点流,读写不方便,为了更快的读写文件,才有了处理流。
- 处理流(高级流)和节点流一块使用,在节点流的基础上,再套接一层,套接在节点流上的就是处理流。如BufferedReader处理流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其他流的多次包装,称为流的链接。
1.3.1 流的原理浅析:
java IO流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java IO流的40多个类都是从如下4个抽象类基类中派生出来的。
- InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
- 对于InputStream和Reader而言,它们把输入设备抽象成为一个”水管“,这个水管的每个“水滴”依次排列。
- 字节流和字符流的处理方式其实很相似,只是它们处理的输入/输出单位不同而已。输入流使用隐式的记录指针来表示当前正准备从哪个“水滴”开始读取,每当程序从InputStream或者Reader里面取出一个或者多个“水滴”后,记录指针自定向后移动;除此之外,InputStream和Reader里面都提供了一些方法来控制记录指针的移动。
- 对于OutputStream和Writer而言,它们同样把输出设备抽象成一个”水管“,只是这个水管里面没有任何水滴。
- 当执行输出时,程序相当于依次把“水滴”放入到输出流的水管中,输出流同样采用隐示指针来标识当前水滴即将放入的位置,每当程序向 OutputStream 或者 Writer 里面输出一个或者多个水滴后,记录指针自动向后移动。
注:Java的处理流模型则体现了Java输入和输出流设计的灵活性。处理流的功能主要体现在以下两个方面。
- 性能的提高:主要以增加缓冲的方式来提供输入和输出的效率。
- 操作的便捷:处理流可能提供了一系列便捷的方法来一次输入和输出大批量的内容,而不是输入/输出一个或者多个“水滴”。
1.3.2 java输入/输出流体系中常用的流的分类表
分类 | 字节输入流 | 字节输出流 | 字符输入流 | 字符输出流 |
---|---|---|---|---|
抽象基类 | InputStream | OutputStream | Reader | Writer |
访问文件 | FileInputStream | FileOutputStream | FileReader | FileWriter |
访问数组 | ByteArrayInputStream | ByteArrayOutputStream | CharArrayReader | CharArrayWriter |
访问管道 | PipedInputStream | PipedOutputStream | PipedReader | PipedWriter |
访问字符串 | StringReader | StringWriter | ||
缓冲流 | BufferedInputStream | BufferedOutputStream | BufferedReader | BufferedWriter |
转换流 | InputStreamReader | OutputStreamWriter | ||
对象流 | ObjectInputStream | ObjectInputStream | ||
抽象基类 | FilterInputStream | FilterOutputStream | FilterReader | FilterWriter |
打印流 | PrintStream | PrintWriter | ||
推回输入流 | PushbackInputStream | PushbackReader | ||
特殊流 | DataInputStream | DataOutputStreamn |
注:表中粗体字所标出的类代表节点流,必须直接与指定的物理节点关联:斜体字标出的类代表抽象基类,无法直接创建实例。
2.1 IO体系的基类(InputStream/Reader,OutputStream/Writer)
字节流和字符流的操作方式基本一致,只是操作的数据单元不同——字节流的操作单元是字节,字符流的操作单元是字符。
InputStream和Reader是所有输入流的抽象基类,本身并不能创建实例来执行输入,但它们将成为所有输入流的模板,所以它们的方法是所有输入流都可使用的方法。
在InputStream里面包含如下3个方法。
- int read(); 从输入流中读取单个字节(相当于从水管中取出一滴水),返回所读取的字节数据(字节数据可直接转换为int类型)。
- int read(byte[] b)从输入流中最多读取b.length个字节的数据,并将其存储在字节数组b中,返回实际读取的字节数。
- int read(byte[] b,int off,int len); 从输入流中最多读取len个字节的数据,并将其存储在数组b中,放入数组b中时,并不是从数组起点开始,而是从off位置开始,返回实际读取的字节数。
在Reader中包含如下3个方法。
- int read(); 从输入流中读取单个字符(相当于从水管中取出一滴水),返回所读取的字符数据(字节数据可直接转换为int类型)。
- int read(char[] b)从输入流中最多读取b.length个字符的数据,并将其存储在字节数组b中,返回实际读取的字符数。
- int read(char[] b,int off,int len); 从输入流中最多读取len个字符的数据,并将其存储在数组b中,放入数组b中时,并不是从数组起点开始,而是从off位置开始,返回实际读取的字符数。
InputStream和Reader提供的一些移动指针的方法:
- void mark(int readAheadLimit); 在记录指针当前位置记录一个标记(mark)。
- boolean markSupported(); 判断此输入流是否支持mark()操作,即是否支持记录标记。
- void reset(); 将此流的记录指针重新定位到上一次记录标记(mark)的位置。
- long skip(long n); 记录指针向前移动n个字节/字符。
OutputStream和Writer的用法也非常相似,两个流都提供了如下三个方法:
- void write(int c); 将指定的字节/字符输出到输出流中,其中c即可以代表字节,也可以代表字符。
- void write(byte[]/char[] buf); 将字节数组/字符数组中的数据输出到指定输出流中。
- void write(byte[]/char[] buf, int off,int len ); 将字节数组/字符数组中从off位置开始,长度为len的字节/字符输出到输出流中。
因为字符流直接以字符作为操作单位,所以Writer可以用字符串来代替字符数组,即以String对象作为参数。Writer里面还包含如下两个方法。
- void write(String str); 将str字符串里包含的字符输出到指定输出流中。
- void write (String str, int off, int len); 将str字符串里面从off位置开始,长度为len的字符输出到指定输出流中。
2.2 IO体系的基类文件流的使用(FileInputStream/FileReader ,FileOutputStream/FileWriter)
- 使用FileInputStream读取文件:
@Test
public void fileInputStreamTest() throws IOException {
FileInputStream fis = null;
try {
// 创建字节输入流
fis = new FileInputStream(
"D:/test/test.txt");
// 创建一个长度为1024的竹筒
byte[] b = new byte[1024];
// 用于保存的实际字节数
int hasRead = 0;
// 使用循环来重复取水的过程
while ((hasRead = fis.read(b)) > 0) {
// 取出竹筒中的水滴(字节),将字节数组转换成字符串进行输出, 后面的gbk是为了处理中文乱码问题
System.out.print(new String(b, 0, hasRead, "gbk"));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
fis.close();
}
}
注:上面程序最后使用了fis.close()来关闭该文件的输入流,与JDBC编程一样,程序里面打开的文件IO资源不属于内存的资源,垃圾回收机制无法回收该资源,所以应该显示的关闭打开的IO资源。Java 7改写了所有的IO资源类,它们都实现了AntoCloseable接口,因此都可以通过自动关闭资源的try语句来关闭这些IO流。
- 使用FileReader读取文件:
@Test
public void fileReaderTest() throws IOException{
FileReader fis = null;
try {
// 创建字节输入流
fis = new FileReader("D:/test/test.txt");
// 创建一个长度为1024的竹筒
char[] b = new char[1024];
// 用于保存的实际字节数
int hasRead = 0;
// 使用循环来重复取水的过程
while ((hasRead = fis.read(b)) > 0) {
// 取出竹筒中的水滴(字节),将字节数组转换成字符串进行输出
System.out.print(new String(b, 0, hasRead));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
fis.close();
}
}
可以看出使用FileInputStream和FileReader进行文件的读写并没有什么区别,只是操作单元不同而且。
- FileOutputStream的用法:
@Test
public void fileOutputStreamTest() throws IOException {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
// 创建字节输入流
fis = new FileInputStream("D:/test/test.txt");
// 创建字节输出流
fos = new FileOutputStream("D:/test/test2.txt");
byte[] b = new byte[1024];
int hasRead = 0;
// 循环从输入流中取出数据
while ((hasRead = fis.read(b)) > 0) {
// 每读取一次,即写入文件输入流,读了多少,就写多少。
fos.write(b, 0, hasRead);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
fis.close();
fos.close();
}
}
注: 使用java的IO流执行输出时,不要忘记关闭输出流,关闭输出流除了可以保证流的物理资源被回收之外,可能还可以将输出流缓冲区中的数据flush到物理节点中里(因为在执行close()方法之前,自动执行输出流的flush()方法)。java很多输出流默认都提供了缓存功能,其实我们没有必要刻意去记忆哪些流有缓存功能,哪些流没有,只有正常关闭所有的输出流即可保证程序正常。
4.缓冲流的使用(BufferedInputStream/BufferedReader, BufferedOutputStream/BufferedWriter):
字节缓冲流
@Test
public void bufferedTest() throws IOException {
FileInputStream fis = null;
FileOutputStream fos = null;
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
// 创建字节输入流
fis = new FileInputStream("D:/test/test.txt");
// 创建字节输出流
fos = new FileOutputStream("D:/test/test3.txt");
// 创建字节缓存输入流
bis = new BufferedInputStream(fis);
// 创建字节缓存输出流
bos = new BufferedOutputStream(fos);
byte[] b = new byte[1024];
int hasRead = 0;
// 循环从缓存流中读取数据
while ((hasRead = bis.read(b)) > 0) {
// 向缓存流中写入数据,读取多少写入多少
bos.write(b, 0, hasRead);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
bis.close();
bos.close();
}
}
可以看到使用字节缓存流读取和写入数据的方式和文件流(FileInputStream,FileOutputStream)并没有什么不同,只是把处理流套接到文件流上进行读写。
上面代码中我们使用了缓存流和文件流,但是我们只关闭了缓存流。这个需要注意一下,当我们使用处理流套接到节点流上的使用的时候,只需要关闭最上层的处理就可以了。java会自动帮我们关闭下层的节点流。
2.3 转换流的使用(InputStreamReader/OutputStreamWriter):
下面以获取键盘输入为例来介绍转换流的用法。java使用System.in代表输入。即键盘输入,但这个标准输入流是InputStream类的实例,使用不太方便,而且键盘输入内容都是文本内容,所以可以使用InputStreamReader将其包装成BufferedReader,利用BufferedReader的readLine()方法可以一次读取一行内容,如下代码所示:
- 转换流的使用
@Test
public void changeStreamTest(){
try {
// 将System.in对象转化为Reader对象
InputStreamReader reader=new InputStreamReader(System.in);
//将普通的Reader包装成BufferedReader
BufferedReader bufferedReader=new BufferedReader(reader);
String buffer=null;
while ((buffer=bufferedReader.readLine())!=null){
// 如果读取到的字符串为“exit”,则程序退出
if(buffer.equals("exit")){
System.exit(1);
}
//打印读取的内容
System.out.print("输入内容:"+buffer);
}
}catch (IOException e){
e.printStackTrace();
}finally {
}
}
上面程序将System.in包装成BufferedReader,BufferedReader流具有缓存功能,它可以一次读取一行文本——以换行符为标志,如果它没有读到换行符,则程序堵塞。等到读到换行符为止。运行上面程序可以发现这个特征,当我们在控制台执行输入时,只有按下回车键,程序才会打印出刚刚输入的内容。
2.4 对象流的使用(ObjectInputStream/ObjectOutputStream)的使用:
对象类:
package com.design.prompt;
import java.io.Serializable;
public class UserTest implements Serializable {
/**
*
*/
private static final long serialVersionUID = 6522988290199824802L;
private String username;
transient String password;
public UserTest(){
}
public UserTest(String username, String password) {
super();
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "UserTest [username=" + username + ", password=" + password
+ "]";
}
}
- 写出对象:
@Test
public void objectWriterTest() {
OutputStream outputStream = null;
BufferedOutputStream buf = null;
ObjectOutputStream obj = null;
try {
// 序列化文件輸出流
outputStream = new FileOutputStream("D:/test/test.txt");
// 构建缓冲流
buf = new BufferedOutputStream(outputStream);
// 构建字符输出的对象流
obj = new ObjectOutputStream(buf);
// 序列化数据写入
obj.writeObject(new UserTest("A", "123"));// Person对象
// 关闭流
obj.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
- 读入对象
@Test
public void objectReaderTest() {
try {
InputStream inputStream = new FileInputStream("D:/test/test.txt");
// 构建缓冲流
BufferedInputStream buf = new BufferedInputStream(inputStream);
// 构建字符输入的对象流
ObjectInputStream obj = new ObjectInputStream(buf);
UserTest tempPerson = (UserTest) obj.readObject();
System.out.println("UserTest对象为:" + tempPerson);
// 关闭流
obj.close();
buf.close();
inputStream.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
注:使用对象流的一些注意事项
1.读取顺序和写入顺序一定要一致,不然会读取出错。
2.在对象属性前面加transient关键字,则该对象的属性不会被序列化。