梳理Java IO流,攻破NIO的堡垒!【技术创作101训练营】

时间:2022-07-25
本文章向大家介绍梳理Java IO流,攻破NIO的堡垒!【技术创作101训练营】,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

前言

在上一篇的文章获取不错的浏览量后,继续加更的念头一直徘徊在心中,本来是想花段时间深入学习tomcat的,可是tomcat的源码中就有至关重要的NIO,于是得先整一下NIO,但是NIO的基础是BIO,于是这篇文章就先写IO流吧。

学习NIO(非阻塞IO),千万不能被IO阻塞住哇!

IO流在java中其实是很重要的一块知识点,难度还好,但是容易被忽视,因为工作中真正写IO的代码少之又少。

IO的难点在于

  1. IO流api很多,各种基础的流,包装的流嵌套使用很难记忆
  2. 基本每个方法都要抛出非运行时异常

导致很多开发学过io流一段时间后,写不出一段正确的io流代码。

本文将从理论+代码的方式由浅入深的带大家学习IO流,通过图解的方式来记忆常用的IO流。

文末有IO总结的思维导图,很多博文采用的都是上来一张图,我觉得对于阅读者来说很容易陷进去,所以建议理清各个流后再去看思维导图。

File

在正式的介绍IO流之前,我觉得应该介绍一下File类,该类主要是对文件和目录的抽象表示,因为学习io流第一反应就是文件,该类提供了对文件的创建、删除、查找等操作。主要有以下特点

  1. java的世界万物皆对象,文件和目录就可抽象为File对象
  2. 对于File而言,封装的并不是真正的文件,封装的仅仅是一个路径名,磁盘文件本身可以存在,也可以不存在
  3. 文件的内容不能用File读取,而是通过流来读取,File对象可以作为流的来源地和目的地

File类的常用构造方法

构造方法

方法说明

File(String pathname)

将路径字符串抽象为File实例,路径字符串可以是相对路径,也可以为绝对路径

File(String parent, String child)

从父路径名和子路径名来构建File实例

File(File parent, String child)

根据父File实例和子路径名来构建File实例

如下示例,表示这几种文件和目录的代码

// pathname
File liuBei = new File("D:/三国/刘备.jpg");
// String parent, String child
File guanYu = new File("D:/三国", "关羽.jpg");
// 目录
File sanGuo = new File("D:/三国");
// File parent, String child
File zhangFei = new File(sanGuo, "张飞.txt");
// 可以声明不存在的文件
File zhuGeLiang = new File(sanGuo, "诸葛亮.txt");

绝对路径和相对路径

绝对路径:从盘符开始的路径,表示一个完整的路径。(经常使用)

相对路径:相对于当前项目目录的路径

File f = new File("D:/bbb.java");
// D:bbb.java
System.out.println(f.getAbsolutePath());
File f2 = new File("bbb.java");
// F:codeadbbb.java
System.out.println(f2.getAbsolutePath());

路径分隔符和换行符

路径分隔符

  1. windows的路径分隔符:
  2. linux的路径分隔符: /

java有常量separator表示路径分隔符

public static final String separator = "" + separatorChar;

换行符

  1. windows的换行符: rn
  2. linux的换行符 n

File的常用方法

创建、删除

方法名

方法说明

boolean createNewFile() throws IOException

当该名称的文件不存在时,创建一个由该抽象路径名的空文件并返回true,当文件存在时,返回false

boolean mkdir()

创建由此抽象路径名命名的目录

boolean mkdirs()

创建由此抽象路径名命名的目录,包括任何必需但不存在的父目录。级联创建目录

boolean delete()

删除由此抽象路径名表示的文件或目录

上述方法比较简单,其中需要注意的是

  • 创建多级目录时,mkdir创建失败,返回false,mkdirs创建成功,返回true(推荐使用mkdirs)
  • 删除目录时,目录内不为空时,删除失败,返回false, 即只能删除文件或者空目录
File shuiHu = new File("D:/四大名著/水浒传");
// 返回false 创建失败
boolean mkdir = shuiHu.mkdir();
// 返回true 创建失败
boolean mkdirs = shuiHu.mkdirs();

