java.io是Java编程语言中处理输入输出(I/O)操作的核心类库,定义了数据流模型与多种接口,支持文件操作、网络通信及对象序列化等功能。其通过流(Stream)机制实现数据传输,涵盖字节流、字符流与缓冲处理等设计,为Java标准库的重要组成部分。

该库将数据源抽象为输入流和输出流,按传输方向分为输入型与输出型,按单位分为字节流(8位字节处理)与字符流(16位字符处理)。功能上包含直接操作设备的节点流和增强功能的过滤流,核心类包括InputStream、OutputStream、Reader、Writer四个抽象类及其子类,形成分层结构的I/O处理体系。

参考原文:全网最完整Java学习笔记

基本介绍

什么是I/O流

I/O流(Input/Output Stream),即输入(Input)和输出(Output)流,是用于处理输入和输出操作的数据流。在Java中,一般用IO流写一个文件输出到操作系统中,或者从操作系统中输入一个文件进行读取。

IO流分类:

按数据流向

  • 输入流:读数据。
  • 输出流:写数据。

按数据类型

  • 字节流:它处理单元为1个字节(byte),操作字节和字节数组,存储的是二进制文件。
    • 使用场景:在Java中,一般用IO流写一个文件输出到操作系统中,或者从操作系统中输入一个文件进行读取。如果是音频文件、图片、歌曲,就用字节流(1byte = 8位)。
  • 字符流:它处理的单元为2个字节的Unicode字符,分别操作字符、字符数组或字符串,字符流是由Java虚拟机将字节转化为2个字节的Unicode字符为单位的字符而成的。
    • 使用场景:如果是关系到中文(文本)的,用字符流(1Unicode = 2字节 = 16位);

字节流

输入字节流 (InputStream)

  • **FileInputStream**:基本的文件输入流,用于读取字节文件(如图片、视频)中的数据。
  • **FilterInputStream**:过滤输入流,是所有过滤输入流的父类。用于对数据进行解密、校验、转换、过滤、缓存。因为它是抽象类,所以我们一般用它的子类:
    • **BufferedInputStream**:相比于FileInputStream,它的读取性能会高很多。因为它多了一个缓冲区数组(在内存中),在第一次调用read()读取数据时,他会将数据尽可能多的填满缓冲区,这样再次read()时会优先从缓冲区中读,而不用直接在磁盘中读。我们知道内存读写性能是远高于磁盘的,所以它更快。默认缓冲区大小是8192字节,即8KB,超过这个容量就必须在磁盘读取了。
    • **DataInputStream**:与相比于FileInputStream,它的读取性能会高很多。因为它直接提供了读取Java中基本数据类型的方法,如 readInt()、readDouble()、readUTF() 等,而不需要额外的解析步骤。
    • **SocketInputStream**:网络输入流。

输出字节流 (OutputStream)

  • **FileOutputStream**:基本的文件输出流,用于将数据写入字节文件(如图片、视频)。
  • **FilterOutputStream**:过滤输出流,是所有过滤输出流的父类,提供了基本的输出流功能。用于压缩数据、加密数据等。因为它是抽象类,所以我们一般用它的子类:
    • **BufferedOutputStream**:为输出流提供缓冲功能,提高写入效率。每次写入数据时,先写进缓冲区,缓冲区满了会自动刷新写入到磁盘中,也可以手动调用flash()方法刷新。
    • **DataOutputStream**:性能高,因为它直接提供了写入Java中基本数据类型的方法,如 wirteInt(),而不需要额外的转换步骤。
    • SocketOutputStream

字符流

字符流是以字符为单位处理数据的流,分为输入字符流和输出字符流。

输入字符流 (Reader)

  • **InputStreamReader**:输入流读取类,用于将字节流转换为字符流再读取字符数据。使用系统默认字符编码。
  • **FileReader**:文件读取类,用于直接从文件中读取字符数据。可以指定字符编码来解码字节流。
  • **BufferedReader**:为字符输入流提供缓冲功能,提高读取效率。

输出字符流 (Writer)

  • **OutputStreamWriter**:将字符流转换为字节流。
  • **FileWriter**:用于将字符数据写入文件。
  • **BufferedWriter**:为字符输出流提供缓冲功能,提高写入效率。

File类

File类是用于表示文件和目录路径名的类。

注意:File类封装的不是真正的文件,只是路径。所以File类可以对文件进行创建、删除、重命名、修改时间、文件大小等操作,但不能对文件里的数据进行读取或者写入,它一般作为输入输出流的构造参数,指明文件的路径。

创建文件示例

1
2
3
4
5
6
7
public class Demo {
public static void main(String[] args) throws IOException {
File f = new File("D:\\1\\2.txt");
//只创建已存在文件夹“D://1”下的2.txt。若“D://1”不存在,报错IOException: 系统找不到指定的路径。
System.out.println(f.createNewFile()); //true,当目标位置已存在同名文件则创建失败输出false
}
}

文件默认路径:

在当前包内IO流,使用File类创建文件时

1
new File("1.txt");

默认存的位置是在项目文件夹下,而不是包文件夹下。

关于路径斜杠和反斜杠:

路径斜杠可以是//,/,\,不能是\,因为它是转义符。

正斜杠“/”和“//”都可以,反斜杠必须“\”,因为“\”会ASCII转义,“\”在字符串里才是“\”。

1
2
3
4
5
6
7
8
File file = new File("D://1//1.txt");
File file1 = new File("D:/1/2.txt");
File file2 = new File("D:\\1\\3.txt");
// File file3 = new File("D:\1\4.txt"); // 这样会报错,单个反斜杠会进行转义
System.out.println(file.createNewFile()); // true
System.out.println(file1.createNewFile()); // true
System.out.println(file2.createNewFile()); // true
// System.out.println(file3.createNewFile()); // false

