Java 之路 (二十) -- Java I/O 上(BIO、文件、数据流、如何选择I/O流、典型用例)

标签: java

前言

Java 的 I/O 类库使用 这个抽象概念,代表任何有能力产出数据的数据源对象或者是有能力接收数据的接收端对象。 屏蔽了实际的 I/O 设备中处理数据的细节。

数据流是一串连续不断的数据的集合,简单理解的话,我们可以把 Java 数据流当作是 管道里的水流。我们只从一端供水(输入流),而另一端出水(输出流)。对输入端而言,只关心如何写入数据,一次整体全部输入还是分段输入等;对于输出端而言,只关心如何读取数据,,无需关心输入端是如何写入的。

对于数据流,可以分为两类:

  1. 字节流:数据流中最小的数据单元是字节(二进制数据)
  2. 字符流:数据流中最小的数据单元是字符(Unicode 编码,一个字符占两个字节)

1. 概述

对于 Java.io 包内核心其实就是如下几个类:InputStream、OutputStream、Writer、Reader、File、(RandomAccessFile)。只要熟练掌握这几个类的使用,那么 io 部分就掌握的八九不离十了。

对于上面的几个类,又可以如下分类:

  1. 文件:File、RandomAccessFile
  2. 字节流:InputStream、OutputStream
  3. 字符流:Writer、Reader

io 包内还有一些其他的类,涉及安全以及过滤文件描述符等等,这里重点只在 io 的输入输出,有兴趣可以自行了解:https://docs.oracle.com/javase/9/docs/api/java/io/package-tree.html

简单介绍一下这几个类:

  1. File:用于描述文件或者目录信息,通常代表的是 文件路径 的含义。
  2. RandomAccessFile:随机访问文件
  3. InputStream:字节流写入,抽象基类。
  4. OutputStream:字节流输出,抽象基类。
  5. Reader:字符流输入,抽象基类
  6. Writer:字符流输出,抽象基类

2. 文件

2.1 File

File - 文件和目录路径名的抽象表示。它既可以指代文件,也可以代表一个目录下的一组文件。当指代文件集时,可以调用 list() 方法,返回一个字符数组,代表目录信息。

下面简单列举 File 的使用:

1. 读取目录

public class TestFile {
    public static void main(String[] args) {
        File path = new File("./src/com/whdalive/io");
        String[] list;
        list = path.list();

        for (String dirItem : list) {
            System.out.println(dirItem);
        }
    }
}

/**输出
TestFile.java
*/

2. 创建目录

public class TestFile {
    public static void main(String[] args) {
        File file = new File("D://test1/test2/test3");
        file.mkdirs();

        System.out.println(file.isDirectory());
    }
}

/**输出
true
*/

需要注意 mkdir() 和 mkdirs() 方法的区别

mkdir() 创建一个文件夹

mkdirs() 创建当前文件夹以及其所有父文件夹

3. 删除目录或文件

public class TestFile {
    public static void main(String[] args) {
        File file = new File("D://test1");
        deleteFolder(file);
    }
    private static void deleteFolder(File folder) {
        File[] files = folder.listFiles();
        if (files!=null) {
            for (File file : files) {
                if (file.isDirectory()) {
                    deleteFolder(file);
                }else {
                    file.delete();
                }
            }
        }
        folder.delete();
    }
}

2.2 RandomAccessFile

RandomAccessFile 是一个完全独立的类,它和其他 I/O 类别有着本质不同的行为,它适用于记录由大小已知的记录组成的文件,因此可以将记录从一处转移到另一处,然后读取或者修改记录。

在 java SE 4 中,它的大多数功能由 nio 存储映射文件所取代,因此该类实际上用的不多了。


3. 数据流

数据流相关类的派生关系如图所示,四个基本的类为 InputStream、OutputStream、Writer、Reader,其余类都是这四个类派生出来的。

3.1 字节流

3.1.1 InputStream

InputStream 是所有字节流输入的抽象基类,作用是用来表示那些从不同数据源产生输入的类。这些数据源包括:

  1. 字节数组
  2. String 对象
  3. 文件
  4. 管道
  5. 一个由其他种类的流组成的序列,以便我们可以将他们收集合并到一个流内
  6. 其他数据源,比如网络链接等。

每种数据源都有一个对应的 InputStream 子类,如下:

