✨你好啊,我是“ 罗师傅”,是一名程序猿哦。
🌍主页链接:楚门的世界 - 一个热爱学习和运动的程序猿
☀️博文主更方向为:分享自己的快乐 briup-jp3-ing
❤️一个“不想让我曾没有做好的也成为你的遗憾”的博主。
💪很高兴与你相遇,一起加油!

前言

目标:Java高级编程,灵活运用反射,线程,IO和网络等进行编程

File类

概述

java.io.file 类是文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作。

  • 构造方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package java.io;
public class File
implements Serializable, Comparable<File>
{
//通过将给定路径名字符串来创建新的 File实例
public File(String pathname) {
if (pathname == null) {
throw new NullPointerException();
}
this.path = fs.normalize(pathname);
this.prefixLength = fs.prefixLength(this.path);
}
//从**父抽象路径名和子路径名字符串**创建新的 File实例
public File(String parent, String child) {
//省略...
}
public File(File parent, String child) {
//省略...
}
//省略...
}
  • 实例对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test011_File {
public static void main(String[] args) {
// 文件路径名
String pathname = "D:\\aaa.txt";
File file1 = new File(pathname);
// 文件路径名
String pathname2 = "D:\\aaa\\bbb.txt";
File file2 = new File(pathname2);
// 通过父路径和子路径字符串
String parent = "D:\\aaa";
String child1 = "bbb.txt";
File file3 = new File(parent, child1);
// 通过父级File对象和子路径字符串
File parentDir = new File("D:\\aaa");
String child2 = "bbb.txt";
File file4 = new File(parentDir, child2);
}
}
  • 一个File对象代表硬盘中实际存在的一个文件或者目录
  • 无论该路径下是否存在文件或者目录,都不影响File对象的创建

使用

  • 路径获取
1
2
3
4
5
6
7
8
//File绝对路径名字符串
public String getAbsolutePath();
//File文件构造路径
public String getPath();
//File文件或目录的名称
public String getName();
//File文件或目录的长度
public long length();

案例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test012_File {
public static void main(String[] args) {
File file = new File("C:\\Users\\luozongwei\\Desktop\\aaa.txt");
System.out.println("文件的绝对路径: " + file.getAbsolutePath());
System.out.println("文件的构造路径: " + file.getPath());
System.out.println("文件或目录的名称:" + file.getName());
System.out.println("文件或目录的长度: " + file.length());
}
}
// 文件的绝对路径: C:\Users\luozongwei\Desktop\aaa.txt
// 文件的构造路径: C:\Users\luozongwei\Desktop\aaa.txt
// 文件或目录的名称:aaa.txt
// 文件或目录的长度: 19

API中说明:length(),表示文件的长度。但是File对象表示目录,则返回值未指定。

  • 路径操作
    • 绝对路径:从盘符开始的路径,这是一个完整的路径
    • 相对路径:相对于项目目录的路径,这是一个便捷的路径,开发中经常使用

案例描述:输入具体文件路径,以及只输入文件名字,通过File的绝对路径方法, 验证结果

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test013_File {
public static void main(String[] args) {
// D盘下的Test1101_File.java文件
File f = new File("D:\\Test1101_File.java");
System.out.println(f.getAbsolutePath());
// 项目下的Test1101_File.java文件(不包含包名) 当前项目就近原则
File f2 = new File("Test1101_File.java");
System.out.println(f2.getAbsolutePath());
}
}
// 输出结果:
// D:\Test1101_File.java
// E:\work\2023-CoreJava\Test1101_File.java
  • 判断操作
1
2
3
4
5
6
//判断文件或目录是否存在
public boolean exists();
//判断是否是文件
public boolean isFile();
//判断是否是目录
public boolean isDirectory();
  • 创建删除操作
1
2
3
4
5
6
7
8
//当且仅当具有该名称的文件尚不存在时,创建一个新的空文件
public boolean createNewFile();
//创建目录
public boolean mkdir();
//创建多级目录
public boolean mkdirs();
//文件或目录的删除
public boolean delete();

API中说明:delete方法,如果此File表示目录,则目录必须为空才能删除。

  • 目录遍历操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class File implements Serializable, Comparable<File>
{
//省略...
//目录文件调用该方法,获取目录中所有子文件名,返回String数组
//其他文件调用该方法,返回null
public String[] list();
//目录文件调用该方法,获取目录中所有子文件,返回File数组
//其他文件调用该方法,返回null
public File[] listFiles();
//目录文件调用该方法,获取目录中符合筛选条件的子文件,返回File数组
//其他文件调用该方法,返回null
public File[] listFiles(FileFilter filter);
//省略...
}
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
public class Test016_File {
public static void main(String[] args) {
String dirPath = "D:\\";
File dirFile = new File(dirPath);

String[] list = dirFile.list();
for (String str : list) {
System.out.println(str);
}
System.out.println("------------------");
// 3、准备普通文件
String fileName = "readme.pdf";
File file = new File(dirFile, fileName);
// 4、普通文件调用list返回null
String[] list2 = file.list();
System.out.println(list2); // null

// 5、获取目录中所有子文件对象,并遍历输出
File[] listFiles = dirFile.listFiles();
for (File f : listFiles) {
System.out.println(f);
}
System.out.println("-------------------");

// 6、使用文件过滤器,获取目录下所有普通文件,并遍历输出
File[] listFiles2 = dirFile.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
if (pathname.isFile()) {
return true;
}
return false;
}
});