查看当前路径:

1
System.out.println(System.getProperty("user.dir"));    //D:\workspace\java\test

常用方法

  • **boolean createNewFile()**: 当且仅当具有该名称的文件尚不存在时,原子地(保证线程安全)创建一个由该抽象路径名命名的新空文件。抽象路径名指的是构造 File 对象时传递的路径名字符串。创建File对象时,构造参数的路径并不一定真实存在,可能是虚拟的、等待通过createNewFile等方法进行创建。
  • boolean mkdir() :创建目录。如果父目录不存在,则不会创建目录,并返回 false。
  • **boolean mkdirs()**: 创建目录。如果父目录不存在,它会一并创建。
  • **boolean delete()**: 删除文件或目录。成功删除文件或目录时返回 true,否则返回 false。注意如果要删除目录,该目录必须为空,否则会返回false。
  • **boolean isDirectory()**: 测试File是否为目录。
  • **boolean isFile()**: 测试File是否为文件。
  • **boolean exists()**: 测试File是否真实存在。
  • **String getAbsolutePath()**: 返回绝对路径名字符串。
  • **String getPath()**: 将抽象路径名。也就是构造 File 对象时传递的路径名字符串,可能是绝对路径,也可能是相对路径。
  • **String getName()**: 返回由此抽象路径名表示的文件或目录的名称。
  • **String[] list()**: 返回对应目录下所有文件的文件名数组。
  • **File[] listFiles()**: 返回对应目录下所有文件的File文件数组。

举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void main(String[] args) {
File f = new File("1\\2\\3");
// mkdirs()如果父目录不存在,它会一并创建。
System.out.println(f.mkdirs());
File f1 = new File("1\\2\\3\\4.txt");
try {
System.out.println(f1.createNewFile());
} catch (IOException e) {
System.out.println("创建失败" + e);
}
// listFiles()是路径下的所有文件组成的数组,若f文件会报错
File[] f2 = f.listFiles();
for (File file : f2) {
if (file.isFile()) {
System.out.println(file.getName() + "," + file.getPath());
}
}
// 删除
// 删除目录时要确保目录下没文件。
// f1.delete();
// f.delete();
}
// 输出结果
// true
// true
// 4.txt,1\2\3\4.txt

字节流的超类

字节输出流OutputStream

概念:

Java中的 InputStream 和 OutputStream 都是 io 包中面向字节操作的顶级抽象类。所有字节流类都是他们的子类。

image-20250722203841265

子类

  • 网络数据传输:SocketOutputStream
  • 文件操作FileOutputStream
  • 字节数据操作:DataOutputStream

常用api

  • void close() :关闭此输出流并释放与此流相关联的任何系统资源。
  • void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。
  • void write(byte[] b) :将 b.length 字节从指定的字节数组写入此输出流。
  • void write(byte[] b, int off, int len) : 从指定的字节数组写入 len 个字节,从偏移 off 开始输出到此输出流。
  • abstract void write(int b) :将指定的字节写入此输出流。

字节输入流InputStream

概念

Java中的 InputStream 和 OutputStream 都是 io 包中面向字节操作的顶级抽象类,关于java同步 io字节流的操作都是基于这两个的。

子类

  • 网络数据传输:SocketInputStream
  • 文件操作:FileInputStream
  • 字节数据操作:DataInputStream。与相比于FileInputStream,它的读取性能会高很多。因为它直接提供了读取Java中基本数据类型的方法
  • 带缓冲区的输入流:BufferedInputStream

常用api

  • **abstract int read()**:从输入流读取数据的下一个字节。
  • **int read(byte[] b)**:从输入流读取一些字节数,并将它们存储到缓冲区 b,返回值为长度 。
  • **int read(byte[] b, int off, int len)**:从输入流读取最多 len字节的数据到一个字节数组。

字节文件流

文件输出流FileOutputStream

基本的文件输出流,用于将数据写入字节文件(如图片、视频)。输出流是写数据,把数据输出到文件里。

构造方法

  • **FileOutputStream(File file)**:创建文件输出流以写入由指定的 File对象表示的文件。
  • **FileOutputStream(String name)**:创建文件输出流以指定的名称写入文件
  • **FileOutputStream(String name, boolean append)**:创建文件输出流以指定的名称写入文件。append默认是false,即彻底覆盖写入。append为true时是追加写入。
  • FileOutputStream(File file, boolean append): 使用文件对象创建文件输出流,并选择是否追加数据。

写操作方法

  • void write(int b): 将指定的字节写入输出流。
  • void write(byte[] b): 将字节数组 b 写入输出流。
  • void write(byte[] b, int off, int len): 将字节数组 b 中从偏移量 off 开始的 len 个字节写入输出流。

关闭方法

  • void close(): 关闭文件输出流并释放与此流有关的所有系统资源。

