033、IO 基础

Par @Martin dans le
Tags :

File 类概述和构造方法

  • A: File 类的概述
    • File 更应该叫做一个路径
      • 文件路径或者文件夹路径
      • 路径可以是绝对路径或者相对路径
    • File 对象是文件和目录的抽象表示形式
  • B: 构造方法
    • File(String pathname): 根据一个路径得到File对象
    • File(String parent, String child): 根据一个目录和一个子文件/目录得到 File 对象
    • File(File parent, String child): 根据一个父File对象和一个子文件/目录得到 File 对象

操作 File 类

File 类创建功能

  • A: 创建功能
    • public boolean createNewFile(): 创建文件 如果存在这样的文件, 就不创建了
    • public boolean mkdir(): 创建文件夹 如果存在这样的文件夹, 就不创建了
    • public boolean mkdirs(): 创建文件夹,如果父文件夹不存在, 会帮你创建出来

File 类重命名和删除功能

  • A: 重命名和删除功能
    • public boolean renameTo(File dest): 把文件重命名为指定的文件路径, 还可以当剪切功能使用
    • public boolean delete(): 删除文件或者文件夹
  • B: 重命名注意事项
    • 如果路径名相同, 就是改名
    • 如果路径名不同, 就是改名并剪切
  • C: 删除注意事项:
    • Java 中的删除不走回收站
    • 要删除一个文件夹, 请注意该文件夹内不能包含文件或者文件夹

File 类判断功能

  • A: 判断功能
    • public boolean isDirectory(): 判断是否是目录
    • public boolean isFile(): 判断是否是文件
    • public boolean exists(): 判断是否存在
    • public boolean canRead(): 判断是否可读
      • setReadable(false) 对 windows 无效, windows 认为一切资源可读(这里说的读写是指 io 流)
    • public boolean canWrite(): 判断是否可写
      • setWritable(false) 对 windows 有效
    • public boolean isHidden(): 判断是否隐藏

File 类获取功能

  • A: 获取功能
    • public String getAbsolutePath(): 获取绝对路径
    • public String getPath(): 获取构造函数中传入的路径
    • public String getName(): 获取名称
    • public long length(): 获取长(字节数)
    • public long lastModified(): 获取最后一次的修改时间(毫秒值)
    • public String[] list(): 获取指定目录下的所有文件或者文件夹的名称数组
    • public File[] listFiles(): 获取指定目录下的所有文件或者文件夹的 File 数组

File 过滤器

  • A: 文件名称过滤器的概述
    • public String[] list(FilenameFilter filter)
    • public File[] listFiles(FileFilter filter)
  • B: 文件名称过滤器的使用
    • 需求: 判断E盘目录下是否有后缀名为 .jpg 的文件, 如果有, 就输出该文件名称
  • C: 源码分析
    • 带文件名称过滤器的 list() 方法的源码

IO 流概述及其分类

  • 概念
    • IO 流用来处理设备之间的数据传输
    • Java 对数据的操作是通过的方式
    • Java 用于操作流的类都在 IO 包
    • 流按流向分为两种
      • 输入流
      • 输出流
    • 流按操作类型分为两种:
      • 字节流
        • 字节流可以操作任何数据,因为在计算机中任何数据都是以字节的形式存储的
      • 字符流
        • 字符流只能操作纯字符数据, 比较方便
  • IO 流常用抽象父类
    • 字节流的抽象父类:
      • InputStream
      • OutputStream
    • 字符流的抽象父类:
      • Reader
      • Writer
  • IO 程序书写
    • 使用前, 导入 IO 包中的类
    • 使用时, 进行 IO 异常处理
    • 使用后, 释放资源

字节流

FileInputStream

java.lang.Object
    java.io.InputStream
        java.io.FileInputStream


read()

功能: 一次读取一个字节, 返回读到的字节, 如果返回 -1 表示文件结束

FileInputStream fis = new FileInputStream("aaa.txt");   // 创建一个文件输入流对象, 并关联 aaa.txt
int b;                                                  // 定义变量, 记录每次读到的字节
while ((b = fis.read()) != -1) {                         // 将每次读到的字节赋值给 b 并判断是否是 -1
    System.out.println(b);                              // 打印每一个字节
}