for (File file1 : listFiles2) {
System.out.println(file1.getName());
}
}
}

IO流

流的概念

在计算机中,流是一个抽象的概念,是对输入输出设备的抽象。在Java程序中,对于数据的输入/输出操作,都是以“流”的方式进行

数据以二进制的形式在程序与设备之间流动传输,就像水在管道里流动一样,所以就把这种数据传输的防暑称为输入流、输出流。这里描述的设备,可以是文件、网络、内存

流具有方向性,可以分为输入和输出流

以java程序本身作为参照点:

  • 如果数据是从程序“流向”文件,那么这个流就是输出流
  • 如果数据是从文件“流向”程序,那么这个流就是输入流

流的分类

Java中的IO流可以根据很多不同的角度进行划分,最常见的是以数据的流向和数据的类型来划分

  • 数据流向
    • 输入流:把数据从其他设备上读取到程序中的流
    • 输出流:把数据从程序中写出到其他设备上的流
  • 数据类型
    • 字节流:以字节为单位(byte),读写数据的流
    • 字符流:以字符为单位(char),读写数据的流

流的结构

在Java中,和IO流相关的类,主要是在java.io包中定义

几乎所有的流,都是派生自四个抽象的父类型:

  • InputStream,字节输入流
  • OutputStream,字节输出流
  • Reader,字符输入流
  • Writer,字符输出流

Java中常用的流及其继承结构:

流会具备起码的三个特点:

  • 是输入还是输出
  • 是字节还是字符
  • 流的目的地
    • 如果是输入流,就表示这个流从什么地方读数据
    • 如果是输出流,就表示这个流把数据写到什么地方

字节流

一切文件数据(文本、图片、视频等)在存储时,都是以二进制数字的形式保存,都一个一个的字节,那么传输时一样如此。所以,字节流可以传输任意文件数据。在操作流的时候,我们要时刻明确,无论使用什么杨的流对象,底层传输的始终为二进制数据。

java.io.InputStream 是所有字节输入流的抽象父类型

java.io.OutputStream 是所有字节输出流的抽象父类型

一般情况,是以哦那个字节流来操作数据的时候,往往时使用一对,一个字节输入流,负责读取数据,一个字节输出流,负责将数据写出去,而这些流都将InputStream和OutputStream的子类型。

使用流的基本步骤

  • 选择流
  • 声明流
  • 创建流
  • 使用流
  • 关闭流

文件输入流

文件字节输入流 FileInputStream ,用于从文件中读取字节数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package java.io;
public class FileInputStream extends InputStream
//省略...
//通过File对象来创建一个 FileInputStream
public FileInputStream(File file) throws FileNotFoundException;
//通过文件路径名(字符串)实例化FileInputStream对象
public FileInputStream(String name) throws FileNotFoundException;
//逐个字节读取,返回值为读取的单个字节
public int read() throws IOException;
//小数组读取,将结果存入数组,返回值为读取的字节个数
public int read(byte b[]) throws IOException;
//小数组读取,存入数组指定位置,返回值为读取的字节个数
public int read(byte b[], int off, int len) throws IOException;
//省略...
}

注意事项: 创建FileInputStream对象时,必须传入一个有效文件路径,否则抛出 FileNotFoundException !

文件输出流

文件字节输出流, FileOutputStream ,用于写入字节数据到文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
package java.io;
public
class FileOutputStream extends OutputStream
//创建文件输出流以写入由指定的 File对象表示的文件。
public FileOutputStream(File file) throws FileNotFoundException;
//创建文件输出流以指定的名称写入文件
public FileOutputStream(String name) throws FileNotFoundException;
public FileOutputStream(File file, boolean append) throws FileNotFoundException;
public void write(int b) throws IOException;
public void write(byte b[]) throws IOException;
public void write(byte b[], int off, int len) throws IOException;
//省略...
}

注意事项:

  • 创建一个输出流对象时传入的文件路径可以不存在,不会抛出异常,系统会走动创建该文件
  • 如果有这个文件,系统默认会清空这个文件的数据

注意:不同操作系统中回车、换行符时不同的

  • 回车符 \r 和 换行符 \n
    • 回车符:回到一行的开头(return)
    • 换行符:下一行(newline)
  • 系统中的换行:
    • Windows系统里,每行结尾时 回车+换行,即 \r\n
    • Unix系统里,每行结尾只有 换行,即\n
    • Mac系统里,每行结尾时 回车,即\r。从 Mac OS X开始与Linux统一

综合案例: 拷贝 src/dir/a.txt 内容到 src/dir/b.txt 中,a.txt文件内容如下。