示例,将“abcde” 写入1.txt文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Test {
public static void main(String[] args) {
FileOutputStream fos=null;
try {
fos=new FileOutputStream("D://1.txt");
byte[] b ="abcde".getBytes();
fos.write(b);
fos.close();
}catch (IOException e){
// 实际开发不建议用e.printStackTrace()打日志,而是logger.error()
e.printStackTrace();
}finally {
// 判空,防止创建流的时候就失败,进入catch中
if(fos!=null){
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
// 可以看到D盘下多了个1.txt,内容是abcde

注意:写数据时的换行符,windows系统下是\r\n,linux的换行是\n,mac换行\r.

文件输入流 FileInputStream

基本的文件输入流,用于读取字节文件(如图片、视频)中的数据。

常用方法

  • **FileInputStream(File file)**:构造方法,创建一个 FileInputStream,它读取从指定File对象的文件。
  • **FileInputStream(String name)**:构造方法,创建一个 FileInputStream,它读取从指定文件路径名的文件
  • **int read()**:读取一个字节的数据。返回的int是读取字节的个数,如果到达流的末尾,则返回值 -1。
  • **int read(byte[] b)**:读取一定字节(0~b.length,具体长度取决于是否读到了末尾)的数据,将其存储在缓冲区数组 b 中。返回读取的字节数,如果没有可用的字节,则返回 -1。
  • **int read(byte[] b, int off, int len)**:最多读取 len 个字节的数据到一个字节数组。尝试读取最多 len 个字节的数据到缓冲区 b 中,从 off 位置开始。返回读取的字节数,如果没有可用的字节,则返回 -1。

代码示例:通过文件输入输出流复制图片

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
public static void main(String[] args) throws IOException {
FileInputStream fis=new FileInputStream("D://1.png");
FileOutputStream fos=new FileOutputStream("D://2.png");
byte []b=new byte[1024];
int len=fis.read(b);
while(len!=-1){
fos.write(b,0,len);len=fis.read(b);
}
}
}
// 结果就是 D盘多了一个2.png图片

扩展

使用maven后可以导入commons-io包,直接通过工具类复制。

使用IOUtils.copy(fis,os)进行流的复制(推荐)

(1)pom.xml添加依赖

1
2
3
4
5
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>

(2)调用工具类方法实现复制

1
2
3
//fis:输入流
//os:输出流
IOUtils.copy(fis,os);

字节缓冲流(推荐)

特点

  • 字节缓冲流仅提供缓冲区,真正读写数据还要靠基本字节流对象操作。

  • 字节缓冲流读写数据比基本字节流快很多,因为它有个存在内存中的字节数组,作为缓冲区,读写时先在缓冲区中读写,性能比直接在磁盘中读写快很多。

字节缓冲输出流BufferedOutputStream

**BufferedOutputStream**是一个带缓冲区的输出流,它跟FileOutputStream一样,都继承自OutputStream抽象类。

与普通的OutputStream相比,BufferedOutputStream在写入数据时使用了一个存在内存中的中间缓冲区。当向BufferedOutputStream写入数据时,数据首先会被写入到缓冲区,而不是直接写入到目标输出流(即最终的目标输出,比如文件、网络连接、控制台)。当缓冲区满了、手动刷新flash()、手动关闭close()时,BufferedOutputStream会将缓冲区中的数据一次性写入到目标输出流中。

这种缓冲机制可以减少实际的物理写操作,从而提高写操作的效率。因为磁盘IO是很慢的,而内存IO是很快的,相比于每次写入一个字节到目标输出流,将多个字节一次性写入会更加高效,磁盘IO次数有效减少。

构造方法

  • BufferedOutputStream(OutputStream out) :创建一个新的缓冲输出流,以将数据写入指定的底层输出流。
  • **BufferedOutputStream(OutputStream out, int size)**:创建一个新的缓冲输出流,以将数据写入指定的底层输出流,并具有指定的缓冲区大小。

写入方法

  • **void write(int b)**:将指定的字节写入此缓冲输出流。
  • **void write(byte[] b, int off, int len)**:将字节数组中的指定字节写入此缓冲输出流。

刷新方法

  • **void flush()**:刷新此缓冲输出流,将任何缓冲的输出字节写出到底层输出流。
    关闭方法

  • **void close()**:关闭此输出流并释放与此流相关联的任何系统资源。在关闭流之前,会刷新它。
    构造方法

  • **BufferedInputStream(InputStream in)**:创建一个新的缓冲输入流,以从指定的底层输入流读取数据。

  • **BufferedInputStream(InputStream in, int size)**:创建一个新的缓冲输入流,以从指定的底层输入流读取数据,并具有指定的缓冲区大小。

读取方法

  • **int read()**:从输入流中读取数据的下一个字节。
  • **int read(byte[] b, int off, int len)**:从输入流中将最多 len 个字节的数据读入一个字节数组。
  • **int read(byte[] b)**:从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。

跳过和可用

  • **long skip(long n)**:跳过和丢弃输入流中最多 n 个字节的数据。
  • **int available()**:返回可以不受阻塞地从输入流中读取的剩余字节数。阻塞(Blocking) 是指程序在等待某些操作完成(例如读取数据、写入数据、获取网络响应等)时停止执行其他操作,直到该操作完成。

标记和重置

  • **void mark(int readlimit)**:在输入流中标记当前位置。
  • **void reset()**:将流重新定位到上次调用 mark 方法时的位置。
  • **boolean markSupported()**:测试输入流是否支持 mark 和 reset 方法。

关闭

  • **void close()**:关闭输入流并释放与此流相关联的任何系统资源。

字节缓冲输入流BufferedInputStream

BufferedInputStream是字节缓冲输入流。

相比于FileInputStream,它的读取性能会高很多。因为它多了一个缓冲区数组(在内存中),在第一次调用read()读取数据时,他会将数据尽可能多的填满缓冲区,这样再次read()时会优先从缓冲区中读,而不用直接在磁盘中读。

内存读写性能是远高于磁盘的,所以它更快。默认缓冲区大小是8192字节,即8KB,超过这个容量就必须在磁盘读取了。

代码示例:使用缓冲流复制图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test {
public static void main(String[] args) throws IOException {
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D://1.png"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D://2.png"));
byte[] buffer = new byte[1024];
int len;
// 读取和写入字节数据
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
bis.close();
bos.close();
}
}
// 结果就是 D盘多了一个2.png图片

字符流

继承关系

字符流的继承关系:

image-20250722203910877

字符流的编码和解码

为什么要用字符流

因为字节流编码不支持汉字。 计算机底层存储数据都是通过二进制的字节文件存储的,所以我们看到的文件信息都是有多少字节、多少KB、多少MB,文本文件需要设置编码是UTF-8、GBK等类型。

GBK编码,一个汉字占用2个字节。
utf-8编码,一个汉字占用3个字节。

一个英文字符占一个字节,一个汉字占多个字节,因为字节流都是单字节读取,所以字节流在读中文的时候有可能会读到半个中文,造成乱码,所以这就引入了字符流。

字符流对字节文件进行编码和解码

  • 编码:字符输出流将字符编码为字节写入字节文件
  • 解码:字符输入流从字节文件解码出字符。

这样可以保证写入文件时,写入的格式是字节文件,读取文件时,将字节文件转为字符文件。

字符串的编码解码方法: 字符串的编码和解码是指在不同字符集之间转换字符数据的过程。

  • 编码:将字符串转换为字节数组
  • 解码:将字节数组转换为字符串。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) throws IOException {
String str = "你好";
// 编码
byte[] bytes = str.getBytes("UTF-8");
// 解码
String bytesToString = new String(bytes, "UTF-8");
System.out.println(bytesToString);
// 如果编码设置的不对,设为GBK就会乱码:浣犲ソ
String bytesToString2 = new String(bytes, "GBK");
System.out.println(bytesToString2);
}
// 输出结果
// 你好
// 浣犲ソ

字符输入输出流

字符输出流OutputStreamWriter

OutputStreamWriter:将字符编码为字节,再写入字节文件。

OutputStreamWriter 是 Java 中一个桥接器类,用于将字符流转换为字节流。它使用指定的字符编码将字符写入字节流。它可以用于写入文件、网络流等。

桥接器类:指充当中介,用于连接或转换两种不兼容类型的类

构造方法:构造参数都有OutputStream,因为要讲字符流编码成字节流OutputStream

  • OutputStreamWriter(OutputStream out): 创建一个使用默认字符编码的 OutputStreamWriter。
  • OutputStreamWriter(OutputStream out, String charsetName): 创建一个使用指定字符编码的 OutputStreamWriter。
  • OutputStreamWriter(OutputStream out, Charset cs): 创建一个使用指定 Charset 的 OutputStreamWriter。
  • OutputStreamWriter(OutputStream out, CharsetEncoder enc): 创建一个使用指定 CharsetEncoder 的 OutputStreamWriter。

写入方法

  • void write(int c): 写入单个字符。
  • void write(char[] cbuf, int off, int len): 写入字符数组的一部分。
  • void write(char[] cbuf): 写入字符数组。
  • void write(String str, int off, int len): 写入字符串的一部分。
  • void write(String str): 写入整个字符串。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) throws IOException {
// 创建文件输出流
FileOutputStream fos = new FileOutputStream("D://output.txt");
// 创建使用 UTF-8 编码的 OutputStreamWriter
OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
// 写入字符串
osw.write("Hello, World!");
osw.write("\n");
osw.write("这是一个测试。");
// 刷新和关闭流
osw.flush();
osw.close();
}
// 文件中会出现以下两行
// Hello, World!
// 这是一个测试。

字符输入流InputStreamReader

InputStreamReader 是 Java 中一个桥接器类,用于从字节文件解码出字符。因为计算机底层存储的数据都是二进制的字节码文件,所以我们要读取文本文件,就需要将其根据编码解析成字符流,才能读取文件中的内容。

构造方法:构造参数都有InputStream,因为InputStreamReader作用是将字节文件转为字符

  • InputStreamReader(InputStream in): 使用默认字符集将给定的输入流 in 转换为字符流。
  • InputStreamReader(InputStream in, Charset cs): 使用指定的字符集 cs 将给定的输入流 in 转换为字符流。

读取字符

  • int read(): 读取单个字符并返回其 Unicode 编码,如果到达流的末尾,则返回 -1。
  • int read(char[] cbuf, int offset, int length): 字符读入数组 cbuf,从 offset 位置开始存放,最多读取 length 个字符。

其他方法

  • void mark(int readAheadLimit): 标记流的当前位置,最多可以在流中读取 readAheadLimit 个字符。
  • void reset(): 将流重置到最后一次标记的位置。
  • boolean markSupported(): 测试此输入流是否支持标记功能。
  • void close(): 关闭流并释放与之关联的所有系统资源。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) throws IOException {
// 创建文件输入流
FileInputStream fis = new FileInputStream("D://output.txt");
// 创建使用 UTF-8 编码的 InputStreamReader
InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
// 读取文件内容并输出
char[] buffer = new char[1024];
int length;
while ((length = isr.read(buffer)) != -1) {
System.out.print(new String(buffer, 0, length));
}
// 关闭流
isr.close();
}
// 输出结果
// Hello, World!
// 这是一个测试。
// 进程已结束,退出代码为 0

