文章目录
- 一. 文件概述
- 二. 文件类File
- 1. 构造方法和常用普通方法
- 2. 4种获取路径方法的比较
- 三. I/O流
- 1. 流的概念
- 2. FileReader和FileWriter
- 3. FileInputStream和FileOutputStream
- 4. 带有缓冲功能的I/O流(处理流)
- 关闭流资源的另一种方法(推荐)
- 5. 对象流—ObjectInputStream 和 ObjectOutputStream(处理流)
- 6. 转换流—InputStreamReader 和 OutputStreamWriter(处理流)
- 7. System.in 和 System.out
- 8. Scanner 和 PrintWriter、PrintStream
📄前言:
本文是对个人学习过程中文件操作、常见I/O流的类型和使用的总结。
一. 文件概述
什么是文件?
文件是计算机中存储数据的一种方式,它可以用来保存各种类型的信息,如文本、图像、音频和视频等。文件通常由文件名和扩展名组成,扩展名(后缀名)表示文件的类型或格式。
在Windows系统中不同的文件后缀通常代表着不同的文件类型和默认打开方式,如 docx为WPS中word文档的后缀,jpg或png为常见的图片文件后缀,exe通过代表一个可执行程序等;但是Unix、Linux等操作系统上则没有这样精确的分类。
文件的分类:
文件总体上可以分为 文本文件(ASCII码文件)和二进制文件。文本文件以字符为基本单位,最常见的文本文件为 txt文件;二进制文件由0、1组成,以字节为基本单位,常见的二进制文件有图片、视频、音频等。
文件路径:
在文件系统中,通过文件路径可以定位到一个唯一的文件,文件路径可以分为绝对路径和相对路径。
绝对路径指从根目录开始到目标文件的完整路径。在Windows系统中文件的根目录可能是C:\,在Linux系统中根目录为/。
相对路径指从当前工作目录到目标文件的路径。在相对路径中通常用 . 表示当前目录,用 .. 表示上一级目录。(注意:在IDEA的Java项目中,如果项目的名称为Test,其绝对路径为D:/javacode/Test,则在创建 File对象时,工作目录在Test文件夹下,即D:/javacode/Test 或 D:/javacode/Test/.)
文件分隔符:
在一个文件路径中,不同的目录之间或目录和文件间用文件分隔符隔开。在Windows系统中,可以用 / 或 \\ 作为文件分隔符;在Linux或Unix操作系统中以 / 作为文件分隔符。
二. 文件类File
1. 构造方法和常用普通方法
在Java程序中,文件用 File类 来表示。File类的构造方法和常用方法如下:
注意:文件路径可使用绝对路径或相对路径。
2. 4种获取路径方法的比较
代码如下(注意:当前项目工作路径为:D:/javacode/Test)
public static void main(String[] args) throws IOException {
File file = new File("../hello.txt");
System.out.println("文件是否存在: " + file.exists()); // 判断文件是否存在
System.out.println("文件名称: " + file.getName()); // 获取文件名称
System.out.println("父目录的路径: " + file.getParent()); // 获取父目录的路径
System.out.println("文件路径: " + file.getPath()); // 获取文件路径
System.out.println("文件的绝对路径: " + file.getAbsolutePath()); // 获取文件的绝对路径
System.out.println("修饰过的绝对路径: " + file.getCanonicalPath()); // 获取文件修饰过的绝对路径
}
程序的运行结果如下:
可以发现:
- 文件是否存在与File类对象的构建无关。
- File 对象父目录的路径与 File 对象创建时写入的路径有关。若使用 File file = new File(“hello.txt”)语句构建对象时,父目录路径为 null。
- getAbsolutePath()获取文件的绝对路径与构建对象时传入参数有关,它使用传入参数来表示文件绝对路径。
- getCanonicalPath()会对文件的绝对路径进行简化,可以更加直观的了解到文件所处的位置。
三. I/O流
1. 流的概念
什么是流?
流是个抽象的概念,是对输入/输出设备的抽象,Java程序中,对于数据的输入/输出操作都是以“流”的方式进行。设备可以是文件,网络,内存等。
什么是 I/O流?
I/O 是 Input/Output 的缩写,因此I/O 流即输入流 / 输出流,用于处理数据的传输,如 读/写 文件、网络通讯等。(注意:输入/输出都是站在内存(程序)的视角看待的,数据从外部设备流向内存称为输入,从内存流向其他设备称为输出)
流的分类有哪些?
- 按操作数据的单位不同可分为:字符流(传输数据以单个字符为基本单位)和字节流(传输数据以1个字节为单位)。所有字符流的顶级抽象父类为: Reader和Writer,字节流为:InputStream和OutputStream。
- 按数据流的流向不同可分为:输入流 和 输出流。所有输入流的顶级抽象父类为:Reader和InputStream,输出流为:Writer和OutputStream。
- 按流的角色不同可分为:节点流(从特定的数据源读写数据,如FileReader、StringReader等)和 处理流/包装流(对已存在的流进行连接和封装,以提供更加强大的数据读写功能,如BufferedReader、BufferedInputStream等)。
注意:所有的流都是一种资源,用完应当关闭,否则会造成文件资源泄露,可能导致文件不能被正常打开!!!
2. FileReader和FileWriter
当我们希望从文本文件数据 读写数据时,可以使用FileReader类 和 FileWriter类。它们的构造方法和常用方法如下:
FileReader类:
部分方法示例如下:
public static void main(String[] args) throws IOException {
Reader fileReader = null;
try {
Reader = new FileReader("D:/test.txt");
int data;
// 每次读取一个字符
while ((data = fileReader.read()) != -1) {
System.out.print((char) data);
}
// 使用数组读取字符
char[] cubf = new char[5];
int readLen = 0;
while ((readLen = fileReader.read(cubf)) != -1) {
for (int i = 0; i < readLen; i++) {
System.out.print(cubf[i]);
}
}
} finally {
// 关闭文件资源
if(fileReader != null) {
fileReader.close();
}
}
}
两个方法的程序运行结果都如下:
注意:
- 读取单个字符时,返回0到65535( 0x00-0xffff )范围内的整数,即字符的Unicode编码,需要进行类型转换才能得到对应的字符
- 使用数组读取字符时,应取实际读取字符的个数 readLen,否则会错误使用之前的读取数据(新读取的数据会覆盖原来数组内容)
- 流使用完应该关闭
============这是分隔线
FileWriter类:
部分方法示例如下:
public static void main(String[] args) throws IOException {
FileWriter fileWriter = null;
try {
Writer = new FileWriter("D:/test.txt"); // 以覆盖的方式写入数据
// 1. 写入单个字符
fileWriter.write('a');
// 2. 写入一个字符串
fileWriter.write(" hello world");
// 3. 写入一个字符数组的一部分
char[] arr = new char[]{' ', ' ', 'a', 'b', 'c'};
fileWriter.write(arr, 1, 3);
} finally {
// 关闭文件资源
if(fileWriter != null) {
fileWriter.close();
}
}
}
程序运行后文件内容如下:
注意:
- 使用FileWriter写入数据时,数据会被立即写入文件。FileWriter没有显式的缓存机制,但在操作系统层面存在一定程度的缓冲机制,因此调用write()方法并不是每次都立即将数据写入文件,为确保数据被及时写入文件,应调用close()方法关闭流达到这一效果。
- FileWriter调用 flush() 方法实际上使用的是Writer的空实现方法,并不起刷新内存的作用。
- 使用完流对象后应当使用 close()方法关闭资源。
3. FileInputStream和FileOutputStream
当我们需要传输视频、音频、图片等二进制文件时,必须使用字节流读写文件。如果使用字符流读取,文件数据很可能会丢失,造成文件的损坏,导致二进制文件不能被正常使用。
FileInputStream 和 FileOutputStream的构造方法和常用方法如下:
使用字节流读文本文件示例如下:
public static void main(String[] args) throws IOException {
InputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream("d:/test.txt");
int data = -1;
while((data = fileInputStream.read()) != -1) {
System.out.printf("%x ", data);
}
} finally {
if(fileInputStream != null) {
fileInputStream.close();
}
}
}
文本文件内容、程序运行结果及文件内容UTF8编码如下:
============这是分隔线
方法使用示例如下:(读取图片文件数据,将数据写入到另一个文件,完成图片的复制)
public static void main(String[] args) throws IOException {
OutputStream fileOutputStream = null;
InputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream("d:/100.jpg");
fileOutputStream = new FileOutputStream("d:/temp/copy.jpg");
byte[] cubf = new byte[1024];
int readLen = 0;
while((readLen = fileInputStream.read(cubf)) != -1) {
fileOutputStream.write(cubf, 0, readLen);
}
} finally {
if(fileInputStream != null) fileInputStream.close();
if(fileOutputStream != null) fileOutputStream.close();
}
}
程序运行结果如下:
注意:
- 使用FileOutputStream写入数据时,数据会被立即写入文件。FileOutputStream没有显式的缓存机制,但在操作系统层面存在一定程度的缓冲机制,因此调用write()方法并不是每次都立即将数据写入文件,为确保数据被及时写入文件,应调用close()方法关闭流达到这一效果。
- FileOutputStream调用 flush() 方法实际上使用的是OutputStream的空实现方法,并不起刷新内存的作用。
- 使用完流对象后应当使用 close()方法关闭资源。
4. 带有缓冲功能的I/O流(处理流)
缓冲区是什么?
缓冲区相当于一个蓄水池,当降水较少,我们可以从蓄水池中取水,避免了因水压低而取水较慢的问题;当降雨量较多,水压处于正常水平时,我们可以往蓄水池中注水,以备下次使用。(例子可能不太恰当,但缓存区可以简单理解为能够存储数据的蓄水池)
使用带缓冲的处理流的好处:
- 减少实际的物理读/写次数,提高性能:当从输入流读取数据时,可以一次性读取多个字节或字符到内存的缓冲区中,后面每次都从缓存区读取数据,当缓冲区为空后才会重新从数据源读取数据;当往输出流写入数据时,会先把数据写入缓冲区,直到缓冲区为满时再将数据一次性写入输出设备。由于内存比硬盘等其他设备读写效率更高,因此减少I/O次数可以减轻系统开销,提高程序性能。
- 支持按行读/写数据:字符流可以使用 readLine() 方法方便地按行读取数据,使用 newLine() 方法方便地写入换行符。
- 支持标记和重置:一些输入流可以在读取过程中标记当前位置,然后在需要时重新回到该位置。
带有缓存作用可以分为字符流和字节流,且通常以Buffered开头:(以下都为实现类)
字节流:BufferedInputStream 和 BufferedOutputStream
字符流:BufferedReader 和 BufferedWriter
以上缓冲I/O流的类图继承关系大致如下:
可以发现它们都继承自身的顶级父类,其构造方法能够接受所有实现子类。
字符缓冲流和字节缓冲流的主要方法与之前的方法介绍几乎一致,但字符缓冲流可以使用 readLine() 方法方便地按行读取数据,使用 newLine() 方法可以方便地写入换行符;且所有的缓冲输入流都支持mark() 和 reset()方法。(具体的方法介绍和使用可以参照 io包下:Java8 API文档)
注意:带缓冲的输出流可以手动调用 flush() 方法刷新缓冲区,让缓冲区中的数据立即写入输出设备。
关闭流资源的另一种方法(推荐)
由于每次创建流对象都会占用文件描述符表等系统资源,如果长期使用完后 没有关闭或忘记关闭,可能导致文件描述表被占满,后续文件不能被正常打开。
因此,我们可以在每次构建流对象时用 try() 语句将 Java语句包裹起来,这样的话我们就可省去了手动调用 close() 方法的步骤,因为当 try 包裹起来的所有语句执行结束后, close()方法会被自动调用。
具体的使用示例如下:
public static void main(String[] args) throws IOException {
// 1. 将先创建的节点流作为参数传递给处理流,先关闭外层流,后关闭内层流即节点流
try(FileInputStream fileInputStream = new FileInputStream("d:/test.txt")) {
try (BufferedInputStream bufferedReader = new BufferedInputStream(fileInputStream)) {
// 具体的代码
}
}
// 2. 直接创建处理流,关闭最外层流即可,内层流也会被关闭
try(BufferedReader bufferedReader = new BufferedReader(new FileReader("d:/test.txt"))) {
// 具体的代码
}
}
注意:
- 只有实现的 Closeable接口的类才可以使用此方式关闭。
- 当使用多个流进行“装饰”时,只需关闭最外层的流对象即可,在JDK源码中内层流也会被关闭。
5. 对象流—ObjectInputStream 和 ObjectOutputStream(处理流)
在某些情况下我们想保存的可能不是文本信息、音频、图片等数据,而是保存数据的值及类型或一个Java对象的信息时,应该使用ObjectInputStream 和 ObjectOutputStream对数据进行读/写。
其中写入数据时,保存数据的值和类型称为序列化;读取数据时,恢复数据的值和类型称为反序列化。
为什么需要进行序列化和反序列化?
可以发现:只有保存了数据的值和类型,才能保证数据在读取时被正确解析成原来的数据。
能够被序列化和反序列化需要满足以下条件之一:
- 类实现Serializable接口(推荐,该接口为标记接口,无需重写方法)
- 类实现Externalizable接口(不推荐,实现该接口需要重写里面的方法)
===================
ObjectOutputStream 和 ObjectInputStream的继承关系图如下:
ObjectOutputStream 和 ObjectInputStream 的构造方法和常用普通方法(只列出ObjectOutStream,ObjectInputStream同理)如下:
方法使用示例如下:
public static void main(String[] args) throws IOException, ClassNotFoundException {
try(ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("d:/data.dat"))) {
objectOutputStream.writeByte(100);
objectOutputStream.writeInt(10000);
objectOutputStream.writeBoolean(false);
objectOutputStream.writeDouble(3.14);
objectOutputStream.writeUTF("你好 world!");
// 写入一个对象,需实现 Serializable接口,否则会报异常
objectOutputStream.writeObject(new Cat());
}
try(ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("d:/data.dat"))) {
System.out.println(objectInputStream.readByte());
System.out.println(objectInputStream.readInt());
System.out.println(objectInputStream.readBoolean());
System.out.println(objectInputStream.readDouble());
System.out.println(objectInputStream.readUTF());
Cat cat = (Cat) objectInputStream.readObject();
// 打印该对象的类型
System.out.println(cat.getClass());
}
}
程序运行结果如下:
注意:
- 序列化/反序列化的顺序必须一致,否则可能导致数据读取出现错误。
- 序列化和反序列化的对象必须都实现 Serializable接口 ,且对象中属性的类型也需要实现序列化接口。
- 序列化对象时,默认将里面所有的属性都进行序列化,除了 static或transient 修饰的成员。
- 为了提高版本的兼容性,序列化的类中建议添加SerialVersionUID。
- 序列化具备可继承性,某类实现了序列化,其所有子类也默认实现了序列化。
6. 转换流—InputStreamReader 和 OutputStreamWriter(处理流)
当从字节流读取字符数据时(如 FileInputStream、Socket.getInputStream()),由于字符编码有很多种,如果没有对所读取的字节流指定所需的字符编码,可能导致数据被错误地解析,引起乱码的现象。(示例如下)
public static void main(String[] args) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("d:/test.txt"))) {
System.out.println(br.readLine());
}
}
解决以上乱码问题的方法就是:先用字节流作为输入流,再使用 InputStreamReader 将字节流转换为字符流 并指定字符编码集,这样就能将字符数据进行正确解析。(示例如下,可以先了解下面的内容再回头看解决方法)
public static void main(String[] args) throws IOException {
try (InputStreamReader isr = new InputStreamReader(new FileInputStream("d:/test.txt"), "GBK")) {
try (BufferedReader br = new BufferedReader(isr)) {
System.out.println(br.readLine());
}
}
}
====================
InputStreamReader 和 OutputStreamWriter 的常用构造方法如下:
通常情况下,转换流只作为中间的“装饰”流,起到指定字符编码的作用,并不作为最终 读/写 数据的输入/输出流,因此其普通方法不用特意了解,只需知道与之前介绍的的字符输入/输出流大致一样即可。
7. System.in 和 System.out
在Java程序中,我们经常会使用到 new Scanner(System.in) 和 System.out.println() 这两种方式来进行输入和输出,其实System.in 和 System.out 也分别代表着一种流。其中
System.in:标准输入流,流的编译类型为InputStream,运行类型为BufferedInputStream,默认设备为键盘。
System.out:标准输出流,流的编译类型为PrintStream,运行类型为PrintStream,默认设备为显示器(控制台)。
若想改变系统默认的输入/输出设备,可以通过System.setIn() 和 System.setOut() 来改变默认配置,其中System.setIn()需要传入InputStream类的子类作为参数,System.setOut()需要传入PrintStream类作为参数。
使用示例如下:(从键盘输入两次,第一次将输入数据打印到控制台,第二次将数据输出到文件)
public static void main(String[] args) throws FileNotFoundException {
// 将内容输出到控制台
Scanner scanner = new Scanner(System.in);
String input1 = scanner.nextLine();
System.out.println(input1);
// 修改输出位置,将内容输出到文件
System.setOut(new PrintStream("d:test.txt"));
String input2 = scanner.nextLine();
System.out.println(input2);
}
程序运行结果如下:
8. Scanner 和 PrintWriter、PrintStream
当我们需要更加灵活地 读取/写入 数据到 输入/输出设备时,可以使用 Scanner对文件或字节输入流进行“装饰”以提供更加强大的数据读取功能(如nextInt(), next()等),使用 PrintWriter 或 PrintStream 对字符流或字节流进行“装饰”以提供更加强大的文本输出功能(如print()、println()可以输出各种基本类型和字符串的数据)。
其类继承关系图如下:
PrintWriter 和 PrintStream的区别与联系:
- PrintWriter 和 PrintStream都可以输出文本数据,提供了对所有基本数据类型的打印方法(如print()、println() ),都可以在构造方法中指定输出文本数据的字符编码。
- PrintWriter是 Writer的子类,它用来处理文本数据;PrintStream是 OutputStream的子类,当需要输出二进制数据时,更适合用PrintStream。
- PrintWriter通过使用checkError()方法来检查是否发生错误;PrintStream在写入过程中则可能抛出IOException异常,需要在代码中进行相应的异常处理。
Scanner 和 PrintWriter、PrintStream详细的构造方法和普通方法可以参考Java8 API文档
Scanner、PrintWriter的使用示例如下:
public static void main(String[] args) throws IOException {
// 使用Scanner读取文件内容
try (Scanner scanner = new Scanner(new File("d:/test.txt"))) {
System.out.println(scanner.nextBoolean());
System.out.println(scanner.nextInt());
System.out.println(scanner.nextLine());
System.out.println(scanner.nextLine());
}
// 使用PrintWriter向文件写入数据,其中字符集为 GBK
try (PrintWriter printWriter = new PrintWriter("d:/f1.txt", "gbk")) {
printWriter.println(true);
printWriter.println(3.1415926);
printWriter.println("hello 世界");
}
}
以上就是本篇文章的全部内容了,如果这篇文章对你有些许帮助,你的点赞、收藏和评论就是对我最大的支持。
另外,文章的不足之处,也希望你可以给我一点小小的建议,我会努力检查并改进。