File four = new File("D:/四大名著");
// 返回false 删除目录时必须目录为空才能删除成功
boolean delete = four.delete();

File shuiHu = new File("D:/四大名著/水浒传");
// true 正确删除了水浒传目录
boolean delete1 = shuiHu.delete();

File liuBei = new File("D:/三国/刘备.jpg");
// 返回true 正确删除了刘备.jpg文件
boolean delete2 = liuBei.delete();

文件检测

方法名

方法说明

boolean isDirectory()

判断是否是目录

boolean isFile()

判断是否是文件

boolean exists()

判断文件或目录是否存在

boolean canWrite()

文件是否可写

boolean canRead()

文件是否可读

boolean canExecute()

文件是否可执行

long lastModified()

返回文件的上次修改时间

注意的是

  • 文件或目录不存在时, isDirectory() 或 isFile() 返回false
  • 可读、可写、可执行是对操作系统给文件赋予的权限
File xiYou = new File("D:/西游记");
// 文件或目录不存在时 返回false
System.out.println(xiYou.isDirectory());

文件获取

方法名

方法说明

String getAbsolutePath()

返回File对象的绝对路径字符串

String getPath()

将此抽象路径名转换为路径名字符串

String getName()

返回文件或目录的名称

long length()

返回由此File表示的文件的字节数

String[] list()

返回目录中的文件和目录的名称字符串数组

File[] listFiles()

返回目录中的文件和目录的File对象数组

注意

  • length() 返回的是文件的字节数,目录的 长度是0
  • getPath()在用绝对路径表示的文件时相同,用相对路径表示的文件时不同
  • listFiles和list方法的调用,必须是实际存在的目录,否则返回null
  • listFiles和list 可以传入FilenameFilter的实现类,用于按照文件名称过滤文件
File shuiHu = new File("D:/水浒传");
// 0
System.out.println(shuiHu.length());
File liuBei = new File("D:/三国/刘备.jpg");
// 24591
System.out.println(liuBei.length());

File f = new File("D:/bbb.java");
 // D:bbb.java
System.out.println(f.getPath());

File f2 = new File("bbb.java");
// bbb.java
System.out.println(f2.getPath());

File sanGuo2 = new File("D:/三国2");
// 该目录不存在,返回null
String[] list = sanGuo2.list();

过滤文件的接口

@FunctionalInterface
public interface FilenameFilter {
  	// 参数为目录和指定过滤名称
    boolean accept(File dir, String name);
}

扩展(由读者自己实现)

读取目录下所有的文件以及目录,包括子目录下所有的文件及目录

IO流

上一章节学习了使用File类创建、查找、删除文件,但是无法读取、传输文件中的内容。

IO流主要是读取、传输、写入数据内容的。

I: input,O:output

这里的主体说的都是程序(即内存),从外部设备中读取数据到程序中 即为输入流,从程序中写出到外部程序中即为输出流

IO的分类

  • 本地IO和网络IO

本地IO主要是操作本地文件,例如在windows上复制粘贴操作文件,都可以使用java的io来操作

网络IO主要是通过网络发送数据,或者通过网络上传、下载文件,我们每天上网无时无刻不在体验着IO的传输

  • 按流向分,输入流和输出流
  • 按数据类型分: 字节流和字符流
  • 按功能分:节点流和处理流
    • 程序直接操作目标设备的类称为节点流
    • 对节点流进行装饰,功能、性能进行增强,称为处理流

IO流主要的入口是数据源,下面列举常见的源设备和目的设备

源设备

  1. 硬盘(文件)
  2. 内存(字节数组、字符串等)
  3. 网络(Socket)
  4. 键盘(System.in)

目的设备

  1. 硬盘(文件)
  2. 内存(字节数组、字符串等)
  3. 网络(Socket)
  4. 控制台(System.out)

本文先探讨本地IO的字节流和字符流,先列举字节流和字符流的公共方法

方法名

方法说明

void close() throws IOException

流操作完毕后,必须释放系统资源,调用close方法,一般放在finally块中保证一定被执行!