FileWriter和FileReader:简化字符流

FileWriter是OutputStreamWriter的子类,FileReader是InputStreamReader的子类。这两个类的作用是简化字符输入输出流的构造方法。

看看FileReader的源码,可以看到它继承了InputStreamReader类,并且只有三个构造方法,这几个构造方法封装了super(new FileInputStream(fileName或者file));,从而可以让我们直接将文件名或者File对象作为构造参数,而不需要再麻烦的创建一个FileInputStream对象,再将它作为构造参数创建InputStreamReader对象了。

1
2
3
4
5
6
7
8
9
10
11
12
package java.io;
public class FileReader extends InputStreamReader {
public FileReader(String fileName) throws FileNotFoundException {
super(new FileInputStream(fileName));
}
public FileReader(File file) throws FileNotFoundException {
super(new FileInputStream(file));
}
public FileReader(FileDescriptor fd) {
super(new FileInputStream(fd));
}
}

下面两段代码效果是一样的,都是创建字符输入流,明显FileReader更方便:

InputStreamReader创建字符输入流方式

1
2
3
4
// 创建文件输入流
FileInputStream fis = new FileInputStream("D://output.txt");
// 创建使用 UTF-8 编码的 InputStreamReader
InputStreamReader isr = new InputStreamReader(fis, "UTF-8");