1
2
3
4
5
6
7
8
hello world
1、确认过眼神,我遇上的人。我策马出征,马蹄声如泪奔。青石板上的月光照进这
山城。
我一路的跟,你轮回声,我对你用情极深。 --方文山 《醉赤壁》
2、无关风月 我题序等你回 悬笔一绝 那岸边浪千叠 --方文山 《兰亭序》
3、那画面太美,我不敢看 --方文山 《布拉格广场》
4、你说 想哭就弹琴 想起你就写信情绪来了就不用太安静 --方文山 《想你就写
信》
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
public class Test024_Copy {
public static void main(String[] args) {
InputStream is = null;
OutputStream os = null;
try {
is = new FileInputStream(new File(CommonPath.DESKTOP, "aaa.txt"));
os = new FileOutputStream(new File(CommonPath.CHAP_11, "bbb.txt"));
byte[] buff = new byte[1024];
int len = -1;
while ((len = is.read(buff)) != -1) {
os.write(buff, 0, len);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (os != null) {
os.close();
}
if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("copy finish");
}
}

注意事项:

  • 逐个字节拷贝效率太低,推荐使用小数组拷贝
  • 关闭资源的顺序应该和打开资源顺序相反:先打开的后关闭,后打开的先关闭

文件追加

回顾之前案例,我们发现每次创建文件输出流对象,在操作时都会清空目标文件 中的数据。

思考:如何保留目标文件中数据,在原有文件内容的后面添加新数据呢?

1
2
3
4
// 创建文件输出流以写入由指定的 File对象表示的文件。
public FileOutputStream(File file, boolean append);
// 创建文件输出流以指定的名称(内容)写入文件
public FileOutputStream(String name, boolean append);
  • boolean append
    • true:表示追加数据
    • false:表示清空原有数据

内存输出流

使用文件流,我们可以操作文件中的数据

使用内存流,我们可以操作内存中字节数组中的数据

内存字字节流,也称为字节数组流,主要有下面两种:

  • java.io.ByteArrayOutputStream
    • 内存输出流,负责把数据写入到内存中的字节数组中
  • java.io.ByteArrayInputStream
    • 内存输入流,负责从内存中的字节数组中读取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package java.io;
public class ByteArrayOutputStream extends OutputStream {
//存储数据的数组
protected byte buf[];
//存入字节数组的元素(字节)个数
protected int count;
//无参构造器创建的字节数组输出流,数组大小为32个字节
public ByteArrayOutputStream() {
this(32);
}
//关键方法:获取内存输出流中存储的数据,返回字节数组
public synchronized byte toByteArray()[] {
return Arrays.copyOf(buf, count);
}
//省略...
}

案例展示: 小数组方式读取 src/dir/a.txt 文件中的所有内容,写入到字节数组输出流 中,然后从字节输出流中获取所有数据,最后转换成String字符串输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test024_ByteArrayOutput {
public static void main(String[] args) throws IOException {
// 1、关联流对象和文件
// 创建内存输出流对象[new byte[32]]
InputStream is = new FileInputStream(new File(CommonPath.CHAP_11, "bbb.txt"));
ByteArrayOutputStream baos = new ByteArrayOutputStream();

// 2、读取文件内容 然后写入到 内存输出流中
byte[] buff = new byte[8];
int len = -1;
while ((len = is.read(buff)) != -1) {
baos.write(buff, 0, len);
}
System.out.println("copy finish");
// 3、关键方法:获取内存输出流中的数据
byte[] byteArray = baos.toByteArray();

// 4、将byte[] --> String 并输出
System.out.println(new String(byteArray));
}
}

注意:内存流使用完不需要close()去释放资源!

因为内存流的底层数据源时内存中的字节数组,不需要进行资源释放或关闭操作。当使用完ByteArrayXxxStream后,可以选择不进行任何操作,它会自动被垃圾回收机制回收

内存输入流

1
2
3
4
5
6
7
8
9
10
11
12
13
package java.io;
public class ByteArrayInputStream extends InputStream {
protected byte buf[];
protected int pos;
protected int count;
//关键构造器
public ByteArrayInputStream(byte buf[]) {
this.buf = buf;
this.pos = 0;
this.count = buf.length;
}
//省略...
}
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 Test024_ByteArrayInput {
public static void main(String[] args) throws IOException {
// 1、实例化sc对象,输入一行
Scanner sc = new Scanner(System.in);
System.out.println("input line:");
String line = sc.nextLine();

// 2、创建内存输入流对象 ByteArrayInputStream
byte[] bytes = line.getBytes();
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
FileOutputStream fos = new FileOutputStream(new File(CommonPath.DESKTOP, "aaa.txt"));

byte[] buff = new byte[5];
int len = -1;
while((len = bais.read(buff)) != -1) {
fos.write(buff, 0, len);
}
System.out.println("success write to file");

fos.close();
bais.close();

}
}

内存流总结:

  • 如果要操作文件,则需要使用文件流
  • 如果要操作内存中的数据,则可以选择内存流

内存字节流提供了一种在内存中进行数据读取和写入的便捷方式。内存流在实际 开发中有专门的应用场景,了解即可。

以下是一些常见的应用场景:

  • 数据的临时存储:内存字节流可以将数据暂时存储在内存中的字节数组 中,而不需要写入到磁盘或网络中。这在一些临时性的数据处理场景中非 常有用,例如在内存中对数据进行加密、解密、压缩、解压缩等操作。
  • 数据的转换:内存字节流可以用于将数据从一种格式转换为另一种格式。 例如,可以将一个对象序列化为字节数组,然后再将字节数组反序列化为 对象。这在一些需要将数据在内存中进行格式转换的场景中非常有用。
  • 测试和调试:内存字节流可以用于测试和调试目的,例如模拟输入流或输 出流的行为。通过将数据写入内存字节流,可以方便地检查数据的内容和 格式,而无需依赖外部资源。
  • 单元测试:内存字节流在单元测试中也非常有用。可以使用内存字节流模 拟输入和输出流,以便在测试中验证代码的正确性和可靠性,而无需依赖 外部文件或网络连接。

需要注意的是,由于内存字节流将数据存储在内存中的字节数组中,因此在处理 大量数据时可能会占用较多的内存。在这种情况下,需要谨慎使用内存字节流, 以避免内存溢出的问题。

字符流

java.io.Reader 是所有字符输入流的抽象父类型:

java.io.Writer 抽线类是表示用于写出字符流的所有类的超类,将指定的字符信息写出到目的地。它定义了字节输出流的基本共性功能方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 写入单个字符
void write(int c);
// 写入字符数组
void write(char[] cbuf);
// 写入字符数组的某一部分,off数组的开始索引,len写的字符个数
abstract void write(char[] cbuf, int off, int len);
// 写入字符串
void write(String str);
// 写入字符串的某一部分,off字符串的开始索引,len写的字符个数
void write(String str, int off, int len);
// 刷新该流的缓冲
void flush();
// 关闭此流,但要先刷新它
void close();

这些字符流中的读和写的方法,与字节流中的读和写方法类似,掌握了字节 流的使用,这些很容易理解

文件字符流

java.io.FileReader 类是读取字符文件(纯文本文件)的便利类。构造时使用系统默认的字符编码和默认字节缓冲区

java.io.FileWriter 类是写出字符到文件的便利类。构造时使用系统默认的 字符编码和默认字节缓冲区。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package java.io;
public class FileReader extends InputStreamReader {
public FileReader(String fileName) throws FileNotFoundException;
public FileReader(File file) throws FileNotFoundException;
//省略...
}
public class FileWriter extends OutputStreamWriter {
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;
//省略...
}

注意事项:

创建一输入流对象时,必须传入一个有效文件路径,否则会抛出 FileNotFoundException

创建一个输出流对象时,传入的文件路径可以不存在,系统会自动创建该文件。如果有这个文件,默认会清空这个文件的数据

案例描述:使用文件字符流拷贝a.txt文件内容到b.txt文件末尾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test025_FileReaderWriter {
public static void main(String[] args) throws IOException {
// 1.实例化流对象
FileReader fr = new FileReader(new File(CommonPath.DESKTOP, "aaa.txt"));
// 设置文件追加
FileWriter fw = new FileWriter(new File(CommonPath.DESKTOP, "bbb.txt"), true);
// 2.使用流进行文件拷贝
char[] cbuf = new char[8];
int len = -1;
while ((len = fr.read(cbuf)) != -1) {
fw.write(cbuf, 0, len);
}
//刷新流
fw.flush();
// 3.关闭流
fw.close();
fr.close();
}
}

操作字节文件

案例描述:使用文件字符流完成图片的拷贝,看结果发现问题

修改上面 Test025_FileReaderWriter 案例:

​ 1.将操作的文件换成 src/dir/001.jpg和src/dir/002.jpg

​ 2.去除输出流追加标志

结论:字符流,只能操作文本文件,不能操作图片,视频等非文本文件

当我们单纯读或写存文本文件时,使用字符流

其他情况(图片、音频、doc、xls、ppt等等),使用字节流

异常处理

之前我们对异常的处理,都是直接throws抛出,实际开发中并不能这样处理,建 议使用 try…catch…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
public class Test026_Catch {
public static void main(String[] args) {
Reader in = null;
Writer out = null;
try {
// 1、实例化流对象
in = new FileReader(CommonPath.DESKTOP + "aaa.txt");
// 设置文件追加
out = new FileWriter(CommonPath.DESKTOP+"bbb.txt", true);
// 2、使用流进行文件拷贝
int len = -1;
char[] cbuf = new char[8];
while ((len = in.read(cbuf)) != -1) {
out.write(cbuf, 0, len);
}
// 刷新流
out.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}

}
}
}

扩展知识:JDK7新增异常处理方式(了解即可)

JDK7优化后的 try-with-resource 语句,该语句确保了每个资源在语句结束 时关闭。

所谓的资源(resource)是指在程序完成后,必须关闭的对象。

格式:

1
2
3
4
5
try (创建流对象语句,如果存在多个,使用';'隔开) {
// 读写数据
} catch (IOException e) {
e.printStackTrace();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test027_Catch {
public static void main(String[] args) {
// 1、实例化流对象,这种格式,系统会自动释放资源
try (FileReader fr = new FileReader("src/dir/a.txt");
FileWriter fw = new FileWriter("src/dir/b.txt", true)){
// 2. 使用流进行文件拷贝
int len = -1;
char[] cbuf = new char[8];
while ((len = fr.read(cbuf)) != -1) {
fw.write(cbuf, 0 ,len);
}
// 刷新流
fw.flush();
System.out.println("copy finish");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}

标准流

  • System.in 标准输入流,是类InputStream的对象默认可以从键盘输入读取字节数据
  • System.out 标准输出流,是类PrintStream的对象默认可以控制台中输出字符和字节数据
  • 非标准流可以改变打印流方向

案例描述:先按照标准输出流输出内容,然后改变输出方向,进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test028_PrintStream {
public static void main(String[] args) throws IOException
{
// 调用系统的打印流,控制台直接输出97
System.out.println(97);
// 创建打印流,指定文件的名称
PrintStream ps = new PrintStream("src/ps.txt");
// 设置系统的打印流流向,输出到ps.txt
System.setOut(ps);
// 调用系统的打印流,ps.txt中输出97
System.out.println(97);
}
}

缓冲流(有cp)

缓冲思想:

在Java的I/O流中,缓冲思想是一种常见的优化技术,用于提高读取和写入数据的效率。它通过在内存中引入缓冲区(Buffer)来减少实际的I/O的操作次数,从而提高数据传输的效率。

缓冲思想的基本原理是将数据暂时存储在内存中的缓冲区中,然后按照一定的块大小进行读取或写入操作。相比于直接对磁盘或网络进行读写操作,使用缓冲区可以减少频繁的I/O操作,从而提高效率。

缓冲流也叫高效流

  • 缓冲字节流
1
2
3
4
// 创建一个新的缓冲输入流
public BufferedInputStream(InputStream in);
// 创建一个新的缓冲输出流
public BufferedOutputStream(OutputStream out)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package java.io;
public class BufferedInputStream extends FilterInputStream {
//缓冲输入流,又称为包装流,使用时需要传入基本输入字节流对象
public BufferedInputStream(InputStream in);
//size,代表设置读取缓冲大小
public BufferedInputStream(InputStream in, int size);
}

public class BufferedOutputStream extends FilterOutputStream {
//缓冲输出流,又称为包装流,使用时需要传入基本输出字节流对象
public BufferedOutputStream(OutputStream out) {
}
//size,代表设置写入缓冲大小
public BufferedOutputStream(OutputStream out, int size) {
}
}

可以看出,字节缓冲流的构造器,要求一定要传入一个字节流对象,然后缓冲流就可以对这个字节流的功能进行增强,提供缓冲数据的功能,从而提高读写的效率。

如何验证使用缓冲流来增强文件字节流的功能,提高读写效率?

案例描述: 完成背影.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
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
public class Test03_BufferedStream {
public static void main(String[] args) {
// 1、声明流
InputStream is = null;
OutputStream os = null;

try {
File file1 = new File("src/dir/背影.txt");
File file2 = new File("src/dir/背影copy.txt");
// is = new FileInputStream(file1);
// os = new FileOutputStream(file2);
is = new BufferedInputStream(new FileInputStream(file1));
os = new BufferedOutputStream(new FileOutputStream(file2));

int data = -1;
byte[] buf = new byte[8];
long startTime = System.currentTimeMillis();
while ((data = is.read(buf)) != -1) {
os.write(buf, 0, data);
}
long endTime = System.currentTimeMillis();
System.out.println("-----costTime: " + (endTime - startTime) + "ms");

} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}

if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
// -----costTime: 368ms
// -----costTime: 39ms + byte[]
// -----costTime: 4ms Buffered
// -----costTime: 2ms Buffered + byte[]
  • 缓冲字符流
1
2
3
4
// 创建一个新的缓冲输入流
public BufferedReader(Reader in);
// 创建一个新的缓冲输出流
public BufferedWriter(Writer out);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package java.io;
public class BufferedReader extends Reader {
//缓冲输入流,又称为包装流,使用时需要传入基本输入字符流对象
public BufferedReader(Reader in) {
}
//size,代表设置读取缓冲大小
public BufferedReader(Reader in, int size) {
}
}
public class BufferedWriter extends Writer {
//缓冲输出流,又称为包装流,使用时需要传入基本输出字符流对象
public BufferedWriter(Writer out) {
}
//size,代表设置写入缓冲大小
public BufferedWriter(Writer out, int size) {
}
}

跟缓冲字节流差不多滴,此处省略啦~

缓冲字符流特殊使用

针对文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package java.io;
public class BufferedReader extends Reader {
//一次读取一行字符串,遇到换行符为止,算是一行
//该方法的返回值,【不会】包含回车换行(\r\n)
public String readLine() throws IOException {
//...
}
}
public class BufferedWriter extends Writer {
//写入一行
public void write(String line) throws IOException {
//...
}
}
  • 如果下一行是空行,readLine方法返回空字符串,也就是String line = “”
  • 如果没有下一行数据了,readLine方法返回null

案例描述:结合字符缓存流读一行特性,完成文件的拷贝

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
public class Test025_BufferedCopy {
public static void main(String[] args) throws IOException {
BufferedReader br = null;
BufferedWriter bw = null;
try {
File f1 = new File("src/dir/背影.txt");
File f2 = new File("src/dir/背影Copy.txt");
br = new BufferedReader(new FileReader(f1));
bw = new BufferedWriter(new FileWriter(f2));
String line = null;
while ((line = br.readLine()) != null) {
// bw.write(line);
// bw.write(line + "\n"); // 不同的操作系统换行可能不一样
bw.write(line);
bw.newLine(); // 插入换行符,会根据系统自动判定
}

bw.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (bw != null) {
bw.close();
}
if (br != null) {
br.close();
}
}
System.out.println("success copy!");
}
}

一般BufferedReader和PrintWriter连用,原因PrintWriter自带换行功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package java.io;
public class PrintWriter extends Writer {
/*println方法重载*/
public void println() {
newLine();//换行功能
}
public void println(boolean x) {
synchronized (lock) {
print(x);
println();
}
}
public void println(String x) {
synchronized (lock) {
print(x);
println();
}
}
//省略...
}

案例如下:

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
public class Test0205_PrintWriter {
public static void main(String[] args) throws IOException {
BufferedReader br = null;
PrintWriter pw = null;
try {
File f1 = new File("src/dir/背影.txt");
File f2 = new File("src/dir/背影Copy.txt");
br = new BufferedReader(new FileReader(f1));
pw = new PrintWriter(new FileWriter(f2));
String line = null;
while ((line = br.readLine()) != null) {
// pw.write(line);
pw.println(line);
}

pw.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (pw != null) {
pw.close();
}
if (br != null) {
br.close();
}
}
System.out.println("success copy!");
}
}

字符数组流:

使用字符流,从字符数组中读取数据,以及向字符数组中写数据。

  • java.io.CharArrayReader 负责从字符数组中读取数据
  • java.io.CharArrayWriter 负责把数据写入到字符数组中
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
public class Test029_CharArray {
public static void main(String[] args) throws IOException {
Reader in = null;
Writer out = null;
char[] arr = "hello China I love you".toCharArray();
in = new CharArrayReader(arr);
out = new CharArrayWriter();

int len = -1;
char[] cbuf = new char[1024];
try {
// 返回本次一共读了多少个字节
in.read(cbuf);
// 将数据写入到了 out对象中的属性里面,该属性是一个字符数组
out.write(cbuf, 0, len);
out.flush();

// CharArrayWriter中的toCharArray方法,可以将写入到out对象中的数据返回
char[] chars = ((CharArrayWriter) out).toCharArray();
System.out.println(Arrays.toString(chars));

} catch (IOException e) {
e.printStackTrace();
} finally {
out.close();
in.close();
}
}
}

观察 CharArrayWriter 源码,查看 toCharArray 方法是如何实现的

1
2
3
4
5
6
7
8
9
10
/**
* Returns a copy of the input data.
*
* @return an array of chars copied from the input data.
*/
public char toCharArray()[] {
synchronized (lock) {
return Arrays.copyOf(buf, count); // 直接是复制一份出来
}
}

转换流

1)字符编码和字符集

计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英 文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符 存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照某种规 则解析显示出来,称为解码 。比如说,按照A规则存储,同样按照A规则解析,那么就能显示正确的文本符号。反之,按照A规则存储,再按照B规则解析,就 会导致乱码现象。

编码:字符(能看懂的)–字节(看不懂的)

解码:字节(看不懂的)–>字符(能看懂的)

  • 字符编码 Character Encoding : 就是一套自然语言的字符与二进制数之间的对应规则。
  • 字符集 Charset :也叫编码表。是一个系统支持的所有字符的集合,包括 各国家文字、标点符号、图形符号、数字等。
    • 编码表:生活中文字和计算机中二进制的对应规则

计算机要准确的存储和识别各种字符集符号,需要进行字符编码,一套字符集必 然至少有一套字符编码。常见字符集有ASCII字符集、GBK字符集、Unicode字符 集等。

可见,当指定了编码,它所对应的字符集自然就指定了,所以编码才是我们最终要关心的。

  • ASCII字符集 :
    • ASCII(American Standard Code for Information Interchange,美国信息交 换标准代码)是基于拉丁字母的一套电脑编码系统,用于显示现代英 语,主要包括控制字符(回车键、退格、换行键等)和可显示字符(英 文大小写字符、阿拉伯数字和西文符号)。
    • 基本的ASCII字符集,使用7位(bits)表示一个字符,共128字符。ASCII 的扩展字符集使用8位(bits)表示一个字符,共256字符,方便支持欧洲 常用字符。
  • ISO-8859-1字符集:
    • 拉丁码表,别名Latin-1,用于显示欧洲使用的语言,包括荷兰、丹麦、 德语、意大利语、西班牙语等。
    • ISO-8859-1使用单字节编码,兼容ASCII编码。
  • GBxxx字符集:
    • GB就是国标的意思,是为了显示中文而设计的一套字符集。
    • GB2312:简体中文码表。一个小于127的字符的意义与原来相同。但两 个大于127的字符连在一起时,就表示一个汉字,这样大约可以组合了包 含7000多个简体汉字,此外数学符号、罗马希腊的字母、日文的假名们 都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了 两个字节长的编码,这就是常说的”全角“字符,而原来在127号以下的那 些就叫”半角“字符了。
    • GBK:最常用的中文码表。是在GB2312标准基础上的扩展规范,使用了 双字节编码方案,共收录了21003个汉字,完全兼容GB2312标准,同时支 持繁体汉字以及日韩汉字等。
    • GB18030:最新的中文码表。收录汉字70244个,采用多字节编码,每个 字可以由1个、2个或4个字节组成。支持中国国内少数民族的文字,同时 支持繁体汉字以及日韩汉字等。
  • Unicode字符集 :
    • Unicode编码系统为表达任意语言的任意字符而设计,是业界的一种标 准,也称为统一码、标准万国码。
    • 它最多使用4个字节的数字来表达每个字母、符号,或者文字。有三种编 码方案,UTF-8、UTF-16和UTF-32。最为常用的UTF-8编码。
    • UTF-8编码,可以用来表示Unicode标准中任何字符,它是电子邮件、网 页及其他存储或传送文字的应用中,优先采用的编码。互联网工程工作 小组(IETF)要求所有互联网协议都必须支持UTF-8编码。所以,我们开 发Web应用,也要使用UTF-8编码。它使用一至四个字节为每个字符编 码,编码规则:
      • 128个US-ASCII字符,只需一个字节编码。
      • 拉丁文等字符,需要二个字节编码。
      • 大部分常用字(含中文),使用三个字节编码。
      • 其他极少使用的Unicode辅助字符,使用四字节编码。

2)编码引出的问题

使用 FileReader 读取项目中的文本文件。由于软件的设置,都是默认的 UTF-8 编码,所以没有任何问题。但是,当读取Windows系统中创建的文本文件 时,由于Windows系统的默认是GBK编码,就会出现乱码。

为方便测试,首先将文件另存为时选择编码为gbk编码,然后代码默认去读取

1
2
3
4
5
6
7
8
9
10
11
public class Test027_GbkToUtf8 {
public static void main(String[] args) throws IOException {
FileReader fileReader = new FileReader("src/dir/a.txt");
int read;
while ((read = fileReader.read()) != -1) {
System.out.print((char) read);
}
fileReader.close();
}
}
// � @% a hello,中国

字节字符转换

  • java.io.OutputStreamWriter, 可以将字节输出流—>为字符输出流,并指定编码
  • java.io.InputStreamReader, 可以将字节输入流—->为字符输入流,并指定编码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package java.io;
public class InputStreamReader extends Reader {
//使用默认编码转换
public InputStreamReader(InputStream in) {
//略...
}
//使用指定编码转换
public InputStreamReader(InputStream in, String charsetName){
//略...
}
//省略...
}
public class OutputStreamWriter extends Writer {
//使用默认编码转换
public OutputStreamWriter(OutputStream in) {
}
//使用指定编码转换
public OutputStreamWriter(OutputStream in, String charsetName){
}
}

可以看出,它的构造器参数,要求传入一个需要转换的字节输出流,和一 指定的字符编码

指定编码读取案例:

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 class Test027_InputStreamReader {
public static void main(String[] args) throws IOException {
// 1.声明流
BufferedReader in = null;
// 转换流,同时也是一个字符流
InputStreamReader isr = null;
// 2.创建流
// 创建文件对象
File file = new File("src/dir/File_GBK.txt");
// 将字节流转换为字符流(编码选择GBK或者UTF-8试试)
//注意:转换的要和文件本身编码一致
isr = new InputStreamReader(new FileInputStream(file), "UTF-8");
// “包裹”转换流,增强这个字符流的功能,可以一次读出一行字
// 注意,转换流同时也是一个字符流
in = new BufferedReader(isr);
String line = null;
while ((line = in.readLine()) != null) {
System.out.println(line);
}
// 关闭流
isr.close();
in.close();
}
}
// 你好,中国

指定编码写入案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test027_OutputStreamWriter {
public static void main(String[] args) throws IOException {
PrintWriter out = null;
OutputStreamWriter osw = null;
File file = new File("src/dir/File_GBK.txt");
osw = new OutputStreamWriter(new FileOutputStream(file), "UTF-8");
out = new PrintWriter(osw);
out.println("你好,程序员!");
out.flush();
osw.close();
out.close();
}
}

可以把编码改为GBK,运行查看文件的内容,观察有什么不一样的地方

数据流

DataOutputStream 负责把指定类型数据,转化为字节并写出去

DataInputStream 负责把读取到的若干个字节,转化为指定类型的数据

案例描述:

完成对应基本数据类型的文件存储,然后按照对应基本数据类型读取文件,需要观 察文件内容格式是否和平常文本直接存入数据是否效果一致,原因为何?

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 class Test028_DataStream {
public static void main(String[] args) throws IOException {
DataInputStream in = null;
DataOutputStream out = null;
// 基本数据以及字符串写入文件
File file = new File("src/dir/a.txt");
// "包裹"文件字节输出流,增强数据写出共嗯
out = new DataOutputStream(new FileOutputStream(file));
out.writeLong(1000L);
out.writeInt(5);
out.writeDouble(10.5D);
out.writeChar('a');
out.writeUTF("hello,中国");
out.flush();
// 基本数据以及字符串读取文件
// “包裹”文件字节输入流,增强读取数据功能
in = new DataInputStream(new FileInputStream(file));
// 注意,数据读出来的顺序要,和之前写进去的顺序一致
System.out.println(in.readLong());
System.out.println(in.readInt());
System.out.println(in.readDouble());
System.out.println(in.readChar());
System.out.println(in.readUTF());
}
}

对象流

java.io.ObjectOutputStream, 将Java对象转换为字节序列,并输出到内存、文件、网络等地方(序列化)

java.io.ObjectInputStream,从某个地方读取出对象的字节序列,并生成对应的对象。(反序列化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package java.io;
public class ObjectOutputStream extends OutputStream implements ObjectOutput,
ObjectStreamConstants
{
public ObjectOutputStream(OutputStream out) throws IOException {
}
//序列化方法
public final void writeObject(Object obj) throws IOException {
}
}
public class ObjectInputStream extends InputStream implements ObjectInput,
ObjectStreamConstants
{
public ObjectInputStream(InputStream in) throws IOException {
}
//反序列化方法
public final Object readObject()throws IOException, ClassNotFoundException{
}
}

序列化机制

Java 提供了一种对象序列化的机制,可以将对象和字节序列之间进行转换:

  • 序列化

程序中,可以用一个字节序列来表示一个对象,该字节序列包含了对象的类型、对象中的数据等。如果这个字节序列写出到文件中,就相当于在文件中保存了这个对象的信息。

  • 反序列化

相反的过程,从文件中将这个字节序列读取回来,在内存中重新生成这个对象,对象的类型、对象中的数据等,都和之前的那个对象保持一致。(注意,这时候的对象和之前的对象,内存地址可能是不同的

完成对象的序列化和反序列化,就需要用到对象流了

序列化要求

在java中,并非所有对象都可以进行序列化和反序列化,而是只有实现了指定接口的对象才可以进行

java.io.Serializable 接口

1
2
3
package java.io;
public interface Serializable {
}

接口中没有抽象方法,这只是一个“标识”接口,实现它的的对象才可以进行 序列化和反序列化操作

案例描述:将学生对象保存在文件中,并读取出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test026_ObjectStream {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream ois = null;
ObjectOutputStream oos = null;
oos = new ObjectOutputStream(new FileOutputStream("src/dir/stu.txt"));
Student stu = new Student("tom", "123");
oos.writeObject(stu);
// 读取文件中对象
ois = new ObjectInputStream(new FileInputStream("src/dir/stu.txt"));
Object o = ois.readObject();
if (o instanceof Student) {
Student stu1 = (Student) o;
System.out.println(stu1);
}

oos.close();
ois.close();
}
}
1
2
3
4
//注意,必须序列化
class Student implements Serializable {
// ...
}

注意,如果不实现这个接口的,将来序列化操作会报错

transient关键字

java中的关键字 transient ,可以修饰类中的属性,它是让对象在进行序列化 的时候,忽略掉指定的属性值

transient 的单词含义就是短暂的、转瞬即逝。

1
2
3
4
5
public class Student implements Serializable {
private String name;
private transient int age; // 用在序列化的时候 序列化后 age = 0 相当于值被消掉了
//...
}

随机访问流

java.io.RandomAccessFile 是JavaAPI中通提供的对文件进行随机访问的流

1
2
3
4
5
6
7
8
9
10
11
12
package java.io;
public class RandomAccessFile implements DataOutput,
DataInput, Closeable {
//传入文件以及读写方式
public RandomAccessFile(File file, String mode) throws
FileNotFoundException{
}
//跳过pos字节
public void seek(long pos) throws IOException {
}
}

可以看出,它并没有继承之前介绍到的那四个抽象父类型

之前使用的每一个流,要么是读数据的,要么是写数据的,而这个随机访问流,它的对象即可读文件,又可写文件,同时它还可以任意定位到文件的某一个位置进行读或者写操作

  • 对象即可读也可以写
  • 随机定位文件中的任意字节位置进行读或写,并且可以前后反复定位

创建该类的对象时,需要指定要操作的文件和操作的模式:

  • “r” 模式,以只读方式来打开指定文件夹。如果试图对该RandomAccessFile执 行写入方法,都将抛出IOException异常。
  • “rw” 模式,以读写方式打开指定文件。如果该文件尚不存在,则试图创建该 文件。
  • “rws” 模式,以读写方式打开指定文件。相对于”rw” 模式,还要求对文件内 容或元数据的每个更新都同步写入到底层设备。
  • “rwd” 默认,以读写方式打开指定文件。相对于”rw” 模式,还要求对文件内 容每个更新都同步写入到底层设备。

案例描述: 将a.txt文件内容:hello peter haha,使用随机访问流进行内容替换,完成后文件 内容:hello parke23

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
public class Test028_RandomAccessFile {
public static void main(String[] args) throws IOException {
RandomAccessFile ras = null;
File file = new File("src/dir/a.txt");
// 文件中要替换数据的位置
int replacePos = 6;
// 文件中要插入的内容
String replaceContent = "parker";
// 设置randomAccessFile为读写模式
ras = new RandomAccessFile(file, "rw");
byte[] buf = new byte[1024];
int len = -1;
// randomAccessFile定位到要替换数据的位置,准备去写要替换的内容
ras.seek(replacePos);
// 在指定位置,写入需要替换的内容,覆盖原来此位置上的内容
ras.write(replaceContent.getBytes());
// 回到开头,进行读数据
ras.seek(0);
while((len = ras.read(buf)) != -1) {
// System.out.println(new String(buf, 0, len));
System.out.write(buf, 0 ,len);
}
System.out.flush();
// 关闭资源
ras.close();
}
}
// hello parker23

Properties类

这个类不属于流,本质上是一个键值对集合,Hashtable的子类,在实际开发中我们用来处理配置文件

可以结合固定文本结构获取数据 创建文件db.properties文件,内容如下

1
2
3
4
driver=com.mysql.cj.jdbc.Driver
url=127.0.0.1
username=lwsj
password=lwsj

案例描述:通过Properties类加载db.properties文件,获取文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test028_Properties {
public static void main(String[] args) throws IOException {
Properties p = new Properties();
// 流加载文件
InputStream in = new FileInputStream("src/dir/db.properties");
// 获取流对象
p.load(in);
String driver = p.getProperty("driver");
String url = p.getProperty("url");
String username = p.getProperty("username");
String password = p.getProperty("password");
System.out.println(driver + "--" + url + "--" + username + "--" + password);
}
}
// com.mysql.cj.jdbc.Driver--127.0.0.1:8080--lwsj--lwsj

❤️❤️❤️忙碌的敲代码也不要忘了浪漫鸭!

欲穷千里目,更上一层楼💪