功能 如何使用
ByteArrayInputStream 允许将内存的缓冲区当作 InputStream 使用 作为一种数据源:将其与 FilterInputStream 对象相连以提供有用接口
StringBufferInputStream(弃用) 将 String 转换成 InputStream 作为一种数据源:将其与 FilterInputStream 对象相连以提供有用接口
FileInputStream 用于从文件读取信息 作为一种数据源:将其与 FilterInputStream 对象相连以提供有用接口
PipedInputStream 产生用于写入相关 PipedOutputStream 的数据。实现”管道化“概念 作为多线程中数据源:将其与 FilterInputStream 对象相连以提供有用接口
SequenceInputStream 将两个或多个 InputStream 对象转换成单一 InputStream 作为一种数据源:将其与 FilterInputStream 对象相连以提供有用接口
FilterInputStream 抽象类,作为装饰器接口。其中装饰器为其他的 InputStream 类提供游泳功能 见↓

FilterInputStream 类的设计采用了装饰器模式,FilterInputStream 类是所有装饰器类的基类,为被装饰的对象提供通用接口,它的子类可以控制特定输入流,以及修改内部 InputStream 的行为方式:是否缓冲,是否保留读过的行,是否把单一字符回退输入流等。

功能 如何使用
DataInputStream 与 DataOutputStream 搭配使用,因此可以按照可移植方式从流读取基本数据类型 包含用于读取基本类型数据的全部接口
BufferedInputStream 防止每次读取时都得进行实际写操作。代表”使用缓冲区“ 与接口对象搭配
LineNumberInputStream(已弃用) 跟踪输入流中的行号 仅增加了行号,因此可能要与接口对象搭配使用
PushbackInputStream 具有”能弹出一个字节的缓冲区“。因此可以将读到的最后一个字符回退 通常作为编译器的扫描器,包含在内是因为 Java 编译器的需要,我们几乎不会用到。

3.1.2 OutputStream

该类同样作为字节输出流的抽象基类,其类别决定了输出所要去往的目标:字节数组、文件或管道。

功能 如何使用
ByteArrayOutputStream 在内存中创建缓冲区,所有送往”流“的数据都要放置在此缓冲区 用于指定数据的目的地:将其与 FilterInputStream 对象相连以提供有用接口
FileOutputStream 用于将信息写至文件 用于指定数据的目的地:将其与 FilterInputStream 对象相连以提供有用接口
PipedOutputStream 任何写入其中的信息都会自动作为相关 PipedInputStream 的输出。实现管道化概念 用于指定多线程的数据的目的地:将其与 FilterInputStream 对象相连以提供有用接口
FiflterOutputStream 抽象类,作为装饰器的接口。其中装饰器为其他 OutputStream 提供有用功能 见↓

同样的,FilterOutputStream 也是装饰器模式:

功能 如何使用
DataOutputStream 与 DataInputStream 搭配使用,因此可以按照可移植方式向流写入基本数据类型 包含用于写入基本类型数据的全部接口
PrintStream 用于产生格式化输出。其中 DataOutputStream 处理数据的存储,PrintStream 处理显示 可以用 boolean 值显示是否在每次换行时清空缓冲区。等等
BufferedOutputStream 代表”使用缓冲区“。可以调用 flush() 清空缓冲区 与接口对象搭配。

3.1.3 序列化

关于序列化对象的输入和输出流:

  1. 对象的输出流ObjectOutputStream   
  2. 对象的输入流: ObjectInputStream

使用:

对象的输出流将指定的对象写入到文件的过程,就是将对象序列化的过程,对象的输入流将指定序列化好的文件读出来的过程,就是对象反序列化的过程。既然对象的输出流将对象写入到文件中称之为对象的序列化,那么可想而知对象所对应的class必须要实现Serializable接口。

示例:

User.java

public class User implements Serializable{

    private static final long serialVersionUID = 1L;
    String uid;
    String pwd;

    public User(String uid,String pwd) {
        // TODO Auto-generated constructor stub
        this.uid = uid;
        this.pwd = pwd;
    }

    @Override
    public String toString() {
        // TODO Auto-generated method stub
        return "id = " + this.uid + ", pwd = " + this.pwd;
    }
}

解释一下 serialVersionUID 这个成员变量的作用:

它是用来记录 class 文件的版本信息,是 JVM 通过类的信息来算出的一个数字,如果我们不显式指定它,当序列话之后我们把这个 User 类改变了,比如增加一个方法,这时 serialVersionUID 的值也会随之改变,这样序列化文件中记录的 serialVersionUID 和项目中的不一致,就找不到对应的类来反序列化。

而当我们显式指定 serialVersionUID 的值后,JVM 就不会再计算这个 class 的 serialVersionUID 了,这样我们不用担心序列化后改变源文件后无法反序列化的问题了。

TestFile.java

