揭秘Java I/O体系-从装饰者模式到Reader、Writer流


作为资深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 的示例

下面演示如何使用 FileInputStreamFileOutputStream 来读取和写入文件:

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 语句来自动关闭流,这是处理 InputStreamOutputStream 的推荐方式,因为它可以确保即使发生异常也会正确关闭资源。

这个示例只是一个简单的例子,InputStreamOutputStream 的子类可以用于更复杂的场景,如网络通信、数据压缩、加密等。

Java提供了许多InputStream/OutputStream的具体实现类,用于满足不同场景下的读写需求。

例如ByteArrayInputStream可读写内存字节数组、ObjectOutputStream可直接序列化Java对象等。它们均遵循装饰者模式,可以相互装饰,形成链式调用。


2、ByteArrayInputStream/ObjectOutputStream详解

ByteArrayInputStreamObjectOutputStream 是 Java 中用于处理数据流的类。ByteArrayInputStream 允许你读取内存中的字节数组,而 ObjectOutputStream 用于将 Java 对象序列化到输出流中。


下面是分别演示如何使用 ByteArrayInputStreamObjectOutputStream

(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 setters
    public 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 接口,这意味着它可以被序列化。我们使用 ObjectOutputStreamMyObject 实例序列化到一个文件中。


3、Reader/Writer详解

(1)、Reader

Reader 是用于读取字符流的抽象基类。它使用 char 类型的缓冲区,并且可以指定字符编码。Reader 的子类包括:

  • FileReader:用于读取字符文件。

  • CharArrayReader:用于从字符数组中读取字符。

  • StringReader:用于从字符串中读取字符。


(2)、Writer

Writer 是用于写入字符流的抽象基类。它使用 char 类型的缓冲区,并且可以指定字符编码。Writer 的子类包括:

  • FileWriter:用于写入字符到文件。

  • CharArrayWriter:用于向字符数组写入字符。

  • StringWriter:用于向字符串写入字符。


(3)、使用 Reader 和 Writer 的示例

下面演示如何使用 FileReaderFileWriter 来读取和写入文本文件:

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 语句来自动关闭流,这是处理 ReaderWriter 的推荐方式,因为它可以确保即使发生异常也会正确关闭资源。

这个示例只是一个简单的例子,ReaderWriter 的子类可以用于更复杂的场景,如网络通信、字符编码转换、国际化等。

与InputStream/OutputStream类似, Java也为Reader/Writer提供了多种具体实现类,如StringReader可读写字符串、CharArrayWriter可写出字符数组等。同时,它们也使用了装饰者模式,支持无缝组合和扩展。


4、StringReader/CharArrayWriter详解

StringReaderCharArrayWriter 是 Java 中的两种字符流类,分别用于从字符串读取字符和向字符数组写入字符。


下面是演示使用 StringReaderCharArrayWriter 的示例。


(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 {
            // 写入字符串到CharArrayWriter
            charArrayWriter.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)
  • 字节流主要用于处理二进制数据,如图片、视频、可执行文件等。

  • 字节流可以处理任何类型的数据,因为它只读取和写入字节。

  • 字节流不关心字符编码,因此它不涉及字符集转换。

  • 字节流的基类是 InputStreamOutputStream


2、字符流(Character Streams)
  • 字符流主要用于处理字符数据,即文本数据。

  • 字符流使用字符集(如 UTF-8, GBK 等)来编码和解码字符。

  • 字符流可以自动处理字符编码转换,适合文本文件的读写。

  • 字符流的基类是 ReaderWriter


3、字节流与字符流的区别:
  1. 数据类型:字节流处理原始字节数据,字符流处理字符数据。

  2. 编码:字节流不涉及编码转换,字符流涉及字符编码和解码。

  3. 用途:字节流适合二进制文件,字符流适合文本文件。

  4. 效率:对于文本数据,字符流可能因为编码转换而效率较低;对于二进制数据,字节流更高效。


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技术分享,敬请继续关注我的博客,下期不见不散!


  • 33
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

w风雨无阻w

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值