fis.close();                                            // 关闭流释放资源


read() 即然是读取字节, 那它的方法返回值为什么是int?

因为字节输入流可以操作任意类型的文件, 比如图片音频等, 这些文件底层都是以二进制形式的存储的, 如果每次读取都返回 byte, 有可能在读到中间的时候遇到 111111111, 那么这 11111111 是 byte 类型的 -1, 我们的程序是遇到 -1 就会停止不读了, 后面的数据就读不到了, 所以在读取的时候用 int 类型接收, 如果 11111111 会在其前面补上 24 个 0 凑足 4 个字节, 那么 byte 类型的 -1 就变成 int 类型的 255 了, 这样可以保证整个数据读完, 而结束标记的 -1 就是 int 类型

FileOutputStream

java.lang.Object
    java.io.OutputStream
        java.io.FileOutputStream


write()

功能: 一次写入一个字节

FileOutputStream fos = new FileOutputStream("bbb.txt"); // 如果没有 bbb.txt, 会创建出一个
//fos.write(97); //虽然写出的是一个 int 数, 但是在写出的时候会将前面的 24 个 0 去掉,所以写入的一个 byte
fos.write(98);
fos.write(99);
fos.close();


追加

以追加的方式写入文件

FileOutputStream fos = new FileOutputStream("bbb.txt", true); // 第二个参数传 true 就是以追加的方式写入文件
fos.write(98);
fos.write(99);
fos.close();


文件的拷贝

简单粗暴的拷贝

FileInputStream fis = new FileInputStream("致青春.mp3");
FileOutputStream fos = new FileOutputStream("copy.mp3");

int b;
while ((b = fis.read()) != -1) {
    fos.write(b);
}

fis.close();
fos.close();


改良版

上面的版本虽然实现了拷贝功能, 然后效率十分低下, 因为它是读一个字节写一个字节, 相当于执行了 (len(致青春.mp3) * 2) 次 IO 操作, 那可不可以一次性把源文件读完, 再去写呢? 也是可以的.

  • available(): 获取 FileInputStream 对象的字节大小
FileInputStream fis = new FileInputStream("致青春.mp3");
FileOutputStream fos = new FileOutputStream("copy.mp3");

byte[] arr = new byte[fis.available()];                 // 根据文件大小做一个字节数组
fis.read(arr);                                          // 将文件上的所有字节读取到数组中
fos.write(arr);                                         // 将数组中的所有字节一次写到了文件上

fis.close();
fos.close();


再次改良

Java 程序是运行在 JVM 上的, 如果使用上面改良版的拷贝功能, 那么就会存在这样的一个问题:
待拷贝文件非常大时(如 十几二十几个 G 的), 那更本不可能申请那么大的数组…

解决方法是使用小数组多次拷贝.

要实现这个功能, 首先要知道两个知识点:

  • read(byte[] b) 读到的字节放在 b 里面, 返回值是读取到的有效字节个数, -1 表示文件结束
  • write(byte[] b, int off, int len)
    • Writes len bytes from the specified byte array starting at offset off to this file output stream.
FileInputStream fis = new FileInputStream("致青春.mp3");
FileOutputStream fos = new FileOutputStream("copy.mp3");
int len;
byte[] arr = new byte[1024 * 8];                    // 自定义字节数组

while ((len = fis.read(arr)) != -1) {
    fos.write(arr, 0, len);                         // 写出字节数组写出有效个字节个数
}

fis.close();
fos.close();


我们来分析下这段代码, 为什么需要使用 write(byte[] b, int off, int len) 这种 write?
假设下, 如果待读文件里面有 3 个字节(abc), 我们申请一个 2 字节的数组, 每次从待读文件里读 2 个字节出来, 那么第一次读出来的是 [ab], 然后写入文件, 第二次读的出来的是 [cb], 为什么会是 [cb] 呢?
因为我们的待读文件里只剩下一个 c 了, 所以只重写了数组第一位, 那这样最后写入文件的就是 [abcb], 明显是不对的, 所以使用 write(byte[] b, int off, int len), 写入的时候只写入我们刚才读出来的有效字节.

