Java IO 学习笔记(二)


Java IO 学习笔记(二)

一、文件流

与文件系统的交互是程序的重中之重。在 Java IO 中,我们把与文件系统交互的流称为文件流,它们分别是 FileInputStream 与 FileOutputStream。

在文件流的学习中,有两个问题需要注意:

1. 从广义上来讲,计算机文件即为二进制文件。无论是纯文本文件还是图像文件等,本质上都是以二进制的形式存储在设备中的。
2. Java 的文件流是字节流,只能从文件中读取字节,也只能向文件中写入字节。


二、File

File 类,即为文件类,它以抽象的方式代表文件名和目录路径名,主要用于文件和目录的创建、文件的查找和文件的删除等。

无论是 FileInputStream 还是 FileOutputStream,都依赖于 File 类。它们都需要通过 File 对象,来对磁盘中实际存在的文件进行操作。

本文重点要介绍的是 IO 操作,因此对于 File 类不做深入讨论,如有兴趣进一步了解,可自行阅读源码或者阅读以下基础教程:Java File 类

三、FileInputStream

FileInputStream 是 Java 文件流中的输入流,继承自 InputStream,可以从本地文件系统中读取字节数据。

3.1 类图

image

3.2 构造方法

如上图所示,FileInputStream 有 3 种构造方法:

方法名 方法详解
FileInputStream(String name) 通过打开一个到实际文件的连接来创建一个 FileInputStream 对象,该文件通过文件系统中的路径名 name 指定。假如文件不存在,会抛出 FileNotFoundException 异常。
FileInputStream(File file) 通过打开一个到实际文件的连接来创建一个 FileInputStream 对象,该文件通过文件系统中的 File 对象 file 指定。假如文件不存在,会抛出 FileNotFoundException 异常。
FileInputStream(FileDescriptor fdObj) 通过文件描述符 fdObj 创建一个 FileInputStream 对象,该文件描述符表示到文件系统中某个实际文件的现有连接。假如文件描述符为空,会抛出 NullPointerException 异常。

3.3 方法详解

从类图中可以看出,FileInputStream 的方法并不少。但实际上,开发人员只要重点掌握以下几个方法即可。

方法名 作用
read() 从文件输入流中读取一个字节数据。
read(byte[] b) 从文件输入流中将最多 b.length 个字节的数据读入一个 byte 数组中。
read(byte[] b, int off, int len) 从文件输入流中第 off 个位置开始,将最多 len 个字节的数据读入一个 byte 数组中。
skip(long n) 从输入流中跳过并丢弃 n 个字节的数据。
available() 返回流中剩余的可读字节数。
close() 关闭流,并释放与该流有关联的系统资源。
finalize() 确保不再持有该输入流的引用时,能调用其 close 方法。

3.4 应用实例

首先,我们在项目源目录下创建文本文件 panda.txt,并在该文件中输入 4 个字符 abcd

然后,我们使用 FileInputStream 尝试读取 panda.txt 的数据。

private static final String FILE_PATH = FileUtil.class.getClassLoader().getResource("").getPath();

