作为资深Java开发者,相信大家对Java的I/O体系都不会陌生。毕竟,I/O操作无处不在,是我们与外部世界进行交互的关键桥梁。今天,就让我带大家领略一下Java I/O体系的精髓所在!
我们将从装饰者模式的设计理念出发,深入分析InputStream/OutputStream和Reader/Writer这两大流体系的工作机制。最后,通过详细的代码示例,让大家亲身体会Java I/O操作的魅力所在。话不多说,让我们直接开始今天的分享吧!
一、装饰者模式的应用
作为23种设计模式之一,装饰者模式在Java I/O体系中得到了大量应用。
它的核心思想是:在不改变原有对象的基础上,动态地给对象添加一些附加职责。这种做法比继承更有弹性,体现了"对修改关闭,对扩展开放"的设计原则。
Java I/O体系之所以具有极高的灵活性和可扩展性,正是因为广泛使用了装饰者模式。
以FileInputStream为例,它只负责读取文件数据流的基本功能,而读取指定字节范围内的数据、支持自动按行读取等高级功能,则由FileInputStream的"装饰类"来提供。
// 创建文件输入流
FileInputStream fis = new FileInputStream("data.txt");
// 装饰为BufferedInputStream以及DataInputStream
BufferedInputStream bis = new BufferedInputStream(fis);
DataInputStream dis = new DataInputStream(bis);// 读取基本数据类型
int num = dis.readInt();
boolean flag = dis.readBoolean();
// ...
1、InputStream/OutputStream详解
(1)、InputStream
InputStream
是用于从不同数据源读取字节的抽象基类。它提供了一个基本的接口,用于读取字节数据。InputStream
的子类包括:
-
FileInputStream
:用于读取文件中的字节。 -
ByteArrayInputStream
:用于从字节数组中读取字节。 -
PipedInputStream
:用于从管道中读取字节。
(2)、OutputStream
OutputStream
是用于向不同数据源写入字节的抽象基类。它提供了一个基本的接口,用于写入字节数据。OutputStream
的子类包括:
FileOutputStream
:用于向文件写入字节。ByteArrayOutputStream
:用于向字节数组写入字节。PipedOutputStream
:用于向管道写入字节。
(3)、使用 InputStream 和 OutputStream 的示例
下面演示如何使用 FileInputStream
和 FileOutputStream
来读取和写入文件:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;public class StreamExample {public static void main(String[] args) {// 定义要读取的文件路径和要写入的文件路径String sourceFilePath = "source.txt";String destinationFilePath = "destination.txt";try (FileInputStream fis = new FileInputStream(sourceFilePath);FileOutputStream fos = new FileOutputStream(destinationFilePath)) {// 定义一个字节数组来临时存储读取的数据byte[] buffer = new byte[1024];int bytesRead;// 读取源文件并写入目标文件while ((bytesRead = fis.read(buffer)) != -1) {fos.write(buffer, 0, bytesRead);}System.out.println("文件复制完成。");} catch (IOException e) {e.printStackTrace();}}
}
在这个示例中,我们创建了两个文件流对象:FileInputStream
用于读取 source.txt
文件,而 FileOutputStream
用于向 destination.txt
文件写入数据。
我们使用 read()
方法从 FileInputStream
中读取数据到一个字节数组 buffer
中,并使用 write()
方法将这些数据写入到 FileOutputStream
中。
注意,我们使用了 try-with-resources 语句来自动关闭流,这是处理 InputStream
和 OutputStream
的推荐方式,因为它可以确保即使发生异常也会正确关闭资源。
这个示例只是一个简单的例子,InputStream
和 OutputStream
的子类可以用于更复杂的场景,如网络通信、数据压缩、加密等。
Java提供了许多InputStream/OutputStream的具体实现类,用于满足不同场景下的读写需求。
例如ByteArrayInputStream可读写内存字节数组、ObjectOutputStream可直接序列化Java对象等。它们均遵循装饰者模式,可以相互装饰,形成链式调用。
2、ByteArrayInputStream/ObjectOutputStream详解
ByteArrayInputStream
和 ObjectOutputStream
是 Java 中用于处理数据流的类。ByteArrayInputStream
允许你读取内存中的字节数组,而 ObjectOutputStream
用于将 Java 对象序列化到输出流中。
下面是分别演示如何使用 ByteArrayInputStream
和 ObjectOutputStream
。
(1)、使用 ByteArrayInputStream 读取内存中的字节数组
这个示例展示了如何将一个字符串转换为字节数组,并使用 ByteArrayInputStream
来读取这个字节数组。
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;public class ByteArrayInputStreamExample {public static void main(String[] args) {try {// 创建一个字符串String originalString = "Hello, this is a test string!";// 将字符串转换为字节数组byte[] byteArray = originalString.getBytes();// 创建 ByteArrayInputStream 对象ByteArrayInputStream inputStream = new ByteArrayInputStream(byteArray);// 读取字节数组int data = 0;while ((data = inputStream.read()) != -1) {System.out.print((char) data);}System.out.println("\nFinished reading.");} catch (IOException e) {e.printStackTrace();}}
}
在这个示例中,我们创建了一个 ByteArrayInputStream
来读取内存中的字节数组,并打印出每个字节对应的字符。
(2)、使用 ObjectOutputStream 序列化 Java 对象
这个示例展示了如何使用 ObjectOutputStream
来序列化一个简单的 Java 对象。
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;public class ObjectOutputStreamExample {public static void main(String[] args) {try {// 创建一个简单的 Java 对象MyObject myObject = new MyObject("Kimi", 2024);// 创建 FileOutputStream 对象FileOutputStream fileOutputStream = new FileOutputStream("myObject.ser");// 创建 ObjectOutputStream 对象ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);// 序列化对象objectOutputStream.writeObject(myObject);// 清理资源objectOutputStream.close();fileOutputStream.close();System.out.println("Object has been serialized.");} catch (IOException e) {e.printStackTrace();}}
}class MyObject implements java.io.Serializable {private static final long serialVersionUID = 1L;private String name;private int year;public MyObject(String name, int year) {this.name = name;this.year = year;}// Getters and setterspublic String getName() {return name;}public void setName(String name) {this.name = name;}public int getYear() {return year;}public void setYear(int year) {this.year = year;}
}
在这个示例中,我们创建了一个 MyObject
类,它实现了 Serializable
接口,这意味着它可以被序列化。我们使用 ObjectOutputStream
将 MyObject
实例序列化到一个文件中。
3、Reader/Writer详解
(1)、Reader
Reader
是用于读取字符流的抽象基类。它使用 char
类型的缓冲区,并且可以指定字符编码。Reader
的子类包括:
-
FileReader
:用于读取字符文件。 -
CharArrayReader
:用于从字符数组中读取字符。 -
StringReader
:用于从字符串中读取字符。
(2)、Writer
Writer
是用于写入字符流的抽象基类。它使用 char
类型的缓冲区,并且可以指定字符编码。Writer
的子类包括:
-
FileWriter
:用于写入字符到文件。 -
CharArrayWriter
:用于向字符数组写入字符。 -
StringWriter
:用于向字符串写入字符。
(3)、使用 Reader 和 Writer 的示例
下面演示如何使用 FileReader
和 FileWriter
来读取和写入文本文件:
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;public class ReaderWriterExample {public static void main(String[] args) {// 定义要读取的文件路径和要写入的文件路径String sourceFilePath = "source.txt";String destinationFilePath = "destination.txt";try (FileReader fr = new FileReader(sourceFilePath);FileWriter fw = new FileWriter(destinationFilePath)) {char[] buffer = new char[1024];int charsRead;// 读取源文件并写入目标文件while ((charsRead = fr.read(buffer)) != -1) {fw.write(buffer, 0, charsRead);}System.out.println("文件复制完成。");} catch (IOException e) {e.printStackTrace();}}
}
在这个示例中,我们创建了两个字符流对象:FileReader
用于读取 source.txt
文件中的字符,而 FileWriter
用于向 destination.txt
文件写入字符。
我们使用 read()
方法从 FileReader
中读取字符到一个字符数组 buffer
中,并使用 write()
方法将这些字符写入到 FileWriter
中。
注意,我们使用了 try-with-resources 语句来自动关闭流,这是处理 Reader
和 Writer
的推荐方式,因为它可以确保即使发生异常也会正确关闭资源。
这个示例只是一个简单的例子,Reader
和 Writer
的子类可以用于更复杂的场景,如网络通信、字符编码转换、国际化等。
与InputStream/OutputStream类似, Java也为Reader/Writer提供了多种具体实现类,如StringReader可读写字符串、CharArrayWriter可写出字符数组等。同时,它们也使用了装饰者模式,支持无缝组合和扩展。
4、StringReader/CharArrayWriter详解
StringReader
和 CharArrayWriter
是 Java 中的两种字符流类,分别用于从字符串读取字符和向字符数组写入字符。
下面是演示使用 StringReader
和 CharArrayWriter
的示例。
(1)、使用 StringReader 读取字符串
StringReader
类是 java.io
包中的一个类,它允许从字符串中读取字符流。
import java.io.StringReader;
import java.io.IOException;public class StringReaderExample {public static void main(String[] args) {String text = "Hello, World!";StringReader stringReader = new StringReader(text);try {int i;while ((i = stringReader.read()) != -1) {// 打印每个字符System.out.print((char) i);}} catch (IOException e) {e.printStackTrace();} finally {// 总是关闭流stringReader.close();}}
}
(2)、使用 CharArrayWriter 写入字符数组
CharArrayWriter
类是 java.io
包中的一个类,它允许将字符写入内部字符数组。
import java.io.IOException;
import java.io.CharArrayWriter;public class CharArrayWriterExample {public static void main(String[] args) {CharArrayWriter charArrayWriter = new CharArrayWriter();try {// 写入字符串到CharArrayWritercharArrayWriter.write("Hello, ");charArrayWriter.write("World!");// 将CharArrayWriter的内容转换为字符串String result = charArrayWriter.toString();System.out.println(result);// 获取字符数组char[] chars = charArrayWriter.toCharArray();System.out.println("字符数组内容:");for (char c : chars) {System.out.print(c);}} catch (IOException e) {e.printStackTrace();}}
}
在 StringReaderExample
示例中,我们创建了一个 StringReader
对象,并使用它来读取字符串 text
中的字符。我们通过循环调用 read()
方法来逐个字符地读取,并打印每个字符。
在 CharArrayWriterExample
示例中,我们创建了一个 CharArrayWriter
对象,并使用它来写入字符串。我们调用 write()
方法来写入字符。之后,我们使用 toString()
方法将 CharArrayWriter
中的内容转换为字符串,并打印出来。我们还可以使用 toCharArray()
方法获取内部字符数组,并打印数组中的字符。
请注意,尽管 CharArrayWriter
可以写入字符,但它通常用于收集字符,然后可以通过 toString()
或 .toCharArray()
方法一次性获取所有字符。而 StringReader
则用于从字符串中逐个读取字符,适合于字符流的读取操作。
二、字节流与字符流的区别
字节流和字符流是 Java I/O 流中两种不同类型的流,它们在处理数据时有不同的特点和用途。
1、字节流(Byte Streams)
-
字节流主要用于处理二进制数据,如图片、视频、可执行文件等。
-
字节流可以处理任何类型的数据,因为它只读取和写入字节。
-
字节流不关心字符编码,因此它不涉及字符集转换。
-
字节流的基类是
InputStream
和OutputStream
。
2、字符流(Character Streams)
-
字符流主要用于处理字符数据,即文本数据。
-
字符流使用字符集(如 UTF-8, GBK 等)来编码和解码字符。
-
字符流可以自动处理字符编码转换,适合文本文件的读写。
-
字符流的基类是
Reader
和Writer
。
3、字节流与字符流的区别:
-
数据类型:字节流处理原始字节数据,字符流处理字符数据。
-
编码:字节流不涉及编码转换,字符流涉及字符编码和解码。
-
用途:字节流适合二进制文件,字符流适合文本文件。
-
效率:对于文本数据,字符流可能因为编码转换而效率较低;对于二进制数据,字节流更高效。
4、字节流示例:复制文件
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;public class ByteStreamExample {public static void main(String[] args) {String sourceFilePath = "source.bin";String destinationFilePath = "destination.bin";try (FileInputStream fis = new FileInputStream(sourceFilePath);FileOutputStream fos = new FileOutputStream(destinationFilePath)) {int byteRead;while ((byteRead = fis.read()) != -1) {fos.write(byteRead);}} catch (IOException e) {e.printStackTrace();}}
}
这个示例展示了如何使用字节流来复制一个二进制文件。
5、字符流示例:复制文本文件
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;public class CharStreamExample {public static void main(String[] args) {String sourceFilePath = "source.txt";String destinationFilePath = "destination.txt";try (FileReader fr = new FileReader(sourceFilePath);FileWriter fw = new FileWriter(destinationFilePath)) {char[] buffer = new char[1024];int charsRead;while ((charsRead = fr.read(buffer)) != -1) {fw.write(buffer, 0, charsRead);}} catch (IOException e) {e.printStackTrace();}}
}
这个示例展示了如何使用字符流来复制一个文本文件。
在这两个示例中,我们使用了 try-with-resources 语句来自动管理资源,确保文件流在使用后能够被正确关闭。字节流示例中,我们逐字节读取和写入数据;而在字符流示例中,我们使用字符数组作为缓冲区,逐字符读取和写入数据。字符流示例中还隐含了字符编码的处理,这是字符流的一个优势。
总的来说,字节流适合用于处理二进制数据,如图片、视频等媒体文件。而字符流则更擅长处理纯文本数据。在具体使用时,如果明确知道只处理纯文本数据,则优先考虑使用Reader/Writer,否则使用InputStream/OutputStream。
不过,对于纯文本数据,字节流和字符流二者是可以相互转换的。InputStreamReader可将字节流转换为字符流,而OutputStreamWriter则相反。它们的作用是编码/解码文本数据,以实现文本数据和纯字节之间的相互转换。
三、总结
通过上述讲解,相信大家已经对Java I/O体系有了更深入的理解。我们首先剖析了装饰者模式在其中的巧妙应用,随后分别介绍了InputStream/OutputStream和Reader/Writer两大流体系的工作原理。通过示例代码,我们也亲身体会了Java I/O操作的便利之处。
当然,本文只是对Java I/O体系的一个概览。在实际开发过程中,我们还需要注意流的正确使用、异常处理、性能优化等诸多细节问题。而且,在Java 7之后,官方还推出了NIO2的新I/O框架,它提供了更现代化、高效的文件系统操作方式,也是我们后续值得学习的重点。
最后,正如开篇时所说,我将为大家留一个小小的悬念。这里提出一个问题:Java中的I/O流是否真的需要手动关闭?如果不手动关闭,会发生什么?欢迎各位读者朋友思考并在下期与我分享你的答案!更多Java技术分享,敬请继续关注我的博客,下期不见不散!