注意:

  • 程序中打开的IO资源不属于内存资源,垃圾回收机制无法回收该资源,需要显式的关闭文件资源
  • 下面的代码示例中就不显示的调用close方法,也不会处理IOException,只是为了代码的简洁,方便阅读

字节流

一切皆为字节

一切文件数据(文本、图片、视频等)在存储时,都是以二进制的形式保存,都可以通过使用字节流传输。

InputStream是字节输入流的顶层抽象

// Closeable有close()方法
public abstract class InputStream implements Closeable {}

核心方法如下

方法名

方法说明

int read() throws IOException;

每次读取一个字节的数据,提升为int类型,读取到文件末尾时返回 -1

int read(byte b[])throws IOException

每次读取到字节数组中,返回读取到的有效字节个数,读取到末尾时返回 -1(常用)

int read(byte b[], int off, int len)

每次读取到字节数组中,从偏移量off开始,长度为len,返回读取到的有效字节个数,读取到末尾时返回 -1

OutputStream是字节输出流的顶层抽象

// Flushable里面有flush()方法
public abstract class OutputStream implements Closeable, Flushable {}

核心方法如下

方法名

方法说明

void write(int b) throws IOException;

将int值写入到输出流中

void write(byte[] b) throws IOException;

将字节数组写入到输出流中

void write(byte b[], int off, int len) throws IOException

将字节数组从偏移量off开始,写入len个长度到输出流中

void flush() throws IOException

刷新输出流并强制缓冲的字节被写出

文件节点流

InputStream有很多的实现类,先介绍下文件节点流,即目标设备是文件,输入流和输出流对应的是

FileInputStream和FileOutputStream

FileInputStream主要从磁盘文件中读取数据,常用构造方法如下

public FileInputStream(File file) throws FileNotFoundException{}
public FileInputStream(String name) throws FileNotFoundException{};

当传入的文件不存在时,运行时会抛出FileNotFoundException异常

  1. read()方法读取
File file = new File("D:/三国/诸葛亮.txt");
FileInputStream fileInputStream = new FileInputStream(file);

// 核心代码
int b;
while ((b = fileInputStream.read()) != -1 ){
    System.out.print((char) b);
}

// 输出结果
abcde
  1. read(byte[])读取
File file = new File("D:/三国/诸葛亮.txt");
FileInputStream fileInputStream = new FileInputStream(file);

// 核心代码
byte[] data = new byte[2];
while (fileInputStream.read(data) != -1) {
    System.out.println(new String(data));
}

// 输出结果
ab
cd
ed

上述代码由于最后一次读取时,只读取一个字节 e ,数组中还是上次的数据cd,只替换了e,所以最后输出了ed

下面是使用FileInputStream读取的正确姿势

File file = new File("D:/三国/诸葛亮.txt");
FileInputStream fileInputStream = new FileInputStream(file);

// 核心代码
byte[] data = new byte[2];
int len;
while ((len = fileInputStream.read(data)) != -1) {
    // len 为每次读取的有效的字节个数
    System.out.println(new String(data, 0, len));
}

// 输出结果
ab
cd
e

注意:使用数组读取,每次读取多个字节,减少了系统间的IO操作次数,从而提高了效率,建议使用

源码解析

public int read() throws IOException {
    return read0();
}
private native int read0() throws IOException;

public int read(byte b[]) throws IOException {
   return readBytes(b, 0, b.length);
}
private native int readBytes(byte b[], int off, int len) throws IOException;

上面列了read()和read(byte[])的源码,可见都是调用native的方法,涉及底层的系统调用。

  • 如果用read()读取文件,每读取一个字节就要访问一次硬盘,这种效率较低。
  • 如果用read(byte[])读取文件,一次读取多个字节,当文件很大时,也会频繁访问硬盘。如果一次读取超多字节,效率也不会高。

FileOutputStream主要是向磁盘文件中写出数据,常用构造方法如下

构造方法名

方法说明

FileOutputStream(File file) throws FileNotFoundException

使用一个File对象来构建一个FileOutputStream

FileInputStream(String name) throws FileNotFoundException

使用一个文件名来构建一个FileOutputStream