Java 自带的缓冲区式拷贝

  • 缓冲思想
    • 从上面的几次加强上已经看到:
      • 字节流一次读写一个数组的速度明显比一次读写一个字节的速度快很多
      • java 本身在设计的时候也考虑到了这样的设计思想, 所以提供了字节缓冲区流
  • BufferedInputStream
    • BufferedInputStream 内置了一个缓冲区(数组)
    • 从 BufferedInputStream 中读取一个字节时
      • BufferedInputStream 会一次性从文件中读取 8192 个, 先存放在缓冲区中
      • 程序再次读取时, 就不用找文件了, 直接从缓冲区中获取
      • 直到缓冲区中所有的都被使用过, 才重新从文件中读取8192个
  • BufferedOutputStream
    • BufferedOutputStream 也内置了一个缓冲区(数组)
    • 程序向流中写出字节时, 不会直接写到文件, 先写到缓冲区中
    • 直到缓冲区写满, BufferedOutputStream 才会把缓冲区中的数据一次性写到文件里
java.lang.Object
    java.io.InputStream
        java.io.FilterInputStream
            java.io.BufferedInputStream


java.lang.Object
    java.io.OutputStream
        java.io.FilterOutputStream
            java.io.BufferedOutputStream


FileInputStream fis = new FileInputStream("致青春.mp3");        // 创建文件输入流对象,关联致青春.mp3
FileOutputStream fos = new FileOutputStream("copy.mp3");        // 创建输出流对象,关联 copy.mp3

BufferedInputStream bis = new BufferedInputStream(fis);         // 创建缓冲区对 fis 装饰
BufferedOutputStream bos = new BufferedOutputStream(fos);       // 创建缓冲区对 fos 装饰

int b;
while ((b = bis.read()) != -1) {
    bos.write(b);
}

bis.close();                        // 只关装饰后的对象即可
bos.close();


自改良和 Buffered 哪个更效率

定义小数组如果是 8192 个字节大小和 Buffered 比较的话, 定义小数组会略胜一筹, 因为读和写操作的是同一个数组, 而 Buffered 操作的是两个数组.

flush 和 close

首先, 先弄明白 BufferedOutputStream 的 IO 流程, 当我们通过 BufferedOutputStream 去写文件时, Java 先向 BufferedOutputStream 的缓冲区中写, 当写满 8192 个字节后, java 才会把缓冲区的内容刷新到硬盘中.

那么问题来了, 谁也不能保证一个文件正好是 8192 倍数, 那当缓冲区不足 8192 个字节时, 怎么通知 java 去把缓冲区的内容刷新到硬盘中呢? 有两种方法:

  • flush()
    • 用来刷新缓冲区的, 刷新后还可以接着使用 BufferedOutputStream
  • close()
    • 关闭流, 但在关闭流之前会刷新缓冲区, 关闭后不能再使用 BufferedOutputStream

读写中文

  • 字节流读取中文的问题
    • 字节流在读中文的时候有可能会读到半个中文,造成乱码
  • 字节流写出中文的问题
    • 字节流直接操作的字节, 所以写出中文必须将字符串转换成字节数组
    • 写出回车换行 write(“\r\n”.getBytes());

总之, 用字节流来读写中文还是有不少问题的…所以当遇到中文时, 一般使用 字符流 来读写, 这个后面再讲.

异常处理

java 1.6 及以前的版本

FileInputStream fis = null;
FileOutputStream fos = null;
try {
    fis = new FileInputStream("aaa.txt");
    fos = new FileOutputStream("bbb.txt");
    int b;
    while ((b = fis.read()) != -1) {
        fos.write(b);
    }
} finally {
    try {
        if (fis != null) {
            fis.close();
        }
    } finally { // 能关一下尽量关一个
        if (fos != null) {
            fos.close();
        }
    }
}


java 1.7 及以上的版本

try (
    FileInputStream fis = new FileInputStream("aaa.txt");
    FileOutputStream fos = new FileOutputStream("bbb.txt");
) {
    int b;
    while ((b = fis.read()) != -1) {
        fos.write(b);
    }
}


