您的位置 首页 java

万字长文+思维导图帮你梳理Java IO流,还学不会你来打我

前言

在上一篇的文章获取不错的浏览量后,继续加更的念头一直徘徊在心中,本来是想花段时间深入学习tomcat的,可是tomcat的源码中就有至关重要的N IO ,于是得先整一下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,当文件存在时,返回falseboolean 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 len gth()返回由此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类型,读取到文件末尾时返回 -1int 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对象来构建一个FileOutputStreamFileInputStream(String name) throws FileNotFoundException使用一个文件名来构建一个FileOutputStreamFileOutputStream(File file, boolean append) throws FileNotFoundExceptionappend传true时,会对文件进行追加FileOutputStream(String name, boolean append) throws FileNotFoundExceptionappend传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从输入流中读取一个字符,读到文件末尾时返回-1int 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开始,长度为lenWriter 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(),不然不会真正的输出到文件中

处理流

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

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

字节流的处理流的基类是 FilterInputStream FilterOutputStream

缓冲流(重点)

前面说了节点流,都是直接使用操作系统底层方法读取硬盘中的数据,缓冲流是处理流的一种实现,增强了节点流的性能,为了提高效率,缓冲流类在初始化对象的时候,内部有一个 缓冲数组 ,一次性从底层流中读取数据到数组中,程序中执行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);
}
  

字符缓冲流的特有方法

类方法名方法说明BufferedReaderString readLine() throws IOException一行行读取,读取到最后一行返回nullBufferedWritervoid 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系列源码解析

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

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

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

文章来源:智云一二三科技

文章标题:万字长文+思维导图帮你梳理Java IO流,还学不会你来打我

文章地址:https://www.zhihuclub.com/176055.shtml

关于作者: 智云科技

热门文章

网站地图