FileOutputStream(File file, boolean append) throws FileNotFoundException

append传true时,会对文件进行追加

FileOutputStream(String name, boolean append) throws FileNotFoundException

append传true时,会对文件进行追加

注意:

  • 上述构造方法执行后,如果file不存在,会自动创建该文件
  • 如果file存在,append没有传或者传了false,会清空文件的数据
  • 如果file存在,append传了true,不会清空文件的数据
File file = new File("D:/三国/赵云.txt");
FileOutputStream fos = new FileOutputStream(file);
FileOutputStream fos1 = new FileOutputStream("D:/三国/司马懿.txt");
// 上述代码中执行完后,赵云.txt和司马懿.txt都会自动创建出来

向文件写数据

FileOutputStream fos = new FileOutputStream("D:/三国/司马懿.txt");
fos.write(96);
fos.write(97);
fos.write(98);
// 文件内容为 abc

FileOutputStream fos = new FileOutputStream("D:/三国/赵云.txt");
fos.write("三国赵云".getBytes());
// 文件内容为 三国赵云

上述代码每执行一次,文件里的内容就会被覆盖,有时候这不是我们想要的场景,我们一般是想追加文件

FileOutputStream fos = new FileOutputStream("D:/三国/赵云.txt", true);
fos.write("有万夫不当之勇".getBytes());
fos.close();
// 文件内容为 三国赵云有万夫不当之勇

应用场景

开发中涉及文件的上传、下载、传输都是用的这个节点流,会结合装饰后的处理流一起使用,在缓冲流部分有介绍。

扩展(由读者自己实现)

利用文件节点流实现文件的复制	

内存节点流

ByteArrayInputStream是从内存的字节数组中读取数据

public ByteArrayInputStream(byte buf[]) {}

注意:不需要close数据源和抛出IOException,因为不涉及底层的系统调用

ByteArrayOutputStream是向内存字节数组中写数据,内部维护了一个数组

public ByteArrayOutputStream() {
  	// 内部维护了一个可变的字节数组
  	// protected byte buf[];
    this(32);
}

内存节点流代码示例

ByteArrayInputStream bis = new ByteArrayInputStream("data".getBytes());
ByteArrayOutputStream bos = new ByteArrayOutputStream();

int len = 0;
while ((len = bis.read()) != -1){
    bos.write(len);
}
// 输出data
System.out.println(new String(bos.toByteArray()));

应用场景

  1. 内存操作流一般在一些生成临时信息时会被使用,如果临时信息保存着文件中,代码执行完还要删除文件比较麻烦
  2. 结合对象流,可以实现对象和字节数组的互转

字符流

字符流封装了更加适合操作文本字符的方法

Reader用于读取文本字符

public abstract class Reader implements Readable, Closeable {}

核心方法

方法名

方法说明

int read() throws IOException

从输入流中读取一个字符,读到文件末尾时返回-1

int read(char cbuf[]) throws IOException

从输入流中读取字符到char数组中

Writer用于写出文本字符

public abstract class Writer implements Appendable, Closeable, Flushable {}

核心方法

方法名

方法说明

void write(int c) throws IOException

写入单个字符到输出流中

void write(char[] cbuf) throws IOException

写入字符数组到输出流中

void write(char[] cbuf, int off, int len) throws IOException

写入字符数组的一部分,偏移量off开始,长度为len到输出流中

void write(String str) throws IOException

直接写入字符串到输出流中(常用)

void write(String str, int off, int len) throws IOException

写入字符串的一部分,偏移量off开始,长度为len

Writer append(char c) throws IOException

追加字符到输出流中

文件节点流

字符流操作纯文本字符的文件是最合适的,主要有FileReader和FileWriter

FileReader主要是向磁盘文件中写出数据,常用构造方法如下

public FileReader(String fileName) throws FileNotFoundException{}
public FileReader(File file) throws FileNotFoundException {}

注意: 当读取的文件不存在时,会抛出FileNotFoundException,这点和FileInputStream一致

  1. read()循环读取文件
FileReader fileReader = new FileReader("D:/三国/赵云.txt");
int b;
while ((b = fileReader.read()) != -1) {
    System.out.println((char) b);
}
  1. read(char[]) 读取文件