上面的代码, try 后面 {} 里的代码执行完后, 就会自动调用流对象的 close() 方法将流关掉 (类似 python 中的 with 语法).

这是 1.7+ 的异常处理新特性, 在 try() 中创建的对象如果实现了 AutoCloseable 这个接口中的 close() 方法(也必须实现, 否则在 try() 中使用会直接报错), 当发生异常或者执行完 {} 里的语句后, 会自动调用我们实现的 close() 方法.
InputStream 及 OutputStream 都已经实现好了.

字符流

字符流专门用来操作文本, 常用的就是 FileReaderFileWriter.

java.lang.Object
    java.io.Reader
        java.io.InputStreamReader
            java.io.FileReader

java.lang.Object
    java.io.Writer
        java.io.OutputStreamWriter
            java.io.FileWriter


写文件:

FileWriter fw = new FileWriter("test.txt"); // 如果没有 test.txt, 会创建出一个
fw.write("text");
fw.close()


读文件:

FileReader fr = new FileReader("text.txt");
int ch = 0;
while((ch = fr.read()) != -1) {
    System.out.println((char)ch);
}


或者用字符数组:

FileReader fr = new FileReader("text.txt");
char[] buf = new char[1024];

int num = 0;
while((num = fr.read(buf)) != -1) {
    System.out.println(new String(buf, 0, num));
}
fr.close();


与字节流相同, 字符流也提供了缓冲区的快捷操作类:

java.lang.Object
    java.io.Reader
        java.io.BufferedReader

java.lang.Object
    java.io.Writer
        java.io.BufferedWriter


字节流转字符流(转换流)

InputStreamReaderOutputStreamWriter 可以将字节流转换为字符流, 是字节流通向字符流的桥梁, 因此又叫转换流.

当使用 InputStreamReaderOutputStreamWriter 转换字节流后, 尽量使用 BufferedReaderBufferedWriter 对其进行包装.

有无缓冲区性能评测

缓冲区 or 非缓冲区的性能与缓冲区的大小以及一次性写入数据的大小有关.

理论上, 待写入的总数据越、一次性写入的数据量越, 使用缓冲区的性能越高, 例如在默认 8 K 缓冲区的情况下:

  • 总共有 1 M 数据需写入, 每次写入 1 K, 那么使用缓冲区的性能高
  • 总共有 1 M 数据需写入, 每次写入 8 K, 那么使用非缓冲区的性能高

字节数组流

ByteArrayInputStreamByteArrayOutputStream, 用于以 IO 流的方式来完成对字节数组内容的读写, 来支持类似内存虚拟文件或者内存映射文件的功能, 还可以配合对象流来实现对象的深度拷贝 (关于这点, 请参考 035、利用对象流实现深度复制).

ByteArrayInputStream

java.lang.Object
    java.io.InputStream
        java.io.ByteArrayInputStream


接收字节数组作为参数创建:

ByteArrayInputStream bArray = new ByteArrayInputStream(byte [] a);

另一种创建方式是接收一个字节数组, 和两个整形变量 off、len, off表示第一个读取的字节, len表示读取字节的长度.

ByteArrayInputStream bArray = new ByteArrayInputStream(byte []a,
                                                       int off,
                                                       int len)


ByteArrayOutputStream

java.lang.Object
    java.io.OutputStream
        java.io.ByteArrayOutputStream


下面的构造方法创建一个32字节 (默认大小) 的缓冲区.

OutputStream bOut = new ByteArrayOutputStream();

另一个构造方法创建一个大小为n字节的缓冲区.

OutputStream bOut = new ByteArrayOutputStream(int a);

标准 IO 流

System 类中的: in、out

  • System.in 的类型是 InputStream
  • System.in 的默认设备是键盘, 可通过 System.setIn 更改


  • System.out 的类型是 PrintStream
    • PrintStream 继承自 FilterOutputStream
      • FilterOutputStream 继承自 OutputStream
  • System.out 的默认设备是显示器, 可通过 System.setOut 更改

File 及 IO 小结