public class TestFile {
    static User user ;
    public static void main(String[] args) {
        File file = new File("D://user.txt");
        writeObject(file);
        readObject(file);
    }
    private static void writeObject(File file) {
        user = new User("whdalive", "123...");
        try {
            FileOutputStream fOutputStream = new FileOutputStream(file);
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fOutputStream);

            objectOutputStream.writeObject(user);
            objectOutputStream.close();
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
    }
    private static void readObject(File file) {
        try {
            FileInputStream fInputStream = new FileInputStream(file);
            ObjectInputStream objectInputStream = new ObjectInputStream(fInputStream);
            User user = (User) objectInputStream.readObject();
            System.out.println("uid = " + user.uid + ", pwd = " + user.pwd);
        } catch (Exception e) {
            // TODO: handle exception
        }
    }
}

结果

//D://User.txt
 sr com.whdalive.io.User        L pwdt Ljava/lang/String;L uidq ~ xpt 123...t whdalive

//输出结果
uid = whdalive, pwd = 123...

3.2 字符流

这里由于 InputStream 和 Reader 类似,OutputStream 和 Writer 类似,只不过是面向的数据流不同,InputStream/OutputStream 是字节流,而 Reader/Writer 是字符流。因此只需要记忆对应关系即可。

字节流 字符流
InputStream Reader
适配器:InputStreamReader
OutputStream Writer
适配器:OutputStreamWriter
FileInputStream FileReader
FileOutputStream FileWriter
StringBufferInputStream(已过时) StringReader
无对应的类 StringWriter
ByteArrayInputStream CharArrayReader
ByteArrayOutputStream CharArrayWriter
PipedInputStream PipedReader
PipedOutputStream PipedWriter

以下是“过滤器”的对应:

过滤器 对应类
FilterInputStream FilterReader
FilterOutputStream FilterWriter
BufferedInputStream BufferedReader
BufferedOutputStream BufferedWriter
DataInputStream 使用DataInputStream(当需要使用 readline() 时,使用 BufferedReader
PirntStream PrintWriter
LineNumberInputStream(已弃用) LineNumberReader
StreamTokenizer StreamTokenizer(使用接收 Reader 的构造器)
PushbackInputStream PushbackReader


4. 如何选择 I/O 流

  1. 输入 vs 输出
    1. 输入:InputStream、Reader
    2. 输出:OutputStream、Writer
  2. 字节(音频文件、图片、歌曲等) vs 字符(涉及到中文文本等)
    1. 字节:InputStream、OutputStream
    2. 字符:Reader、Writer
  3. 数据来源和去处
    1. 文件
      1. 读:FileInputStream、FileReader
      2. 写:FileOutputStream、FileWriter
    2. 数组
      1. byte[]:ByteArrayInputStream、ByteArrayOutputStream
      2. char[]:CharArrayReader、CharArrayWriter
    3. String
      1. StringReader、StringWriter
  4. 标准I/O
    1. System.in
    2. System.out
    3. System.err
  5. 格式化输出
    1. printStream、printWriter

5. 典型使用实例

5.1 标准输入(键盘输入)显示到标准输出(显示器)

public class TestFile {
    public static void main(String[] args) {
        displayInput();
    }
    private static void displayInput() {
        String ch;
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        try {
            while ((ch =  in.readLine())!= null){
                System.out.println(ch);
            }
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
    }
}

5.2 将文件内容打印到显示器

public class TestFile {
    public static void main(String[] args) {
        displayFile();
    }
    private static void displayFile() {
        File file = new File(".\\src\\com\\whdalive\\io\\User.java");
        String string;
        StringBuilder sb = new StringBuilder();
        BufferedReader bufferedReader;
        try {
            bufferedReader = new BufferedReader(new FileReader(file));
            while((string = bufferedReader.readLine())!=null) {
                sb.append(string + "\n");
            }
            bufferedReader.close();
            System.out.println(sb.toString());
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
    }
}

5.3 将标准输入保存到文件

public class TestFile {
    public static void main(String[] args) {
        copyScan();
    }
    private static void copyScan() {
        Scanner in = new Scanner(System.in);
        FileWriter out;
        String string;
        try {
            out = new FileWriter("D://log.txt");
            while(!(string = in.nextLine()).equals("Q")) {
                out.write(string + "\n");
            }
            out.flush();
            out.close();
            in.close();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

总结

关于 Java I/O 本篇还远远不是全部,本文只是简单介绍了 BIO 的内容(即 JDK 1.0 就加入的 java.io 包)。虽然类的扩展性很好,但是代价也在此:实现一个输入输出,需要使用的类过多。尽管如此,只要分类记忆还是比较容易记住的,多学多用,掌握 Java I/O 不是什么特别难的问题。

愿本文对大家有所帮助。

共勉。

原文链接:加载失败,请重新获取