FileReader fileReader = new FileReader("D:/三国/赵云.txt");
int len;
char[] data = new char[2];
while ((len = fileReader.read(data)) != -1) {
    System.out.println(new String(data, 0, len));
}
// 两个字符两个字符依次读取

FileWriter构造方法如下,和FileOutStream构造方法类似,和FileOutputStream类似。

public FileWriter(String fileName) throws IOException {}
public FileWriter(String fileName, boolean append) throws IOException {}
public FileWriter(File file) throws IOException{}
public FileWriter(File file, boolean append) throws IOException {}

常用的写数据进文件的方法

FileWriter fileWriter = new FileWriter("D:/三国/孙权.txt");
fileWriter.write(97); 
fileWriter.write('b'); 
fileWriter.write('C'); 
fileWriter.write("权"); 
fileWriter.append("力");

注意:

  • 如果不执行close()或者flush()方法,数据只是保存到缓冲区,不会保存到文件。这点和与FileOutputStream不同,原因见 字节流和字符流的共同点章节

应用场景

纯文本文件的io操作,配合处理流一起实现。

内存节点流

字符流也有对应的内存节点流,常用的有StringWriter和CharArrayWriter

StringWriter是向内部的StringBuffer对象写数据。

// 定义
public class StringWriter extends Writer {

    private StringBuffer buf;

    public StringWriter() {
        buf = new StringBuffer();
        lock = buf;
    }
}

// 应用
StringWriter sw = new StringWriter();
sw.write("hello");

StringBuffer buffer = sw.getBuffer();
// 输出hello
System.out.println(buffer.toString());

CharArrayWriter是向内部的char数组写数据

// 定义
public class CharArrayWriter extends Writer {
    protected char buf[];
}

// 应用 
CharArrayWriter caw = new CharArrayWriter();
caw.write("hello");
char[] chars = caw.toCharArray();
for (char c : chars) {
	// 输出了h e l l o
	System.out.println(c);
}

四种常用节点流的使用总结

字节流和字符流的共同点

注意到,OutputStream、Reader、Writer都实现了Flushable接口,Flushable接口有flush()方法

flush():强制刷新缓冲区的数据到目的地,刷新后流对象还可以继续使用

close(): 强制刷新缓冲区后关闭资源,关闭后流对象不可以继续使用

缓冲区:可以理解为内存区域,程序频繁操作资源(如文件)时,性能较低,因为读写内存较快,利用内存缓冲一部分数据,不要频繁的访问系统资源,是提高效率的一种方式

具体的流只要内部有维护了缓冲区,必须要close()或者flush(),不然不会真正的输出到文件中

处理流

上面的章节介绍了字节流和字符流的常用节点流,但是真正开发中都是使用更为强大的处理流

处理流是对节点流在功能上、性能上的增强

字节流的处理流的基类是FilterInputStreamFilterOutputStream

缓冲流(重点)

前面说了节点流,都是直接使用操作系统底层方法读取硬盘中的数据,缓冲流是处理流的一种实现,增强了节点流的性能,为了提高效率,缓冲流类在初始化对象的时候,内部有一个缓冲数组,一次性从底层流中读取数据到数组中,程序中执行read()或者read(byte[])的时候,就直接从内存数组中读取数据。

分类

字节缓冲流:BufferedInputStream , BufferedOutputStream 
字符缓冲流: BufferedReader , BufferedWriter 

字节缓冲流

可见构造方法传入的是节点流,是对节点流的装饰

// 内部默认8192 =8*1024 即8M的缓冲区
public BufferedInputStream(InputStream in) {
  	// 8192    
  	// 内部维护了下面这样的字节数组
    // protected volatile byte buf[];
    this(in, DEFAULT_BUFFER_SIZE);
}
public BufferedOutputStream(OutputStream out) {
        this(out, 8192);
}

这里使用复制一部1G的电影来感受缓冲流的强大

  1. 使用基本的流读取数据(一次传输一个字节)
long start = System.currentTimeMillis();
FileInputStream fis = new FileInputStream("D:/三国/视频.mp4");
FileOutputStream fos = new FileOutputStream("D:/三国/拷贝.mp4");

