创作不易,如果觉得这篇文章对你有帮助,欢迎各位老铁点个赞呗,您的支持是我创作的最大动力!
1 Java中流的定义
流是一个很形象的概念,当程序需要读取数据的时候,就会开启一个通向数据源的流,这个数据源可以是文件,内存,或是网络连接。类似的,当程序需要写入数据的时候,就会开启一个通向目的地的流。这时候你就可以想象数据好像在这其中“流”动一样。
Java的核心库java.io提供了全面的IO接口,包括:文件读写、标准设备输出等。Java中IO是以流为基础进行输入输出的,所有数据被串行化写入输出流,或者从输入流读入
。(百度百科)
2 Java中流的分类
2.1 字节流和字符流
文件通常是由一连串的字节或字符构成,组成文件的字节序列称为字节流
,组成文件的字符序列称为字符流
。
2.2 输入流和输出流
Java中根据流的方向可以分为输入流
和输出流
。
输入流是将文件或其它输入设备的数据加载到内存的过程
。输出流则恰恰相反,是将内存中的数据保存到文件或其他输入设备
,详见下图:
文件是由字符或字节构成,那么将文件加载到内存或再将文件从内存输出到文件,需要有输入和输出流的支持,那么在Java语言中又把输入流和输出流分为了两个,字节输入流和字节输出流,字符输入流和字符输出流
,如下图所示:
2.2.1 InputStream(字节输入流)
InputStream是字节输入流,InputStream是一个抽象类,所有实现了inputStream的类都是字节输入流,核心的子类如下:
主要方法介绍:
void close()
关闭此输入流并释放与该流关联的所有系统资源。
abstract int read()
从输入流读取下一个数据字节。
int read(byte[] b)
从输入流中读取一定数量的字节并将其存储在缓冲区数组 b 中。
int read(byte[] b, int off, int len)
将输入流中最多 len 个数据字节读入字节数组。
2.2.2 OutputStream(字节输出流)
所有实现了OutputStream都是字节输出流
主要方法介绍:
void close()
关闭此输出流并释放与此流有关的所有系统资源。
void flush()
刷新此输出流并强制写出所有缓冲的输出字节。
void write(byte[] b)
将 b.length 个字节从指定的字节数组写入此输出流。
void write(byte[] b, int off, int len)
将指定字节数组中从偏移量 off 开始的 len 个字节写入此输出流。
abstract void write(int b)
将指定的字节写入此输出流。
2.2.3 Reader(字符输入流)
所有实现了Reader都是字符输如流
主要方法介绍:
abstract void close()
关闭该流。
int read()
读取单个字符。
int read(char[] cbuf)
将字符读入数组。
abstract int read(char[] cbuf, int off, int len)
将字符读入数组的某一部分。
2.2.4 Writer(字符输出流)
所有实现了Writer都是字符输出流
主要方法介绍:
Writer append(char c)
将指定字符追加到此 writer。
abstract void close()
关闭此流,但要先刷新它。
abstract void flush()
刷新此流。
void write(char[] cbuf)
写入字符数组。
abstract void write(char[] cbuf, int off, int len)
写入字符数组的某一部分。
void write(int c)
写入单个字符。
void write(String str)
写入字符串。
void write(String str, int off, int len)
写入字符串的某一部分。
3 文件流
文件流主要分为:文件字节输入流
、文件字节输出流
、文件字符输入流
、文件字符输出流
3.1 FileInputStream(文件字节输入流)
FileInputStream
主要按照字节方式读取文件
,例如我们准备读取一个文件,该文件的名称为test.txt
。
【代码示例】
public static void main(String[] args) {
InputStream is = null;
try {
is = new FileInputStream("d:\\test.txt");
int b = 0;
while ((b = is.read()) != -1) {
//直接打印
//System.out.print(b);
//输出字符
System.out.print((char) b);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
} catch (IOException e) {
}
}
}
执行结果:
从以上执行结果可以看出,test.txt文件可以正确的读取,但是打印的结果乱码了。因为这个文件中,写入了两行汉字,在使用了字节输入流读取文件的时候,它是一个字节一个字节读取的,而汉字是两个字节,所以读出一个字节就打印,那么汉字是不完整的,所以就乱码了
。
3.2 FileOutputStream(文件字节输出流)
FileOutputStream
主要按照字节方式写文件
例如: 我们做文件的复制,首先读取文件,读取后在将该文件另写一份保存到磁盘上,这就完成了备份。
【示例代码】
public static void main(String[] args) {
InputStream is = null;
OutputStream os = null;
try {
is = new FileInputStream("d:\\test.txt");
os = new FileOutputStream("d:\\test.txt.bak");
int b = 0;
while ((b = is.read()) != -1) {
os.write(b);
}
System.out.println("文件复制完毕!");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
if (os != null) {
os.close();
}
} catch (IOException e) {
}
}
}
3.3 FileReader(文件字符输入流)
FileReader
是以字符为单位读取文件
,也就是说,一次读取一个字符,即一次读取两个字节。
【代码示例】
public static void main(String[] args) {
Reader reader = null;
try {
reader = new FileReader("d:\\test.txt");
int b = 0;
while ((b = reader.read()) != -1) {
//输出字符
System.out.print((char) b);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (reader != null) {
reader.close();
}
} catch (IOException e) {
}
}
}
执行结果:
这是一段难忘的时光,在忙碌中度过了一年。
这是一段难忘的旅程,工作辛苦而充实。
因为以上是通过字符流读取的文件数据,所以没有出现乱码。
3.4 FileWriter(文件字符输出流)
【代码示例】
public static void main(String[] args) {
Writer writer = null;
try {
//以下方式会将文件的内容进行覆盖
//writer = new FileWriter("d:\\test.txt");
//writer = new FileWriter("d:\\test.txt", false);
//以下为true表示,在文件后面追加
writer = new FileWriter("d:\\test.txt", true);
writer.write("你好你好!!!!");
//换行
writer.write("\n");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (writer != null) {
writer.close();
}
} catch (IOException e) {
}
}
}
4 缓冲流
缓冲流主要是为了提高效率而存在的,减少物理读取磁盘的次数,以此来提高性能。
缓冲流主要有: BufferedInputStream
、BufferedOutputStream
、BufferedReader
、BufferedWriter
,并且BufferedReader提供了实用方法readLine()
,可以直接读取一行,BufferWriter提供了newLine()
可以用作换行符。
4.1 采用字节缓冲流改造文件复制代码
使用缓冲流,对以上代码测试用例,进行优化改造:
采用BufferedInputStream
对InputStream
进行装饰,BufferedInputStream
会将数据先读到缓存里,Java程序再次读取数据时,直接到缓存中读取
,减少Java程序物理读取的次数
,提高性能。
采用BufferedOutputStream
对FileOutputStream
进行装饰,每次写文件的时候,先放到缓存
了,然后再一次性的将缓存中的内容保存到文件中,这样会减少写物理磁盘的次数
,提高性能。
大致改造如下:
【改造后代码示例】
public static void main(String[] args) {
InputStream is = null;
OutputStream os = null;
try {
is = new BufferedInputStream(new FileInputStream("d:\\test.txt"));
os = new BufferedOutputStream(new FileOutputStream("d:\\test.txt.bak"));
int b = 0;
while ((b = is.read()) != -1) {
os.write(b);
}
//手动调用flush,将缓冲区中的内容写入到磁盘,也可以不用手动调用,缓存区满了自动会清除了
//当输出流关闭的时候也会先调用flush()
os.flush();
System.out.println("文件复制完毕!");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
if (os != null) {
//在close前会先调用flush()
os.close();
}
} catch (IOException e) {
}
}
}
可以显示的调用flush(),flush()方法的含义是刷新缓冲区
,也就是将缓存区中的数据写到磁盘上,不再放到内存里了
,在执行os.close()
关闭流资源时,其实默认执行了os.flush()
,我们在这里可以不用显示的调用flush()。
4.2 采用字符缓冲流改造文件复制代码
【改造后的代码示例】
public static void main(String[] args) {
BufferedReader r = null;
BufferedWriter w = null;
try {
r = new BufferedReader(new FileReader("d:\\test.txt"));
w = new BufferedWriter(new FileWriter("d:\\test.txt.bak"));
String s = null;
while ((s = r.readLine()) != null) {
w.write(s);
//w.write("\n");
//BufferedReader提供了实用方法readLine(),可以直接读取一行,可以采用如下方法换行
w.newLine();
}
System.out.println("文件复制完毕!");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (r != null) {
r.close();
}
if (w != null) {
//在close前会先调用flush
w.close();
}
} catch (IOException e) {
}
}
}
5 转换流
转换流主要有两个: InputStreamReader
和OutputStreamWriter
InputStreamReader
主要是将字节流输入流,转换成字符输入流OutputStreamWriter
主要是将字节流输出流,转换成字符输出流
回顾字符缓冲流:
BufferedReader
提供了实用方法readLine()
,可以直接读取一行,BufferWriter
提供了newLine()
可以用作换行符。
5.1 InputStreamReader
-
InputStreamReader
主要是将字节流输入流,转换成字符输入流
【代码示例】/** * <p> * 对FileInputStreamTest01.java进行改造,使用字符流 * <p/> * * @param args * @return void * @Date 2020/6/5 20:58 */ public static void main(String[] args) { BufferedReader br = null; try { br = new BufferedReader(new InputStreamReader(new FileInputStream("d:\\test.txt"))); String s = null; //BufferedReader提供了实用方法readLine(),可以直接读取一行 while ((s = br.readLine()) != null) { System.out.println(s);//你好你好!!!! } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { try { if (br != null) { br.close(); } } catch (IOException e) { } } }
5.2 OutputStreamReader
-
OutputStreamWriter
主要是将字节流输出流,转换成字符输出流
【代码示例】public static void main(String[] args) { BufferedWriter bw = null; try { bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("d:\\test.txt"))); bw.write("hello world"); bw.newLine(); bw.write("奋斗的青春"); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { try { if (bw != null) { bw.close(); } } catch (IOException e) { } } }
6 打印流
打印流主要包含两个: PrintStream 打印字节流
和PrintWriter 打印字符流
6.1 完成屏幕打印的重定向
System.out
其实对应的就是PrintStream打印字节流
,默认输出到控制台,我们可以重定向它的输出,可以定向到文件,也就是执行System.out.println("hello")
不输出到屏幕,而是输出到文件。
【代码示例】
/**
* @author smilehappiness
* 打印字节流
* @version 1.0
* @ClassName PrintStreamTest
* @Date 2020/6/5 21:30
*/
public class PrintStreamTest {
/**
* <p>
* 重定向它的输出,打印信息记录到文件,不再打印到控制台
* <p/>
*
* @param args
* @return void
* @Date 2020/6/5 21:21
*/
public static void main(String[] args) {
OutputStream os = null;
try {
os = new FileOutputStream("d:/log.txt");
System.setOut(new PrintStream(os));
System.out.println("hello world");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (os != null) {
os.close();
}
} catch (IOException e) {
}
}
}
}
6.2 接受屏幕输入
System.in
可以接收屏幕输入
【示例代码】
/**
* @author smilehappiness
* System.in可以接收屏幕输入
* @version 1.0
* @ClassName PrintStreamTest02
* @Date 2020/6/5 10:05
*/
public class PrintStreamTest02 {
public static void main(String[] args) {
BufferedReader br = null;
try {
br = new BufferedReader(new InputStreamReader(System.in));
String s = null;
while ((s = br.readLine()) != null) {
System.out.println(s);
//输入hello的时候,退出循环
if ("hello".equals(s)) {
break;
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (br != null) {
br.close();
}
} catch (IOException e) {
}
}
}
}
7 对象流
对象流可以将Java对象转换成二进制写入磁盘,这个过程通常叫做序列化
,并且还可以从磁盘读出完整的Java对象,而这个过程叫做反序列化
。
对象流主要包括:ObjectInputStream
和ObjectOutputStream
7.1 序列化
如果实现序列化,该类必须实现序列化接口java.io.Serializable
,该接口没有任何方法,该接口只是一种标记(标识)接口,标记这个类是可以序列化的。
【测试用例】
/**
* <p>
* 序列化测试类
* <p/>
*
* @author smilehappiness
* @Date 2020/6/5 22:05
*/
public class ObjectStreamTest01 {
public static void main(String[] args) {
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("d:/Person.dat"));
Person person = new Person();
person.setName("张三");
oos.writeObject(person);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (oos != null) {
oos.close();
}
} catch (IOException e) {
}
}
}
}
/**
* <p>
* 实现序列化接口
* <p/>
*
* @author smilehappiness
* @Date 2020/6/5 22:05
*/
@Getter
@Setter
class Person implements Serializable {
private String name;
}
如果Person不实现Serializable接口,不能实现序列化,对序列化的类是有要求的,这个序列化的类必须实现一个Serializable接口
,这个接口没有任何方法声明,它是一个标识接口
,如:java中的克隆接口Cloneable
,也是起到了一种标识性的作用。
7.2 反序列化
把序列化的对象,从磁盘中完整的读出来,就是反序列化。
【代码示例】
public static void main(String[] args) {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("d:/Person.dat"));
//反序列化
Person person = (Person) ois.readObject();
System.out.println(person.getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (ois != null) {
ois.close();
}
} catch (IOException e) {
}
}
}
7.3 serialVersionUID属性
7.3.1 反序列化错误场景演示
【示例代码】
在person对象中加入一个成员属性age,然后再读取person.dat文件
package cn.smilehappiness.io;
import java.io.*;
/**
* <p>
* serialVersionUID属性测试类
* <p/>
*
* @author smilehappiness
* @Date 2020/6/5 22:40
*/
public class ObjectStreamTest03 {
public static void main(String[] args) {
//writeObject();
readObject();
}
/**
* <p>
* 反序列化对象
* <p/>
*
* @param
* @return void
* @Date 2020/6/5 22:58
*/
private static void readObject() {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("d:/Person.dat"));
//反序列化
Person person = (Person) ois.readObject();
System.out.println(person.getName());
System.out.println(person.getAge());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (ois != null) {
ois.close();
}
} catch (IOException e) {
}
}
}
/**
* <p>
* 序列化对象
* <p/>
*
* @param
* @return void
* @Date 2020/6/5 22:58
*/
private static void writeObject() {
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("d:/Person.dat"));
Person person = new Person();
person.setName("张三");
person.setAge("20");
oos.writeObject(person);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (oos != null) {
oos.close();
}
} catch (IOException e) {
}
}
}
}
运行结果:
34行错误的原因:在序列化存储Person时,它会为该类生成一个serialVersionUID= -8840000405364729590
,而我们在该类中加入了一个age属性后,那么在使用的时候他就会为该类生成一个新的serialVersionUID= 234231939624627535
,这个两个UID(-8840000405364729590
和234231939624627535
)不同,所以Java认为是不兼容的两个类,导致反序列化生成Person对象时报错。
注意:实际开发中,也会遇到这种问题,比如说,开发完成后,发布了测试服务,然后由于变动,在一些实体类增加了新的属性,这时候在反序列化获取对象信息时,就会出现版本不一致问题,导致程序反序列化错误
。
7.3.2 解决序列化版本号不一致问题
通常在实现序列化的类中增加如下定义:
private static final long serialVersionUID = -111111111111111111L;
如果在序列化类中定义了成员域serialVersionUID
,系统会把当前serialVersionUID成员域的值作为类的序列号(类的版本号),这样不管你的类如何升级,那么他的序列号(版本号)都是一样的,就不会产生类的兼容问题
。
@Getter
@Setter
class Person implements Serializable {
//加入版本号,防止序列化兼容问题
private static final long serialVersionUID = -111111111111111111L;
private String name;
private String age;
}
以上不再出现序列化的版本问题,因为他们有统一的版本号:-111111111111111111L
用一个图来加深对序列化id的理解:
请老铁们记住:serialVersionUID
就和序列化有关。
8 File类
请参考我另一篇博文的详细介绍:Java中File类,你知道有哪些api方法吗?
9 缓冲器的作用原理
下面这段代码,摘自第8节,File详细介绍中(Java中File类,你知道有哪些api方法吗?)的一段代码,这段代码,使用缓冲字节输入流读取文件的数据
,然后使用缓冲字节输出流,写数据到另一个磁盘文件中
。
对以上代码中,bytes=new Byte[1024]
进行分析:
我的最初想法:
1,bufferedInputStream每读取一个字节,都要给i赋值,不到文档末尾i不会是-1,所以每次都要输出一个越来越长的String,直到该文件内容全部输出。
2,如果文件字节数大于1024,那么i=1024时bytes就被填满了,所以有可能只能输出文件的前1024个字节。
测试结果说明上面两个想法都是错误的,控制台输出的是整个文件的内容,并不是“越来越长”的部分内容。在文件字节大于1024时,依然能正常输出。
下面这段解释摘自原文:https://blog.csdn.net/zzuwlan_high/article/details/78553193
合理的解释:
引用API文档:“public int read(byte[] b) throws IOException:从此输入流中将最多 b.length 个字节的数据读入一个 byte 数组中。在某些输入可用之前,此方法将阻塞。
”
注意“阻塞
”二字,bufferedInputStream
执行read()方法时,不会每读一个字节就对i赋值,所以while循环就被一直堵在判断语句中,直到bytes被赋满出现异常,读取的阻塞释放,i
终于被赋了一个值1024,接下来就执行循环体的打印。
bytes
字节数组第一次塞满时,文件被读到的地方会有一个记录,所以当循环体执行完后,bufferedInputStream
会从上次循环结束的记录向下读文件,又把while循环阻塞在判断语句,直到读完最后一个字节,读取阻塞再次被释放,这次i
再次被赋值,该值为这次bufferedInputStream读到的字节数
,然后,执行循环体打印。
最后,bufferedInputStream
再次尝试读取文件,这时候已经没有字节可读,故返回-1赋给i
,循环体不再执行,循环结束。
简单来理解,i=bufferedInputStream.read(b)
的意思是:从输入流中读取bytes字节数组大小的字节,并将其存储在bytes里面,返回读取的字节数i,每循环一次bytes就被重新赋值一次
。
写博客是为了记住自己容易忘记的东西,另外也是对自己工作的总结,希望尽自己的努力,做到更好,大家一起努力进步!
如果有什么问题,欢迎大家评论,一起探讨,代码如有问题,欢迎各位大神指正!
给自己的梦想添加一双翅膀,让它可以在天空中自由自在的飞翔!