什么是流
流是个抽象的概念,是对输入输出设备的抽象,Java程序中,对于数据的输入/输出操作都是以“流”的方式进行。设备可以是文件,网络,内存等。
流具有方向性,至于是输入流还是输出流则是一个相对的概念,一般以程序为参考,如果数据的流向是程序至设备,我们成为输出流,反之我们称为输入流。
当程序需要从某个数据源读入数据的时候,就会开启一个输入流,数据源可以是文件、内存或网络等等。相反地,需要写出数据到某个数据源目的地的时候,也会开启一个输出流,这个数据源目的地也可以是文件、内存或网络等等。
流的分类
可以从不同的角度对流进行分类
- 处理的数据单位不同,可分为:字符流,字节流
- 数据流方向不同,可分为:输入流,输出流
- 功能不同,可分为:节点流,处理流
Java所有的流类位于java.io包中,都分别继承自以下四种抽象流类型
字节流 | 字符流 | |
输入流 | InputStream | Reader |
输出流 | OutputStream | Writer |
流读写操作流程
在Java中IO操作也是有相应步骤的,以文件操作为例,主要的操作流程如下:
- 使用File类打开一个文件
- 通过字节流或字符流的子类,指定输出的位置
- 进行读/写操作
- 关闭输入/输出
IO操作属于资源操作,一定要记得关闭。
字节流和字符流
在Java中如果数据流中最小的数据单元是字节,那么称这种流为字节流;如果数据流中最小的数据单元是字符,那么称这种流为字符流。
在计算机中,所有文件的储存是都是字节(byte)的形式储存。在磁盘上存储的并不是文件的字符,而是先把字符编码成字节,再储存这些字节到磁盘。在读取文件时(特别是文本文件),也是一个字节一个字节地读取以形成字节序列。
字节流可用于任何类型的对象,包括二进制对象,而字符流只能处理字符或者字符串;字节流提供了处理任何类型的IO操作的功能,但它不能直接处理Unicode字符,而字符流就可以。
字节流
在字节流中输出数据主要是使用OutputStream
完成,输入使的是InputStream
。主要用来处理字节或二进制对象。
这里我们以操作文件字节流的FileInputStream
和FileOutputStream
为例。
文件写操作
import java.io.*;
public class FileStream_Learning {
public void File_Output() throws IOException{
//创建一个File类用于操作文件
File file = new File("test.txt");
//创建一个文件字节输入流FileOutputStream
OutputStream outputStream = new FileOutputStream(file);
String content = "Hello World!";
//将String转为byte类型
byte[] bytes = content.getBytes();
//流操作,将字节流写入文件
outputStream.write(bytes);
//关闭字节输入流
outputStream.close();
}
public static void main(String[] args) throws IOException {
new FileStream_Learning().File_Output();
}
}
结果如下
如果要追加的话,请看FileOutputStream类的另一个构造方法
public FileOutputStream(File file,boolean append)throws FileNotFoundException
文件读操作
与OutputStream类一样,InputStream本身也是一个抽象类,必须依靠其子类,如果现在是从文件中读取,就用FileInputStream来实现。
import java.io.*;
public class FileStream_Learning {
public void File_Input() throws IOException{
//创建一个File类用于操作文件
File file = new File("test.txt");
//创建一个文件字节输出流FileInputStream
InputStream inputStream = new FileInputStream(file);
//开辟一块1024字节大小的内存空间
byte[] bytes = new byte[1024];
//从输入流中读取字节,并返回字节长度
int len = inputStream.read(bytes);
//将字节转为字符
String content = new String(bytes,0,len);
System.out.println(content);
inputStream.close();
}
public static void main(String[] args) throws IOException {
new FileStream_Learning().File_Input();
}
}
当然,如果觉得byte数组开辟的空间过大,也可以写成以下形式
public void File_Input() throws IOException{
//创建一个File类用于操作文件
File file = new File("test.txt");
//创建一个文件字节输出流FileInputStream
InputStream inputStream = new FileInputStream(file);
//根据文件大小开辟内存空间
byte[] bytes = new byte[(int) file.length()];
//从输入流中读取字节,并返回字节长度
inputStream.read(bytes);
//将字节转为字符
String content = new String(bytes);
System.out.println(content);
inputStream.close();
}
字符流
在字符流中输出主要是使用Writer
类完成,输入流主要使用Reader
类完成。字符流处理的单元为2个字节的Unicode字符,分别操作字符、字符数组或字符串。
文件写操作
public void File_Writer() throws IOException{
File file = new File("test2.txt");
Writer writer = new FileWriter(file);
String content= "Hello World!";
writer.write(content);
writer.close();
}
文件读操作
public void File_Reader() throws IOException{
File file = new File("test2.txt");
Reader reader = new FileReader(file);
char[] chars = new char[(int) file.length()];
reader.read(chars);
System.out.println(chars);
reader.close();
}
字节流和字符流之间的转换
可以使用InputStreamReader
和OutputStreamReader
类来把一个以字节流转换成一个字符流。
字节流和字符流的区别
除了我们上文提到的内容,字节流和字符流还有那些区别呢?
字节流在操作的时候本身是不会用到缓冲区(内存)的,是与文件本身直接操作的,而字符流在操作的时候是使用到缓冲区的。
字节流在操作文件时,即使不关闭资源(close方法),文件也能输出,但是如果字符流不使用close方法的话,则不会输出任何内容,说明字符流用的是缓冲区,并且可以使用flush方法强制进行刷新缓冲区,这时才能在不close的情况下输出内容。
在所有的硬盘上保存文件或进行传输的时候都是以字节的方法进行的,包括图片也是按字节完成,而字符是只有在内存中才会形成的,所以使用字节的操作是最多的。
如果要java程序实现一个拷贝功能,应该选用字节流进行操作(可能拷贝的是图片),并且采用边读边写的方式(节省内存)。
缓冲流
IO
操作是一个比较耗时的操作,而字节流的 read()
方法一次只能返回一个字节,那么当我们需要读取多个字节时就会出现每次读取都要进行一次 IO
操作,而缓冲流内部定义了一个大小为 8192
的 byte
数组,当我们使用了缓冲流时,读取数据的时候则会一次性最多读取 8192
个字节放到内存,然后一个个依次返回,这样就大大减少了 IO
次数;同样的,写数据时,缓冲流会将数据先写到内存,当我们写完需要写的数据时再一次性刷新到指定位置,如磁盘等。
在Java中的缓冲流同样可以分为字节流和字符流
- 字节缓冲流:BufferedInputStream,BufferedOutputStream
- 字符缓冲流:BufferedReader,BufferedWriter
缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO读取次数,从而提高读写的效率。
需要注意的是,缓冲流有一个flush()
方法,用于刷新缓冲区。该方法会将内存缓冲区内容全部写入到磁盘文件中,避免我们在关闭流的时候造成缓冲区中的数据丢失。
节点流和处理流
节点流
节点流从一个特定的数据源读写数据。即节点流是直接操作文件,网络等的流,例如FileInputStream
和FileOutputStream
,它们直接从文件中读取或往文件中写入字节流。
处理流
“连接”在已存在的流(节点流或处理流)之上通过对数据的处理为程序提供更为强大的读写功能。过滤流是使用一个已经存在的输入流或输出流连接创建的,过滤流就是对节点流进行一系列的包装。例如BufferedInputStream
和BufferedOutputStream,使用已经存在的节点流来构造,提供带缓冲的读写,提高了读写的效率,以及DataInputStream和DataOutputStream,使用已经存在的节点流来构造,提供了读写Java中的基本数据类型的功能,他们都属于过滤流。
对象流
对象流是Java中用于存储和读取基本数据类型数据或对象的流,它将对象写入到数据源中,或从数据源中还原。这里的数据源可以是文件、网络等,所以对节点流进行了包装,是一类处理流。
对于我们要处理的对象来说,该对象必须是可序列化的,即继承了Serializable接口。并且对象流不能序列化被static
和transient
修饰的成员变量。因为序列化保存的是对象的状态,包括对象的属性值等。而static修饰的成员变量保存在全局变量区,在初始化对象之前就已经存在,无法存储。
下面我们来看一下例子
类Person.java
public static class Person implements Serializable {
public int age;
public double weight;
public String name;
public Person(int age, double weight, String name){
this.age=age;
this.weight=weight;
this.name=name;
}
public int getAge(){
return this.age;
}
public double getWeight(){
return this.weight;
}
public String getName(){
return this.name;
}
}
序列化
将对象序列化为字节流保存在二进制文件中
public void serialize(Object o) throws IOException {
//开启一个对象输出流,包装的是文件输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
//向文件输出流中写入序列化对象并保存在文件中
oos.writeObject(o);
oos.close();
}
反序列化
下面我们使用对象输入流来从文件中恢复对象
public Object deserialize(String s) throws IOException, ClassNotFoundException {
//开启一个对象输入流,包装的是文件输入流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(s));
//从文件输入流中读取序列化字节对象,然后将其反序列化恢复成对象
Object o= ois.readObject();
ois.close();
return o;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = (Person) new ObjectStream_Learning().deserialize("ser.bin");
System.out.println(person.getAge());
}
结果如下