int data;
while ((data = fis.read()) != -1) {
    fos.write(data);
}
log.info("拷贝电影耗时:{}ms", System.currentTimeMillis() - start);
// 五分钟还没拷好,关闭程序了...
  1. 使用基本的流读取数据(一次传输一个8M的字节数组)
long start = System.currentTimeMillis();
FileInputStream fis = new FileInputStream("D:/三国/视频.mp4");
FileOutputStream fos = new FileOutputStream("D:/三国/拷贝.mp4");

int len;
byte[] data = new byte[1024 * 1024 * 1024];
while ((len = fis.read(data)) != -1) {
    fos.write(data, 0, len);
}
log.info("拷贝电影耗时:{}ms", System.currentTimeMillis() - start);
// 拷贝电影耗时:4651ms
  1. 使用缓冲流读取数据(一次传输一个字节)
long start = System.currentTimeMillis();
BufferedInputStream fis = new BufferedInputStream(new FileInputStream("D:/三国/视频.mp4"));
BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream("D:/三国/拷贝.mp4"));

int data;
while ((data = fis.read()) != -1) {
    fos.write(data);
}

log.info("拷贝电影耗时:{}ms", System.currentTimeMillis() - start);
// 拷贝电影耗时:39033ms
  1. 使用缓冲流读取数据(一次传输一个8M的字节数组)(最常使用)
long start = System.currentTimeMillis();
BufferedInputStream fis = new BufferedInputStream(new FileInputStream("D:/三国/视频.mp4"));
BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream("D:/三国/拷贝.mp4"));

int len;
byte[] data = new byte[8 * 1024];
while ((len = fis.read(data)) != -1) {
    fos.write(data, 0, len);
}

log.info("拷贝电影耗时:{}ms", System.currentTimeMillis() - start);
// 拷贝电影耗时:1946ms

由上述四个例子可以得出结论,缓冲流读取数据比普通流读取数据快很多!

注意: 采用边读边写的方式,一次传输几兆的数据效率比较高,如果采用先把文件的数据都读入内存,在进行写出,这样读写的次数是较小,但是占用太大的内存空间,一次读太大的数据也严重影响效率!

字符缓冲流

对字符节点流的装饰,下面是字符缓冲流的构造方法

public BufferedReader(Reader in) {
	// private static int defaultCharBufferSize = 8192;
	// 内部维护了一个字符数组
    // private char cb[];
    this(in, defaultCharBufferSize);
}

public BufferedWriter(Writer out) {
        this(out, defaultCharBufferSize);
}

字符缓冲流的特有方法

方法名

方法说明

BufferedReader

String readLine() throws IOException

一行行读取,读取到最后一行返回null

BufferedWriter

void newLine() throws IOException

写一个换行符到文件中,实现换行

