IO流在整个Java的体系中都是十分重要的,最近用来觉得一些东西比较生疏,就回过头梳理一下整个知识体系加深印象。
概念
首先我们来讲一些概念性的东西,从整体上感受一下这个流程:IO就是Input和Output。一个是in,一个是out。对应的:一个是进,一个是出。
我总是很喜欢用参照系这个概念,IO的参照系就是我们的应用程序,这里可以理解成我们编写的项目工程。
那么In是什么,In就是向我们的程序中读入,我们叫Read。Out是什么,Out就是我们从程序写出,我们叫Write。
很好理解吧?那么我们就可以想象一下整个数据走过的流程了:从数据源的源设备我们把数据读入到程序内,经过我们程序的一系列处理对数据进行加工,数据处理好了,再从我们的应用程序把数据给写出来到目标设备。
刚刚我们提到了一个东西叫数据源,数据源,顾名思义就是数据的源头。一般的,可以作为数据源的,可以是我们的本地文件、数据库,再或者服务器。也就是说,凡是可以和我们的应用程序产生数据交互的都可以称之为数据源。
注意我们这里说的是交互,也就是说,不只是为应用程序(Program)提供数据的叫数据源,最后接受承载来自应用程序的也称为数据源。数据源分为:源设备(Source),对应输入流;目标设备(Dest),对应输出流。
这里其实不必纠结,反过来思考一下,为什么引入数据源这个概念?其实,我们对于设备而言,是不是既可以做源设备提供数据,也可以作为目标设备接收数据呢?也就是说,我们的数据在实际情况下,往往不只需要一个应用程序的一次加工,那么应用程序输出的数据,也就可以作为下一次加工的输入数据了。
接下来,我们再说一下上面提到的这个输入流和输出流。我们总是说IO流,其实说的就是输入流和输出流。那么什么是流?我们用图来理解一下。
我们说,由于文件的形式、格式有很多种,大小也各不相同,我们往往无法一次性的将所有的数据都灌入到程序中,同样对于输出也是一样。那么,我们就可以在数据源与应用程序之间,建立一个管道,让数据像流一样,通过管道源源不断的流入或流出。这,就是流的概念。当然了,从数据源流入到应用程序的,就叫做输入流(InputStream);从应用程序流出到数据源的,就叫做输出流(OutputStream)。那么,我们数据流需要一个管道作为媒介,这个管道我们把它称为缓冲区,缓冲区的大小我们和以理解成管道的容积。
那么对于任何的数据源和应用程序,我们都可以建立这种流关系,将数据进行读入与写出。工作都做完了,最后,我们还不要忘了把管子给拔下来,也就是关闭流,调用close()方法。
现在概念我们都理解了,接下来就谈一谈对于程序来说,整个过程怎样编写。
File类
想要学习IO,需要了解一个与IO流密不可分的类——File。File就是文件,IO流的本质也就是对文件的处理。
涉及到File对象的创建,路径的使用等等。还有一些File的常用方法,需要掌握。
File体系比较庞大,这里不做详细说明。
有关File类的具体内容请看我的这篇文章。
四大常用IO抽象类
在Java中,IO流有四大常用抽象类。也就是说,一般的情况下,我们可以通过继承这四个类完成基本的流操作。而其他更高级的类与方法,也都是在这四个类的基础上编写的,又一次很好地体现了Java的继承思想。
那么下面我们就来介绍这四个类:
InputSream / OutputStream
InputStream是字节输入流的所有类的父类。
OutputStream是字节输出流的所有类的父类。
所有数据的读入和写出都需要他们的子类来实现。
数据单位为字节Byte(8 bit)。
Reader/Writer
Reader是字符输入流的所有类的父类。
Writer是字符输出流的所有类的父类。
所有数据的读入和写出都需要他们的子类来实现。
数据单位为字符char(2 Byte 16 bit)。
下面我们简单说一下字节流和字符流的区别:
也就是什么时候该使用字节流,什么时候使用字符流?
对于字节流,它可以支持声音,视频,图片,文本等所有类型,而字符流只支持文本文件。
但是,我们说字节流虽然可以处理所有的文件类型,可是我们需要注意,当用字节流处理文本文件时,由于编码方式的原因,他无法解析中文。所以,我们一般用字符流处理文本文件。
提到了字符和字节,咱们需要简单了解一下编码:
我们知道,计算机的底层都是通过二进制实现的,计算机他只懂得0和1,也只能显示0和1,那如果一直这样的话,我们编写程序和读程序的时候难不成要成天01010101的吗?这不好写,也根本看不懂。
因此,我们就需要把它转化成我们所能理解的方式,这就引入了一种方法——编码。由我们人为地来规定一种规则,什么样的01组合代表什么文字。
在程序编写时,我们用某种编码方式对程序进行编写,在查看程序时,再用该编码对应的解码方式进行解码。(我们的编译器的本质其实就是这个作用)
常见的编码方式有:
ASCII码:单字节编码,不支持中文。
GBK:国标码。国产的,采用单双字节变长编码,英文使用单字节完全兼容ASCII字符编码,中文部分采用双字节编码。
Unicode:ISO制定,是一套字符集,支持中文。有三种编码方式:
UFT-8:一种变长的编码方案,使用 1~6 个字节来存储;
UFT-32:一种固定长度的编码方案,不管字符编号大小,始终使用 4 个字节来存储;
UTF-16:介于 UTF-8 和 UTF-32 之间,使用 2 个或者 4 个字节来存储,长度既固定又可变。
UTF-8编码:双字节编码(一个字符),支持中文。(tip:utf-8中,一个汉字占3个字节)
说到utf-8多说几句,在编程中往往需要设置编码方式。对于utf-8而言,很多时候写UTF-8对,utf-8对,utf8也对。
注:由于Unicode编码强制两字节,对于单字节文字造成空间的浪费,所以在Unicode的基础上,改进并诞生了UTF-8。utf-8 区分每个字符的开始是根据字符的高位字节来区分的,比如用一个字节表示的字符,第一个字节高位以“0”开头;用两个字节表示的字符,第一个字节的高位为以“110”开头,后面一个字节以“10开头”;用三个字节表示的字符,第一个字节 以“1110”开头,后面俩字节以“10”开头;用四个字节表示的字符,第一个字节以“11110”开头,后面的三个字节以“10”开头。
如果编码方式和解码方式不一致的话,就会导致乱码,就是我们常常会看到的那样,文件打开是一堆看不懂的文字,时不时还能蹦出来个笑脸。
这部分我们简单了解一下就好,无需深究。
那么我们突发奇想一下,既然字节比字符单位更小,那么能不能用字节流读取字符呢?
答案是可以的,但是,我们说由于编码方式不同,字节流在读取字符流的时候,会按照ASCII码的方式解码,将字符输出为对应的ASCII码。
言归正传,Java所采用的编码方式是Unicode编码,所以,是双字节编码。两个字节,一个字符。我们继续对四个抽象类的派生类及运用进行进一步的解析:
InputStream
三个常用的类方法为:ByteArrayInputStream、StringBufferInputStream、FileInputStream。它们分别对应了:字节数组、字符串缓冲区和文件的数据读入操作。
PipedInputStream用于建立多线程的数据通道,将多个数据通道合为一个通道,为线程共用的管道中读取数据。
ObjectInputStream 和FilterInputStream 是装饰流,用于流的修饰,前者是用于对象反序列化,后者为过滤器。
//序列化操作可以将一个对象写出,或者读取一个对象到程序中,也就是执行了序列化和反序列化操作。具体后面进行说明。
OutputStream
两个常用类方法为:ByteArrayOutputStream、FileOutputStream 分别对应项Byte 数组、和文件中写出数据。
PipedOutputStream 是向与其它线程共用的管道中写入数据。
ObjectOutputStream 和所有FilterOutputStream 同样用于流的修饰。
Reader
Reader有四个常用子类FileReader、 BufferedReader、InputStreamReader、CharArrayReader。
FileReader实现字符串的读入。
BufferedReader采用缓冲区的方式对数据高效读入。
InputStreamReader用于从字节到字符的转换。
CharArrayReader用于数组数据的读入。
Writer
Writer有六个常用子类:BufferedWriter、FileWriter、PrintWriter、PipedWriter、PrintSWrite、CharArrayWriter。
BufferedWriter重写了父类flush()方法,运用了缓冲区。
FileWriter用于写入字符串到文件。
PipedWriter主要用于线程间通讯,也可以用来传输字符。
PrintSWrite本质上是PrintStream的字符形式的版本
CharArrayWriter是向数组中写出数据。
由于IO的子类都是一对一对的,而且都是源自于四个基础的抽象类,讲了这么多,我们来简单整理一下:
讲了这么多,下面我们对共性的性质进行下简单的描述:
细化
我们已经对四大抽象类进行了介绍,对IO流有了一个初步的认识,下面我们继续从宏观上整体细化一下IO的整体架构。
我们做一张图对流进行一下描述:
节点流
文件:对文件进行处理
数组 :对数组进行处理的节点流,对应是内存中的一个数组。
字符串 :对字符串进行处理的节点流
线程:对管道进行处理的节点流,实现多线程的流共用。
FileInputStream读入
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class firstIO {
//程序的主方法
public static void main(String [] args) throws IOException{
int i=0;
FileInputStream in=null;
try {
in=new FileInputStream("C:\\Users\\10854\\Desktop\\1.txt");//从文件外读数据
} catch (FileNotFoundException e) {
e.printStackTrace();
}
try {
int num=0;//字节计数器
while((i=in.read())!=-1){
System.out.println((char)i);//将ASCII码值转换成字符
num++;
}
in.close();
System.out.println("传输字节个数:"+num);
} catch (Exception e) {
// TODO: handle exception
System.out.println("读取文件错误");
}
}
}
FileOutputStream写出
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class outIO {
public static void main(String []args){
int i=0;
FileInputStream in=null;
FileOutputStream out=null;
try {
//实例化FileInputStream,FileOutputStream对象
in=new FileInputStream("C:\\Users\\10854\\Desktop\\1.txt");
out=new FileOutputStream("C:\\Users\\10854\\Desktop\\2.txt");
while((i=in.read())!=-1){
out.write(i);
System.out.println((char)i);
}
in.close();
out.close();
System.out.println("文件已复制");
} catch (Exception e) {
// TODO: handle exception
System.out.println("复制失败");
System.exit(-1);
}
}
}
FileRead、FileWriter进行文件复制
import java.io.FileReader;
import java.io.FileWriter;
public class TestFieldWriter1 {
public static void main(String []args){
FileReader fr=null;
FileWriter fw=null;
try {
fr=new FileReader("C:\\Users\\10854\\Desktop\\1.txt");//读取的文件
fw=new FileWriter("C:\\Users\\10854\\Desktop\\3.txt");//目的文件
int i=0;
while((i=fr.read())!=-1){
fw.write(i);
System.out.println((char)i);//将得到的ASCII码值转换成字符
}
fr.close();
fw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
处理流
缓冲流:增加缓冲功能,避免频繁读写硬盘。(通过Buffer实现)
转换流:实现字节流和字符流之间的转换,需要InputStream或OutputStream作为参数。
数据流: 提供将基础数据类型写入到文件中,或者读取出来。
缓冲区
我们上面整理过,处理流中有一个流叫做缓冲流,缓冲流的类包括:BufferedInputStream 、BufferedOutputStream、 BufferedReader、 BufferedWriter。观察可以发现,这四个类都有一个共性的关键字为Buffer(缓冲器、缓冲区),Buffer就是我们缓冲流的辨别标志。
接下来我们来讲一下缓冲区(Buffer),有一个方法flush,作用是刷新缓冲区。
我们前文提到,数据之间的传输我们可以通过一根管子(流)来建立连接,那么有的时候,我们完成了一个阶段的数据传输,想查看一下数据,可是此时管子里还留存着一部分数据,这部分数据如果不进行处理,就无法在文件中查看到这部分数据,那么我们就需要做一个操作flush(刷新)。将这部分数据处理掉,让这一阶段的所有的数据都到它该去的地方,完成数据传输。
对于BufferedInputStream和BufferedReader体现在数据的读入上,缓冲区对于它的意义就是提高运输量,减少解码次数,提高程序的效率,这里我们无需主动调用flush方法。
有一位博主的一篇文章解释得特别好,这里分享给大家BufferedReader流
然后我们继续说写出流,以BufferedInputStream为例,我们首先来看一下源码:
BufferedInputStream 继承自 FilterInputStream。InputStream和FilterInputStream中没有flush方法,buffer的操作是体现在BufferedInputStream对close方法的重写中的。
public void close() throws IOException {
byte[] buffer;
while ( (buffer = buf) != null) {
if (bufUpdater.compareAndSet(this, buffer, null)) {
InputStream input = in;
in = null;
if (input != null)
input.close();
return;
}
// Else retry in case a new buf was CASed in fill()
}
}
BufferedOutputStream类重写了父类的flush方法,并在其中调用了flushBuffer()方法以及OutputStream的flush()方法。
BufferedOutputStream继承自FilterOutputStream,FilterOutputStream继承自OutputStream。FilterOutputStream和OutputStream类的flush()什么操作都没做,方法为空。
public void flush() throws IOException {
}
对于BufferedOutputStream我们查看一下源码:
public synchronized void flush() throws IOException {
flushBuffer();
out.flush();
}
private void flushBuffer() throws IOException {
if (count > 0) {
out.write(buf, 0, count);
count = 0;
}
}
public synchronized void write(byte b[], int off, int len) throws IOException {
if (len >= buf.length) {
/* If the request length exceeds the size of the output buffer,
flush the output buffer and then write the data directly.
In this way buffered streams will cascade harmlessly. */
flushBuffer();
out.write(b, off, len);
return;
}
if (len > buf.length - count) {
flushBuffer();
}
System.arraycopy(b, off, buf, count, len); //写入缓冲区
count += len;
}
可以看到重写了本为空的flush,调用了新方法flushBuffer。flushBuffer中调用了write。我们可以看到缓冲区buffer本质为一个byte[]数组,BufferedOutputStream的每一次write其实是将内容写入byte[],当buffer空间满了之后,就会将数据写入。
然后我们看一下close()方法:
@SuppressWarnings("try")
public void close() throws IOException {
synchronized (lock) {
if (out == null) {
return;
}
try (Writer w = out) {
flushBuffer();
} finally {
out = null;
cb = null;
}
}
}
可以发现在close方法中调用了flushBuffer,清空了缓冲区。
BufferedReader可以指定缓冲区的大小,默认为8192 char。这点在源码中有所体现。
那么我们来总结一下什么时候才需要主动调用flush()?
示例如下:
public static void main(String[] args) throws IOException {
File demo = new File("demo");
if(!demo.exists()){
demo.mkdir();
}
File file = new File(demo, "raf.dat");
if(!file.exists()){
file.createNewFile();
}
PrintWriter pw = new PrintWriter(file);
String s = "";
for(int i = 0;i<2000;i++){
s="我是要写入到记事本文件的内容"+i;
pw.write(s);
}
pw.close();
}
在这段代码中,
只写pw.close() ,完整输出“我是要写入到记事本文件的内容1999”。
不写pw.close(),输出“我是要写入到记事本文件的内容1804”(不完整输出,因为1804到1999这部分没有填满缓冲区)。
只写pw.flush();也可以进行完整的输出。(清空了缓冲区,所以完整输出。不过此时流未关闭,实际情况下应注意将流关闭)。
也就是close()时会自动flush。而在暂时不关闭流的情况下,需要执行一个刷新操作,把缓冲区的内容写出,就需要调用flush();
BufferWriter源码原理设定与BufferedOutputStream类似,故这种情况适用于字节流:BufferedOutputStream和Writer。
Serializable序列化
Serializable是对象序列化接口,一个类只有实现了Serializable接口,它的对象才能够被实例化。序列化的目的是为了将对象持久性保存。Serializable其实是空的,它只是一个标识接口没有实质性内容,而是作为一个标识存在作用就是为了提醒JVM,这个类需要JVM进行序列化操作。
serialversionUID
实体类在实现Serializable接口后,需要定义一个long类型的值 serialversionUID,这个UID将会在序列化操作时被系统写入到文件。它的作用是作为一个检测依据,当对文件数据进行反序列化操作时,判断二者的serialversionUID是否一致。如若一致,说明一切正常,可以反序列化成功;如果不一致,说明序列化前后类的属性发生了改变,将无法反序列化成功,抛出异常。
已经了解了什么是序列化,下面我们就对ObjectOutputStream和ObjectInputStream进行讲解。大家注意一下顺序:ObjectOutputStream、ObjectInputStream。
ObjectOutputStream可以将一个对象从程序中写出到文件,执行序列化操作。
ObjectInputStream可以将一个对象从文件读入到程序中,执行反序列化操作。
也就是ObjectOutputStream把对象变成序列存到文件中,而ObjectInputStream从文件中把序列取出来变回对象。
下面我们演示一下:
首先创建一个演示类:
public class Student implements Serializable {
//注:类应该实现序列化接口Serializable
private String name;
private Integer age;
public Student(String name, Integer age) {
super();
this.name = name;
this.age = age;
}
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
}
ObjectOutputStream
实现对象的序列化。
public class TestObjectOutputStream {
public static void main(String[] args) throws IOException{
Student s1=new Student("张三",18);
Student s2=new Student("李四",19);
ObjectOutputStream os=new ObjectOutputStream(new FileOutputStream("Students.txt"));
os.writeObject(s1);
os.writeObject(s2);
os.close();
}
}
ObjectInputStream
实现对象的反序列化。
public class TestObjectInputStream {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream ois=new ObjectInputStream(new FileInputStream("Students.txt"));
Student s1=(Student) ois.readObject();
Student s2=(Student) ois.readObject();
System.out.println(s1);
System.out.println(s2);
ois.close();
}
}
优化
在此基础上,我们可以采用数据结构对对象的存取进行优化。
public class Test{
public static void main(String[] args) throws IOException, ClassNotFoundException {
ArrayList<Student> list1=new ArrayList<Student>();
list1.add(new Student("张三",18));
list1.add(new Student("李四",19));
list1.add(new Student("王五",20));
list1.add(new Student("赵六",21));
ObjectOutputStream os=new ObjectOutputStream(new FileOutputStream("Students.txt"));
os.writeObject(list1);
System.out.println("写入成功!");
os.close();
ObjectInputStream osi=new ObjectInputStream(new FileInputStream("Students.txt"));
ArrayList<Student> list2=(ArrayList<Student>) osi.readObject();
osi.close();
System.out.println("读取对象集合");
for (Student student : list2) {
System.out.println(student);
}
}
}