public static void readFile() {

    File file = new File(FILE_PATH + File.separator + "panda.txt");

    try {
        InputStream in = new FileInputStream(file);
        int len;
        while ((len = in.read()) != -1) {
            System.out.println(len);
        }
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

代码编写完后,点击运行,发现控制台打印出如下结果:97 98 99 100

这时候我们就遇到一个问题了。为什么我们输入的是 abcd,结果 FileInputStream 读到的却是这 4 个数字?

答案就在下面这张表格,也是著名的 ASCII 码表的一部分。

字符 二进制 十进制
a 0110 0001 97
b 0110 0010 98
c 0110 0011 99
d 0110 0100 100

我们一再强调,FileInputStream 是字节流,只能从文件中读取字节。因此,当你的文本文件中含有字符 a 时,FileInputStream 所获取的实际上是字符 a 在存储空间中所代表的二进制数据 0110 0001,然后再以十进制的形式返回到我们的控制台上。

这时候我们又碰到了一个问题。如果以上论述成立的话,那么,字符与二进制之间的关系是如何确立的?

这个问题,就需要编码来解决了。在我们本例中,因为文本文件中存储的是英文字母,所以只需要 ASCII 码表就够了。但假如文本文件中含有中文呢?结果又会是怎样?

现在,我们将 panda.txt 中的数据改为 ,然后再次运行程序,得到以下结果:228 184 128

wtf?我输入的是汉字,你给我的是什么玩意?实际上,这三个数字是由 UTF-8 编码将汉字 转化成字节数据的结果。如果此时我们将 panda.txt 的编码格式改为 GBK,会发现程序的运行结果如下:210 187

我们可以看到,在 UTF-8 编码中,汉字 占据了 3 个字节,而在 GBK 编码中,汉字 仅占据了 2 个字节。

综上,我们可以做如下结论:FileInputStream 只能读取字节,而字节的值,是由编码方式与字符集决定的。

四、FileOutputStream

FileOutputStream 是 Java 文件流中的输出流,继承自 OutputStream,可以向本地文件系统写入字节数据。

4.1 类图

image

4.2 构造方法

如上图所示,FileOutputStream 有 5 种构造方法:

方法名 方法详解
FileOutputStream(String name) 创建一个向指定名称的文件写入字节数据的文件输出流。假如文件不存在或者文件为根目录,会抛出 FileNotFoundException 异常。
FileOutputStream(String name, boolean append) 创建一个向指定名称的文件写入字节数据的文件输出流。假如 append 为 ture,则输出的字节数据将补充在文件的尾部,否则将在文件的起始位置写入。
FileOutputStream(File file) 创建一个向指定 File 对象表示的文件写入字节数据的文件输出流。假如文件不存在或者文件为根目录,会抛出 FileNotFoundException 异常。
FileOutputStream(File file, boolean append) 创建一个向指定 File 对象表示的文件写入字节数据的文件输出流。假如 append 为 ture,则输出的字节数据将补充在文件的尾部,否则将在文件的起始位置写入。
FileOutputStream(FileDescriptor fdObj) 创建一个向指定文件描述符写入字节数据的文件输出流,该文件描述符表示一个到文件系统中的某个实际文件的现有连接。

4.3 方法详解

相对 FileInputStream,FileOutputStream 的方法数量会少一些,常用的方法如下表所示。

方法名 作用
write(int b) 将指定字节写入到此文件输出流。
write(byte[] b) 将 b.length 个字节从指定 byte 数组写入到此文件输出流。
write(byte[] b, int off, int len) 从 byte 数组第 off 个位置开始,将最多 len 个字节的数据写入到此文件输出流。
close() 关闭流,并释放与该流有关联的系统资源。
finalize() 确保不再持有该输出流的引用时,能调用其 close 方法。

4.4 应用实例

首先,我们完成一个 FileOutputStream 的代码实例。

public static void writeFile() {

    File file = new File(FILE_PATH + File.separator + "out.txt");

    try {
        OutputStream out = new FileOutputStream(file);
        out.write(97);
        out.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

然后我们打开 out.txt 文件,发现里面有一个字符 a。但是,我们注意到,代码里面 write(int b) 方法的参数明明是 97 啊,为什么会变成字符 a 了?还是跟我们在 FileInputStream 中的解释一样。 FileOutputStream 只能写出字节数据! 所以你给了它一个值为 97 的参数,它不会明白 97 的意思,但是它能找到 97 对应的字符 a

我们再来做一件有趣的事情,试一试写入代表中文字符的字节。

public static void writeFile() {
    File file = new File(FILE_PATH + File.separator + "out.txt");

    try {
        OutputStream out = new FileOutputStream(file);
        String text = "一";
        out.write(text.getBytes());
        out.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

因为 FileOutputStream 不能直接写入字符串,因此我们需要调用 String 类的 getBytes() 方法,获取字节数组。

点击运行程序后,我们打开 out.txt 文件,发现里面确实写入了字符

嗯,看样子我们像是已经完成了 FileOutputStream 的学习,但是别急,我们还有一件事没做。

首先,我们查看了 out.txt 的编码,发现编码方式为 UTF-8。这时候问题来了,我们什么时候指定编码方式了?仔细思考一下,应该是在调用 getBytes() 方法时实现了编码的设置,因此我们从 getBytes() 方法跟踪进去,最后在 Charset 类中发现了下面的代码:

public static Charset defaultCharset() {
    if (defaultCharset == null) {
        synchronized (Charset.class) {
            String csn = AccessController.doPrivileged(
                new GetPropertyAction("file.encoding"));
            Charset cs = lookup(csn);
            if (cs != null)
                defaultCharset = cs;
            else
                defaultCharset = forName("UTF-8");
        }
    }
    return defaultCharset;
}

这段代码有什么作用呢?它可以返回 JVM 的默认编码格式,而 JVM 的编码格式,又依赖于底层操作系统。因为我的系统使用的是 UTF-8 编码,因此我们在运行上面的示例代码时,默认使用了 UTF-8

当然,这个编码方式我们也可以自己指定。

public static void writeFile() {
    File file = new File(FILE_PATH + File.separator + "out.txt");

    try {
        OutputStream out = new FileOutputStream(file);
        String text = "一";
        out.write(text.getBytes("GBK"));
        out.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

点击运行后,我们使用 Binary Viewer 查看 out.txt 文件,发现里面有两个字节,值为 210 187,刚好与我们在 FileInputStream 中的示例一致。

综上,我们可以做如下结论:FileOutputStream 只能输出字节,而字节的编码方式,可以用户自己设置,也可以使用 JVM 的默认编码。

写在最后:实际上,目前需要程序直接做文本输入输出的情况已经非常少了,大多数情况,我们可以选择使用更为专业的 Property 工具类。但是,我们还是有必要深入学习文本的输入与输出,特别是其中涉及的编码与解码。只有这部分的基础牢固了,才能在日后的开发中避免乱码的发生。

猜你喜欢

转载自blog.csdn.net/magicpenta/article/details/78173689