// 创建流对象
BufferedReader br = new BufferedReader(new FileReader("D:/三国/赵云.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("D:/三国/赵子龙.txt"));
String line = null;
while ((line = br.readLine())!=null) {
  System.out.println(line);
  bw.write(line);
  bw.newLine();
}
// 结果
我乃常山赵子龙
于万军从中,取上将首级

缓冲流的正确姿势

缓冲流是IO流中最重要的知识点,下面通过代码实现正确用IO流的姿势

BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
    bis = new BufferedInputStream(new FileInputStream(new File("D:/三国/视频.mp4")));
    bos = new BufferedOutputStream(new FileOutputStream(new File("D:/三国/拷贝.mp4")));
    int len;
    // 一次传输8M的文件,实际测试这里传输的大小并不影响传输的速度
    byte[] data = new byte[8 * 1024];
    while ((len = bis.read(data)) != -1) {
        bos.write(data, 0, len);
    }
} catch (IOException e) {
    log.error("error", e);
} finally {
  	// finally块中关闭流,确保资源一定被关闭
    if (bis != null) {
        try {
            bis.close();
        } catch (IOException e) {
            log.error("error", e);
        }
    }
    if (bos != null) {
        try {
            bos.close();
        } catch (IOException e) {
            log.error("error", e);
        }
    }
}

转换流

字符编码与字符集

字符编码

计算机存储的数据都是二进制的,而我们在电脑上看到的数字、英文、汉字等都是二进制转换的结果
  • 将字符转换成二进制,为编码
  • 将二进制转换为字符,为解码

字符编码 就是 自然语言和二进制的对应规则

字符集

就是一个编码表,常见的字符集有ASCII字符集、GBK字符集、Unicode字符集等,具体各个编码的介绍在这里就不介绍了。

IDEA中,使用 FileReader 读取项目中的文本文件。IDEA可以设置为GBK 编码,当读取Windows系统中创建的默认的UTF8文本文件时,就会出现乱码 。

如下例

idea的字符集设置 默认是UTF-8,这里修改为GBK

运行的代码及结果

FileReader fileReader = new FileReader("D:/sanguo/utf8.txt");
int read;
while ((read = fileReader.read()) != -1) {
    System.out.print((char)read);
}
// 浣犲ソ

InputStreamReader

Reader的子类,读取字节,并使用指定的字符集将其解码为字符。字符集可以自己指定,也可以使用平台的默认字符集。

构造方法如下

// 使用平台默认字符集
public InputStreamReader(InputStream in) {}
// 指定字符集
public InputStreamReader(InputStream in, String charsetName)
        throws UnsupportedEncodingException{}

读取文件的“你好",文件默认的字符集是UTF8

// 创建流对象,默认UTF8编码
InputStreamReader isr = new InputStreamReader(new FileInputStream("D:/三国/utf8.txt"));
// 创建流对象,指定GBK编码
InputStreamReader isr2 = new InputStreamReader(new FileInputStream("D:/三国/utf8.txt"), "GBK");

int read;
while ((read = isr.read()) != -1) {
    System.out.println((char) read);
}

while ((read = isr2.read()) != -1) {
    System.out.println((char) read);
}

// 输出结果
你好
浣犲ソ

OutputStreamWriter

Writer的子类,使用指定的字符集将字符编码为字节。字符集可以自己指定,也可以使用平台的默认字符集。

构造方法如下

// 使用平台默认字符集
public OutputStreamWriter(OutputStream out) {}
// 使用平台默认字符集
public OutputStreamWriter(OutputStream out, String charsetName)
throws UnsupportedEncodingException{}

如下面的代码,将你好写入文件。写入后两个文件的字符集不一样,文件大小也不同

// 创建流对象,默认UTF8编码
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("D:/三国/黄忠.txt"));
osw.write("你好"); // 保存为6个字节

// 创建流对象,指定GBK编码
OutputStreamWriter osw2 = new OutputStreamWriter(new FileOutputStream("D:/三国/马超.txt"),"GBK");
osw2.write("你好");// 保存为4个字节

对象流

序列化

jdk提供了对象序列化的方式,该序列化机制将对象转为二进制流,二进制流主要包括对象的数据、对象的类型、对象的属性。可以将java对象转为二进制流写入文件中。文件会持久保存了对象的信息。

同理,从文件中读出对象的信息为反序列化的过程

对象想序列化,满足的条件:

  1. 该类必须实现 java.io.Serializable 接口, Serializable 是一个标记接口(没有任何抽象方法),不实现此接口的类将不会使任何状态序列化或反序列化,会抛出 NotSerializableException 。
  2. 该类的所有属性必须是可序列化的,如果有一个属性不需要可序列化的,则该属性使用transient 关键字修饰

ObjectOutputStream

该类实现将对象序列化后写出到外部设备,如硬盘文件

public ObjectOutputStream(OutputStream out) throws IOException{}

常用方法

方法名

方法说明

void writeObject(Object obj) throws IOException

将指定的对象写出

如下代码,将User对象写入文件中

public class User implements Serializable {
    private static final long serialVersionUID = 8289102797441171947L;

    private String name;
    private Integer age;
}
// 下面是将对象输出到文件的核心代码
User user = new User("马超",20);
// 创建序列化流对象
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:/三国/马超.txt"));
// 写出对象
out.writeObject(user);