FileReader创建字符输入流方式:

1
FileReader reader = new FileReader("output.txt")

除了构造方法外,其他方法和他们的父类一致,这里不再赘述。

FileReader构造方法

  • FileReader(String fileName): 创建一个新的 FileReader,读取指定文件。
  • FileReader(File file): 创建一个新的 FileReader,读取指定的 File 对象。

FileWriter构造方法

  • FileWriter(String fileName): 创建一个使用默认字符编码的 FileWriter,将数据写入指定文件。如果文件不存在,则创建新文件;如果文件存在,则覆盖原有内容。

  • FileWriter(String fileName, boolean append): 创建一个使用默认字符编码的 FileWriter,将数据写入指定文件。若 append 为 true,则在文件末尾追加内容,而不是覆盖。

  • FileWriter(File file): 创建一个 FileWriter,将数据写入指定的 File 对象。

  • FileWriter(File file, boolean append): 创建一个 FileWriter,将数据写入指定 File 对象,并选择是否在文件末尾追加。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws IOException {
// 写文件
FileWriter writer = new FileWriter("output.txt");
writer.write("Hello, World!");
writer.write("\nWelcome to Java FileWriter.");
writer.close();
// 读文件
FileReader reader = new FileReader("output.txt");
int character;
while ((character = reader.read()) != -1) {
System.out.print((char) character);
}
reader.close();
}
// 输出结果
// Hello, World!
// Welcome to Java FileWriter

字符缓冲流(推荐)

BufferedWriterBufferedReader是带缓冲区的字符流,读写效率相对于普通的字符流OutputStreamWriter和InputStreamReader会高很多。

为什么字符缓冲流读写效率高?

原因跟字节缓冲流类似,它维护了默认容量8192容量的char类型数组,在写入时会将字符先写进这个存于内存的数组中,等容量满了之后,会一次性将数据写进磁盘,而不是每次写数据都进行磁盘IO一次,有效降低了读写次数,又因为磁盘的IO效率是远低于内存IO效率的,所以字符缓冲流的写效率高。

BufferedReader也一个道理,它在第一次读的时候将数据尽可能多的塞满char类型数组,下次读的时候优先从char数组中读,读的效率肯定比直接在磁盘中读的效率高很多。

BufferedWriter

构造方法

  • BufferedWriter(Writer out): 创建一个使用默认大小输出缓冲区的缓冲字符输出流。默认值8192足够大,可用于大多数用途。

    1
    2
    3
    4
    public BufferedWriter(Writer out) {
    // 没指定缓冲区容量时,默认是最大长度8192
    this(out, defaultCharBufferSize);
    }
  • BufferedWriter(Writer out, int sz): 创建一个使用指定大小输出缓冲区的缓冲字符输出流。参数sz可以指定缓冲区大小,或者可以接受默认大小8192。