最后以一个例子小结吧.

/**
 * 复制一个目录及其子目录、文件到另外一个目录
 * @param src
 * @param dest
 * @throws IOException
 */
private void copyFolder(File src, File dest) throws IOException {
    if (src.isDirectory()) {
        if (!dest.exists()) {
            dest.mkdir();
        }
        String files[] = src.list();
        for (String file : files) {
            File srcFile = new File(src, file);
            File destFile = new File(dest, file);
            // 递归复制
            copyFolder(srcFile, destFile);
        }
    } else {
        InputStream in = new FileInputStream(src);
        OutputStream out = new FileOutputStream(dest);

        byte[] buffer = new byte[1024];

        int length;

        while ((length = in.read(buffer)) > 0) {
            out.write(buffer, 0, length);
        }
        in.close();
        out.close();
    }
}


RandomAccessFile

概述

RandomAccessFile 是用来读/写文件的, 可以用 seek( ) 方法来访问设置的记录, 并进行读写; 这些记录的大小不必相同, 但是其大小和位置必须是可知的, 但是该类仅限于操作文件.

RandomAccessFile 不属于 InputStreamOutputStream 类系的, 实际上, 除了实现 DataInputDataOutput接口之外,它和这两个类系毫不相干, 甚至不使用 InputStreamOutputStream 类中已经存在的任何功能;

它是一个完全独立的类, 所有方法 (绝大多数都只属于它自己) 都是从零开始写的.

这可能是因为 RandomAccessFile 能在文件里面前后移动, 所以它的行为与其它的 I/O 类有些根本性的不同, 总而言之, 它是一个直接继承 Object 的、独立的类.

基本上, RandomAccessFile 的工作方式是, 把 DataInputStreamDataOutputStream 结合起来, 再加上它自己的一些方法, 比如:

  • 定位用的 getFilePointer( )
  • 在文件里移动用的 seek( )
  • 判断文件大小的 length( )
  • 以及跳过多少字节数的 skipBytes()

此外, 它的构造函数还要一个表示以只读方式 ("r"),还是以读写方式 ("rw") 打开文件的参数, 和 C 的 fopen( )一模一样, 它不支持只写文件.

RandomAccessFile 的绝大多数功能, 但不是全部, 已经被 JDK 1.4 的 NIO 的 “内存映射文件 (memory-mapped files)” 给取代了, 你该考虑一下是不是用 “内存映射文件” 来代替 RandomAccessFile 了.

RandomAccessFile 中文乱码

首先, 任何数据在内存中都是以二进制的形式保存, 也就是说一个文本文件, 不管你看到的是中文还是英文或者是法文, 它在内存也都是一串二进制数据 (或者说是一串字节数组), 那么当用一个文本浏览器打开一个文本文件时, 该文本浏览器就会拿它读出来的字节数组去 “查字典”, 然后将查到的结果展现出来.

这个 “查字典” 的动作就被称之为解码, 同时也不叫它 “查字典”, 而叫它查码表, 常见的码表有 ASCII、ISO-8859-1、GB2312、GBK、UTF-8、UTF-16 等.

(解码, 解码, 可以理解成破解密码, 二进制数据相当于密码, 人眼不可识别, 你需要把这串密码破解出来才能知道它是什么内容, 所以叫解码).

相对的, 将可识别的文字转换成字节数组的进程就称之为编码, 根据使用的码表不同, 同样的文字会被编码成不同的字节数组.

再来分析下 RandomAccessFile 下的 readLine 方法.

该方法先从文本文件中读取一行字符编码 (也就是字节数组), 然后以 ISO-8859-1 的方式对该字节数组进行解码 (也就是去 “查字典”), 但是很遗憾, 它查的 “字典” 不对 (一般都不是以 ISO-8859-1 码表编码的), 所以显示出来的就是乱码了, 应该要按原始文本文件的编码格式进行解码才对, 我们做下转换就好了:

RandomAccessFile fl = new RandomAccessFile(file, "rw");
System.out.println(new String(fl.readLine().getBytes("ISO-8859-1"), "gb2312")); // gb2312 是你文本文件的编码格式