注意:

  1. 实现了Serializable的实体一定要加一个serialVersionUID变量,这也是习惯问题,idea可以设置一下。
  2. serialVersionUID生成后不要改变,避免反序列化失败,改变后会抛出InvalidClassException异常

生成的文件内容如下

ObjectInputStream

该类将ObjectOutputStream写出的对象反序列化成java对象

public ObjectInputStream(InputStream in) throws IOException 

常用方法

方法名

方法说明

Object readObject() throws IOException, ClassNotFoundException

读取对象

ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:/三国/马超.txt"));
// 强转为user
User user = (User) in.readObject();
System.out.println(user);
// 输出内容
User(name=马超, age=20)

对象和字节数组的转换

利用对象流和字节数组流结合 ,可以实现java对象和byte[]之间的互转

// 将对象转为byte[]
public static <T> byte[] t1(T t) {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(bos);
    oos.writeObject(t);
    return bos.toByteArray();
}
// 将byte[]转为对象
public static <T> T t2(byte[] data) throws IOException, ClassNotFoundException {
    ByteArrayInputStream bos = new ByteArrayInputStream(data);
    ObjectInputStream oos = new ObjectInputStream(bos);
    return (T) oos.readObject();
}

管道流(了解)

管道流主要用于两个线程间的通信,即一个线程通过管道流给另一个线程发数据

注意:线程的通信一般使用wait()/notify(),使用流也可以达到通信的效果,并且可以传递数据

使用的类是如下

  • PipedInputStream和PipedOutStream
  • PipedReader和PipedWriter

这里使用字节流为例

class Sender implements Runnable {
    private PipedOutputStream pos;
    private String msg;

    public Sender(String msg) {
        this.pos = new PipedOutputStream();
        this.msg = msg;
    }

    @Override
    public void run() {
       pos.write(msg.getBytes());
    }

    public PipedOutputStream getPos() {
        return pos;
    }
}

class Receiver implements Runnable {
    private PipedInputStream pis;

    public Receiver() {
        this.pis = new PipedInputStream();
    }


    @Override
    public void run() {
         byte[] data = new byte[1024];
            int len;
            while ((len = pis.read(data)) != -1) {
                System.out.println(new String(data, 0, len));
            }
    }
}
    
Sender sender = new Sender("hello");
Receiver receiver = new Receiver();
receiver.getPis().connect(sender.getPos());
new Thread(sender).start();
new Thread(receiver).start();
// 控制台输出  hello

输入与输出流(了解)

System.in和System.out代表了系统标准的输入、输出设备

默认输入设备是键盘,默认输出设备是控制台

可以使用System类的setIn,setOut方法对默认设备进行改变

我们开发中经常使用的输出到控制台上的内容的方法。

System.out.println("a");
System.out.print("b");
class System{
    public final static InputStream in = null;
    public final static PrintStream out = null;
}
public PrintStream(String fileName) throws FileNotFoundException{}

数据流(了解)

主要方便读取Java基本类型以及String的数据,有DataInputStream 和 DataOutputStream两个实现类

DataOutputStream dos = new DataOutputStream(new FileOutputStream("D:/三国/周瑜.txt"));
dos.writeUTF("周瑜");
dos.writeBoolean(false);
dos.writeLong(1234567890L);

DataInputStream dis = new DataInputStream(new FileInputStream("D:/三国/周瑜.txt"));
String s = dis.readUTF();
System.out.println(s);
boolean b = dis.readBoolean();
System.out.println(b);
// 输出
周瑜
false

IO流总结

以上各个章节详细介绍了各个流,可见流的种类比较多,记忆确实增加了困难。但是可以通过思维导图的方式整理出来,方便记忆。

字节流的导图

字符流的导图

按照功能划分

输入、输出对应关系

结语

短期的加更计划

  1. NIO
  2. tomcat系列源码解析

文章篇幅较长,给看到这里的小伙伴点个大大的赞!由于作者水平有限,文章中难免会有错误之处,欢迎小伙伴们反馈指正。

如果觉得文章对你有帮助,麻烦 点赞、评论、转发、在看 、关注 走起

你的支持是我加更最大的动力!!!