常用方法

  • void write(int c): 写入单个字符。

  • void write(char[] cbuf, int off, int len): 写入字符数组的某一部分。

  • void write(String s, int off, int len): 写入字符串的某一部分。

  • void newLine(): 写入一个行分隔符。

  • ```java
    public void newLine() throws IOException {

    // lineSeparator是本类的一个属性,在构造函数执行时获取当前操作系统的行分隔符并赋值
    write(lineSeparator);
    

    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55

    - **`void flush()`: 刷新该流的缓冲区。**每次写完bw.flush();可以将缓冲字符写入文本,虽然close()也能刷新后关闭,但加上flush更保险。

    - **`void close()`**: 关闭该流,但要先刷新它。

    **BufferedReader**

    **构造方法**:可以指定缓冲区大小,或者可以使用默认大小8192。 默认值足够大,可用于大多数用途。

    - **`BufferedReader(Reader in)`**: 创建一个使用默认大小输入缓冲区的缓冲字符输入流。
    - **`BufferedReader(Reader in, int sz)`**: 创建一个使用指定大小输入缓冲区的缓冲字符输入流。

    **常用方法**

    - **`int read()`**: 读取单个字符。
    - **`int read(char[] cbuf, int off, int len)`**: 将字符读入数组的某一部分。
    - **`String readLine()`**: 读取一个文本行。
    - **`boolean ready()`**: 判断输入流是否准备好被读取。
    - **`void close()`**: 关闭该流并释放与之关联的所有资源。

    **代码示例**:

    BufferedWriter写入三行文字,然后BufferedReader读出来:

    ```java
    public static void main(String[] args) {
    // 定义文件路径
    String filePath = "example.txt";
    // 使用 BufferedWriter 写文件
    // 使用JDK7的异常处理形式,就不需要finally里关闭流对象了,出异常后系统会自动关闭
    try (BufferedWriter bw = new BufferedWriter(new FileWriter(filePath))) {
    bw.write("你好");
    bw.newLine();
    bw.write("世界");
    bw.newLine();
    bw.write("宇宙");
    } catch (IOException e) {
    // 实际场景建议用logger.error(),而不是e.printStackTrace()
    e.printStackTrace();
    }

    // 使用 BufferedReader 读文件
    try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
    String line;
    while ((line = br.readLine()) != null) {
    System.out.println(line);
    }
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    // 输出结果
    // 你好
    // 世界
    // 宇宙

BufferedReader实现读取键盘录入:

1
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void main(String[] args) {
// 使用 InputStreamReader 包装 System.in,再使用 BufferedReader 包装 InputStreamReader
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
// 提示用户输入
System.out.println("请输入一些文字:");
// 读取用户输入并输出
try {
String input = br.readLine();
System.out.println("你输入的是:" + input);
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭 BufferedReader
try {
if (br != null) {
br.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 打印结果
// 请输入一些文字:
// 123
// 你输入的是:123

I/O流JDK7异常处理

I/O流的异常处理:

JDK7之前的常规做法:try…catch…finally

1
2
3
4
5
6
7
try {
// 可能出现异常的代码
} catch (异常类名 变量名) {
// 异常的处理代码
} finally {
// 执行所有清除操作
}

JDK7改进方案:try-with-resources

因为之前方案,每次都要finally中再次判空、try-catch区关闭IO流,太过麻烦,所以JDK7引入了新的异常处理形式,可以把流对象放到try()的括号里,这样就不需要再写finally了,系统会自动释放资源

1
2
3
4
5
6
try (定义流对象) {
// 可能出现异常的代码
} catch (异常类名 变量名) {
// 异常的处理代码
}
// 自动释放资源

代码示例:

使用字符缓冲流写文件后,读文件:

JDK7之前格式:

异常处理后,需要在finally里关闭流对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public static void main(String[] args) {
// 定义文件路径
String filePath = "example.txt";
// 使用 BufferedWriter 写文件
BufferedWriter bw = null;
try {
bw = new BufferedWriter(new FileWriter(filePath));
bw.write("Hello, World!");
bw.newLine();
bw.write("Welcome to Java BufferedWriter.");
bw.newLine();
bw.write("This is an example of BufferedWriter.");
} catch (IOException e) {
// 实际场景建议用logger.error(),而不是e.printStackTrace()
e.printStackTrace();
} finally {
if (bw != null) {
// 关闭流时也需要异常处理,防止关闭失败
try {
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

// 使用 BufferedReader 读文件
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(filePath));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

JDK7格式:

异常处理后,不需要再写finally了,系统会自动释放资源,可以看到代码变简洁很多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void main(String[] args) {
// 定义文件路径
String filePath = "example.txt";
// 使用 BufferedWriter 写文件
// 使用JDK7的异常处理形式,就不需要finally里关闭流对象了,出异常后系统会自动关闭
try (BufferedWriter bw = new BufferedWriter(new FileWriter(filePath))) {
bw.write("你好");
bw.newLine();
bw.write("世界");
bw.newLine();
bw.write("宇宙");
} catch (IOException e) {
// 实际场景建议用logger.error(),而不是e.printStackTrace()
e.printStackTrace();
}
// 使用 BufferedReader 读文件
try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}

特殊操作流

标准输入输出流:System.in和System.out

什么是标准输入输出流?

  • 标准输出流:System.out。用于指定输出到控制台的数据。
  • 标准输入流:System.in。用于读取用户在控制台输入的数据。

标准输出流

基本介绍

标准输出流:System.out。用于往控制台输出数据。

常用方法:

  • print(): 打印数据到控制台,不换行。
  • println(): 打印数据到控制台,并换行。
  • printf(): 使用指定的格式字符串和参数将数据格式化并打印到控制台。

代码示例:

1
2
3
4
5
6
7
8
9
10
public class Test {
public static void main(String[] args) {
// 输出 "Hello, " 不换行
System.out.print("Hello, ");
// 输出 "World!" 并换行
System.out.println("World!");
// 使用格式化输出
System.out.printf("数字: %d", 42);
}
}

常用格式化符号
使用System.out、String.format可以对字符串格式化。

常用格式化符号

%d:十进制整数
%f:十进制浮点数
%e:科学计数法表示的十进制数
%g:根据值的大小,自动选择使用普通计数法或科学计数法
%s:字符串
%c:字符
%b:布尔值(true 或 false)
%x:整数的十六进制表示
%o:整数的八进制表示
%t 或 %T:日期和时间(需要与日期时间转换符配合使用)
%%:文字百分号(% 本身)

日期和时间转换符

%tF:年-月-日(yyyy-MM-dd)
%tD:月/日/年(MM/dd/yy)
%tT:时:分:秒(HH:mm:ss)
%tR:时:分(HH:mm)
%tY:年(四位数)
%ty:年(两位数)
%tm:月(两位数)
%td:日(两位数)
%tH:小时(24 小时制,两位数)
%tI:小时(12 小时制,两位数)
%tM:分钟(两位数)
%tS:秒(两位数)
%tp:小写上午或下午标记(am 或 pm)
%tZ:时区

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Test {
public static void main(String[] args) {
int number = 123;
double pi = 3.14159;
String name = "小明";
boolean flag = true;
// 格式化输出整数
System.out.printf("整数:%d\n", number);
// 格式化输出浮点数
System.out.printf("浮点数(两位小数):%.2f\n", pi);
// 格式化输出字符串
System.out.printf("名字:%s\n", name);
// 格式化输出布尔值
System.out.printf("布尔值:%b\n", flag);
// 格式化输出十六进制整数
System.out.printf("十六进制:%x\n", number);
// 格式化输出八进制整数
System.out.printf("八进制:%o\n", number);
// 格式化输出百分比
double percentage = 0.85;
System.out.printf("百分比:%.2f%%\n", percentage * 100);
// 左对齐和宽度格式化
// %-10s 用于左对齐并指定宽度为10的字符串
System.out.printf("左对齐:%-10s|\n", name);
// 右对齐和宽度格式化
// %10s 用于右对齐并指定宽度为10的字符串
System.out.printf("右对齐:%10s|\n", name);
// 格式化输出带符号的数值
// %+d 用于格式化带符号的整数
System.out.printf("带符号的整数:%+d\n", -number);
// %+.2f 用于格式化带符号的浮点数
System.out.printf("带符号的浮点数:%+.2f\n", pi);
}
}

标准输入流

基本介绍

标准输入流:System.in。用于读取用户在控制台输入的数据。

用法:

  • 字符输入流:作为构造参数传入字符输入流InputStreamReader中,例如new InputStreamReader(System.in)。
  • Scanner:因为用字符输入流包装太麻烦,所以Java提供了一个工具类实现键盘录入,即Scanner类,专门用于从各种输入源(如控制台、文件、字符串等)读取输入。

常用方法:

  • read(): 从输入流中读取一个字节。
  • read(byte[] b): 从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。
  • read(byte[] b, int off, int len): 从输入流中将 len 个字节的数据读入一个字节数组。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
// 使用 BufferedReader 包装 System.in
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
// 提示用户输入
System.out.print("随便输入一些文字: ");
try {
// 读取用户输入
String text = reader.readLine();
// 输出用户输入
System.out.println("你输入的内容是: " + text);
} catch (IOException e) {
// 实际场景不建议e.printStackTrace(),而是logger.error()
e.printStackTrace();
}
}

Scanner类

因为用字符输入流包装太麻烦,所以Java提供了一个工具类实现键盘录入,即Scanner类,专门用于从各种输入源(如控制台、文件、字符串等)读取输入。

构造方法:

  • Scanner(InputStream source): 构造一个新的 Scanner,生成的扫描器从指定的输入流读取数据。用于读取用户在控制台输入的数据。一般用System.in,即标准输入流,用于读取用户在控制台输入的数据。
  • Scanner(File source): 构造一个新的 Scanner,生成的扫描器从指定的文件读取数据。
  • Scanner(String source): 构造一个新的 Scanner,生成的扫描器从指定的字符串读取数据。

常用方法:

  • nextInt()、nextDouble()、next():获取输入的整数、浮点数、字符串(不包括空格)等。
  • nextLine():获取一行输入(包括空格)。
  • hasNextInt()、hasNextDouble()、hasNext(): 判断下一个输入是否为整数、浮点数、字符串。
  • useDelimiter(String pattern): 设置分隔符模式,用于指定不同类型数据之间的分隔符,默认为空白字符。
  • close():关闭扫描器。

示例:

从控制台输入整数、浮点数、字符串,并在控制台打印。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Test {
public static void main(String[] args) {
// 创建 Scanner 对象,关联 System.in(标准输入流)
Scanner scanner = new Scanner(System.in);
// 从控制台读取整数
System.out.print("Enter an integer: ");
int intValue = scanner.nextInt();
System.out.println("You entered: " + intValue);
// 从控制台读取浮点数
System.out.print("Enter a double: ");
double doubleValue = scanner.nextDouble();
System.out.println("You entered: " + doubleValue);
// 从控制台读取字符串
System.out.print("Enter a string: ");
String stringValue = scanner.next();
System.out.println("You entered: " + stringValue);
// 关闭 Scanner 对象
scanner.close();
}
}

打印流

字节打印流:PrintStream

PrintStream用于将不同的数据类型格式化为字节并写入到输出流,支持自动刷新。它只负责输出数据,不负责读取数据。

一般情况下,打印数据用System.out.printf()就可以了,PrintStream适用于需要重定向输出的场景,例如将打印的结果写入一个文件中。

重定向输出:将输出重定向到文件、网络流或其他输出目标。

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
try (PrintStream ps = new PrintStream(new FileOutputStream("output.txt"))) {
ps.println("Hello, World!");
ps.println(123);
ps.println(true);
} catch (IOException e) {
e.printStackTrace();
}
}

构造方法:

  • PrintStream(OutputStream out): 创建一个新的打印流,使用指定的输出流。
  • PrintStream(OutputStream out, boolean autoFlush): 创建一个新的打印流,使用指定的输出流和自动刷新设置。
  • PrintStream(OutputStream out, boolean autoFlush, String encoding): 创建一个新的打印流,使用指定的输出流、自动刷新设置和字符编码。
  • PrintStream(String fileName): 创建一个新的打印流,使用指定的文件名。
  • PrintStream(String fileName, String encoding): 创建一个新的打印流,使用指定的文件名和字符编码。
  • PrintStream(File file): 创建一个新的打印流,使用指定的文件。
  • PrintStream(File file, String encoding): 创建一个新的打印流,使用指定的文件和字符编码。

常用方法:

  • void print(String s): 打印字符串。
  • PrintStream printf(String format, Object… args): 使用指定的格式字符串和参数,将格式化的字符串输出到该流。
  • void println(int i): 打印整数并换行。
  • close():关闭。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 打印到控制台:
public static void main(String[] args) {
// true 表示自动刷新
PrintStream ps = new PrintStream(System.out, true);
ps.println("Hello, World!");
ps.println("Hello, World2!");
}
// 打印到文件:
public static void main(String[] args) {
PrintStream pw= null;
try {
pw = new PrintStream("1.txt");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
pw.println("hello");
pw.println(23);
pw.close();
}

字符打印流:PrintWriter

PrintWriter与 PrintStream 类似,都用于打印数据到文件。主要区别是PrintWriter用于打印字符流,而不是字节流,它可以自动处理字符编码。

构造方法

  • PrintWriter(OutputStream out): 创建一个新的 PrintWriter,从现有的 OutputStream 输出字符。

  • PrintWriter(OutputStream out, boolean autoFlush): 创建一个新的 PrintWriter,从现有的 OutputStream 输出字符,并指定是否在调用 println、printf 或 format 方法时自动刷新。

  • PrintWriter(Writer out): 创建一个新的 PrintWriter,从现有的 Writer 输出字符。

  • PrintWriter(Writer out, boolean autoFlush): 创建一个新的 PrintWriter,从现有的 Writer 输出字符,并指定是否在调用 println、printf 或 format 方法时自动刷新。

  • PrintWriter(String fileName): 创建一个新的 PrintWriter,将输出写入指定的文件。

  • PrintWriter(String fileName, String csn): 创建一个新的 PrintWriter,将输出写入指定的文件,使用给定的字符集。

  • PrintWriter(File file): 创建一个新的 PrintWriter,将输出写入指定的文件。

  • PrintWriter(File file, String csn): 创建一个新的 PrintWriter,将输出写入指定的文件,使用给定的字符集。

常用方法:

  • 跟PrintStream基本一致,这里不再赘述。

代码示例:

1
2
3
4
5
6
7
8
public static void main(String[] args) throws IOException {
//自动刷新true
PrintWriter pw=new PrintWriter("2.txt");
//直接写一行加刷新
pw.println("hello");
pw.println(23);
pw.close();
}

序列号流和反序列化流

Serializable 接口和serialVersionUID变量

序列化和反序列化:

  • 序列化:将对象转换为字节流。转为字节流后可以把它写入文件、数据库,或者传输到网络中。
  • 反序列化:序列化的逆过程,也就是将字节流转换回对象。

不参与序列化的字段:被transient修饰的成员变量不参与序列化。

被序列化的类必须实现Serializable接口,并且声明一个成员变量serialVersionUID

Serializable 接口:

用于标记一个类的对象可被序列化。该接口没有任何方法,它仅仅是一个标记,表明实现这个接口的类可以被 Java 的序列化机制处理。

若一个类的对象没有实现这个接口,并且他的对象被序列号,则会报错不能序列号异常java.io.NotSerializableException

erialVersionUID变量:

  • 作用:serialVersionUID是类的版本号,类型是长整型数值,该字段的主要作用是检查反序列化时类的版本,与序列化时类的版本是否一致。
  • 私有:为了保证不同Java编译器实现之间的一致的serialVersionUID值,一个可序列化的类必须声明一个显式的serialVersionUID值,尽量使用private修饰符。
  • 序列化类必须包含serialVersionUID:为了保证不同Java编译器实现之间的一致的serialVersionUID值,一个可序列化的类必须声明一个显式的serialVersionUID值,尽量使用private修饰符。
  • 建议手动指定:如果实现了Serializable接口,并且没有指定serialVersionUID,编译器会隐式地自动生成一个serialVersionUID,但是建议手动指定,因为默认的serialVersionUID计算对类详细信息非常敏感,从而出现InvalidClassException

InvalidClassException异常:

序列化时版本号不匹配,会抛出无效类异常。例如反序列化时类的版本号,与序列化时类的版本号不一致在序列化之后,类的结构发生了变化,例如添加、删除或修改了字段,导致版本号不匹配。

代码示例:

1
2
3
4
5
// 创建一个可序列号的Dog类
public class Dog implements Serializable {
private static final long serialVersionUID = 1L;
// ... 其他定义
}

对象序列化流:ObjectOutputStream

序列化流可以将对象转换为字节流,并保存到文件或者传输到网络。

构造方法:

  • ObjectOutputStream(OutputStream out): 创建一个写入指定 OutputStream 的 ObjectOutputStream。构造参数是FileOutputStream等OutputStream的子类。

常用方法:

  • void writeObject(Object obj): 将指定的对象写入 ObjectOutputStream。
  • void close(): 关闭输出流并释放与此流相关联的所有系统资源。
  • void flush(): 刷新输出流,并强制任何缓冲的输出字节被写出。

代码示例:被序列化的类使用上一节创建的Dog类:

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
public static void main(String[] args) {
Dog dog = new Dog(5, "旺财");
// 使用JDK7的异常捕获,不需要手动关闭流
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("dog.txt"))) {
oos.writeObject(dog);
System.out.println("序列化完成:" + dog);
} catch (IOException e) {
e.printStackTrace();
}
}
}

对象反序列化流:ObjectInputStream

ObjectInputStream是序列化的逆过程,也就是将字节流转换回对象。

构造方法

  • ObjectOutputStream(OutputStream out): 创建一个写入指定 OutputStream 的 ObjectOutputStream。

常用方法

  • void writeObject(Object obj): 将指定的对象写入 ObjectOutputStream。
  • void close(): 关闭输出流并释放与此流相关联的所有系统资源。
  • void flush(): 刷新输出流,并强制任何缓冲的输出字节被写出。

代码示例:反序列化上一节创建的dog.txt:

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
public static void main(String[] args) {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("dog.txt"))) {
Dog dog = (Dog) ois.readObject();
System.out.println("反序列化完成:" + dog);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
// 输出结果
// 反序列化完成:Dog{weight=0,name="旺财"}