学堂 学堂 学堂公众号手机端

Java IO 要点梳理与总结

lewis 1年前 (2024-04-02) 阅读数 6 #技术

本篇文章主讲 Java IO,使用的 Java 版本为 Java 8,先说下结论:

字节流:使用字节进行输入输出。其他流都是基于这个流。Java 官方强调这个不推荐日常使用。

字符流:我们平常输入输出几乎都是字符,使用字节流一个一个字节读取就不合适了,于是出现了字符流,字符流包装了字节流,它是操作字符的。


缓冲流:字节流和字符流,都是每一次进行读写就会进行一个物理 IO 操作,效率不高。于是出现缓冲流,写操作先写进内存中的一个区域(缓冲区),写满在调用物理 IO。读操作也是先读取缓冲区,读满再展示。

Scanning 和 Formatting:平时读取和写入是需要一些格式的,比如像读取不同数据类型的数据、换行输入内容。这时就用到 Scanning 和 Formatting。Scanning 的代表是 Scanner 类,虽然它不是流,但是它包装了流。Formatting 最常用的就是我们的 System.out,它实际上是 PrintStream 对象。

命令行 I/O:标准流和 Console。用于命令行上的读写。标准流有三种:System.in、System.out、System.err。Console,必须要在命令行交互的情况下才能使用,它相比较于标准流,可以安全的读取重要敏感数据(比如密码)。

Data Streams:用于处理二进制 I/O 基本数据类型和 String 的读写。它们是包装了字节流,更方便我们操作基本数据类型和 String 的读写。

Object Streams:用于处理二进制对象的读写。它们也可以处理基本类型和 String,因为它们共同直接或间接实现了同样的接口 DataInput、DataOutput。拥有同样的功能。

字节流

使用字节进行输入输出,所有的字节流类都源于 InputStream、OutputStream。

以 FileInputStream 和 FileOutPutStream 为例
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class CopyBytes {
    public static void main(String[] args) throws IOException {

        FileInputStream in = null;
        FileOutputStream out = null;

        try {
            in = new FileInputStream("xanadu.txt");
            out = new FileOutputStream("outagain.txt");
            int d;

            while ((d = in.read()) != -1) {
                out.write(d);
            }
        } finally {
            if (in != null) {
                in.close();
            }
            if (out != null) {
                out.close();
            }
        }
    }
}
读取图解

字节输入流读取数据 read,读取的数据赋值到 d,将 d 写入输出流。

注意事项

当流不再被使用时,一定要关闭。可以看到程序中是在 finally 关闭字节流的(close 方法)。当出现异常时,in、out 可能为 null,所以关闭前进行了判空。

字节流的使用场景

字节流应该是被避免使用的一种低级 IO(low level I/O)。当 xanadu.txt 包含字符数据时,最好使用字符流。

So why talk about byte streams? Because all other stream types are built on byte streams.

那为什么还要学字节流,因为所有其他流类型都基于字节流。

字符流

在大多数应用中,字符流都可以替代字节流。

字符流使用

所有的字节流类都源于 Reader 和 Writer。

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;


public class CopyCharacters {
    public static void main(String[] args) throws IOException {

        FileReader inputStream = null;
        FileWriter outputStream = null;

        try {
            inputStream = new FileReader("xanadu.txt");
            outputStream = new FileWriter("characteroutput.txt");

            // 这里 int 存储的是一个字符值(使用 int 的后 16 位)
            int c;
            while ((c = inputStream.read()) != -1) {
                outputStream.write(c);
            }
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        }
    }
}

这个程序和字节流的很像,区别在于:FileReader 被替换成 FileInputStream,FileWriter 被替换成 FileOutputStream。

字符流和字节流

• 字节流源于:InputStream、OutputStream
• 字符流源于:Reader、Writer。

字符流是字节流的包装。字符流使用字节流来进行物理 I/O,并且处理字符和字节的转换。
Java 官方不推荐使用字节流,只要数据包含字符,就应该使用字符流。

Scanning and Formatting

在使用输入输出时,一般会喜欢格式化。Java 提供两种 API 来帮助实现。scanner 和 format,分别用来输入和输出不同格式的数据。

Scanning

代表类就是 Scanner 类,使用例子如下:

public class ScanSum {
    public static void main(String[] args) throws IOException {

        Scanner s = null;
        double sum = 0;

        try {
            s = new Scanner(new BufferedReader(new FileReader("usnumbers.txt")));
            // 设置语言环境(不同语言环境千分位可能不一样)
            s.useLocale(Locale.CHINA);

            // Scanner 对象可以调用 nextXXX 方法来读取不同数据类型的数据。
            while (s.hasNext()) {
                if (s.hasNextDouble()) {
                    sum += s.nextDouble();
                } else {
                    s.next();
                }
            }
        } finally {
            if (s != null) {
                s.close();
            }
        }

        // System.out 是 PrintStream 对象
        System.out.println(sum);
    }
}

Scanner 对象不是流,但是包装了输入流,所以可以进行 I/O 操作。通过它的对象调用 nextXXX 方法,就可以读取不同类型的值。

Formatting

我们常用的 System.out 其实就是 PrintStream 的对象。与它类似的还有 System.err。在需要自定义对象时,要使用类 PrintWriter 而不是 PrintStream。

常用的方法是:print()、println()、format()。

public class FormatDemo {
    public static void main(String[] args) {
        int i = 2;
        double r = Math.sqrt(i);
        // 换行使用 %n 而不是 \n(\n 会生成一个换行符)
        System.out.format("The square root of %d is %f.%n", i, r);

        // 格式化日期,输出月份
        System.out.format("%tB", new Date());
    }
}

格式说明符:

最常用的格式说明符:

d:整数

f:浮点数

s:字符串

%n:用来换行,需要换行时不推荐使用 \n,使用 %n,Java 会根据操作系统生成不同的换行符。

总结

当输入与输出的都是不同类型、不同格式的数据时,就可以用 Scanning 和 Formatting。

Scanning:代表类 Scanner。它不是流,但是因为包装了输入流,所以可以进行 IO 操作。

Formatting:最常被使用的就是 System.out 。他是 PrintStream 的对象,在需要自定义 Formatting 类型的对象时,要使用 PrintWriter 创建对象而不是 PrintStream。

命令行 IO

从命令行读写有两种方式:

通过标准流(Standard Streams)通过控制台(Console)Standard Streams

标准流,一般来说是从键盘读取,在控制台显示读取的内容。Java 有三种标准流:

System.in:标准输入System.out:标准输出System.err:标准错误

System.in 是字节流不是字符流,如果想要使用字符标准输入,需要使用 InputStreamReader 包装(转为字符流):

public class StandardStreamDemo {
    public static void main(String[] args) throws IOException {
        int b = 0;
        InputStreamReader in = new InputStreamReader(System.in);
        try {
            while (((b = in.read()) != -1))
                System.out.println((char) b);
        } finally {
            in.close();
        }
    }
}

其实 Scanner 本身就做了这样的操作,它的其中一个构造方法如下:

    public Scanner(InputStream source) {
        this(new InputStreamReader(source), WHITESPACE_PATTERN);
    }
Console

相比较于 Standard Streams,他更安全,可以用来安全的输入密码(readPassword 方法)。

public class Password {

    public static void main(String[] args) throws IOException {

        Console c = System.console();
        if (c == null) {
            System.err.println("No console.");
            System.exit(1);
        }

        String login = c.readLine("Enter your login: ");
        char[] oldPassword = c.readPassword("Enter your old password: ");

        if (verify(login, oldPassword)) {
            boolean noMatch;
            do {
                char[] newPassword1 = c.readPassword("Enter your new password: ");
                char[] newPassword2 = c.readPassword("Enter new password again: ");
                noMatch = !Arrays.equals(newPassword1, newPassword2);
                if (noMatch) {
                    c.format("Passwords don't match. Try again.%n");
                } else {
                    change(login, newPassword1);
                    c.format("Password for %s changed.%n", login);
                }
                Arrays.fill(newPassword1, ' ');
                Arrays.fill(newPassword2, ' ');
            } while (noMatch);
        }

        Arrays.fill(oldPassword, ' ');
    }

    // Dummy change method.
    static boolean verify(String login, char[] password) {
        // This method always returns
        // true in this example.
        // Modify this method to verify
        // password according to your rules.
        return true;
    }

    // Dummy change method.
    static void change(String login, char[] password) {
        // Modify this method to change
        // password according to your rules.
    }
}

上面的程序步骤:

拿到 Console 对象。(System.console())(必须在命令行下执行 Java 程序,如果用 IDE 会拿不到 Console 对象)通过 readLine 拿到登录用户通过 readPassword 拿到旧密码(使用该方法命令行不会显示输入的内容)验证(此处为假逻辑)通过 readPassword 拿到新密码和确认密码修改密码(此处为假逻辑)旧密码已被覆盖

程序的效果大概是这样的:

总结

命令行 I/O 在 Java 有两种实现:

Stardard StreamsConsole

其中 Stardard Streams 有三种:

System.in:标准输入

System.out:标准输出

System.err:错误输出

而 Console 相比较与 Stardard Streams,可以安全的,在命令行获取输入的密码(不会显示),但是必须是在命令行才可以获取 Console 对象。

Data Streams

Data Streams 支持二进制 I/O(八大基本数据类型和 String)。它们的实现类都实现接口 DataInput、DataOutPut。这次的例子使用的是它们最广泛的实现类 DataInputStream、DataOutPutStream。

import java.io.*;

public class DataStreamDemo {
    static final String dataFile = "invoicedata.txt";

    static final double[] prices = { 19.99, 9.99, 15.99, 3.99, 4.99 };
    static final int[] units = { 12, 8, 13, 29, 50 };
    static final String[] descs = {
            "Java T-shirt",
            "Java Mug",
            "Duke Juggling Dolls",
            "Java Pin",
            "Java Key Chain"
    };

    public static void main(String[] args) throws IOException {
        // DataOutputStream 包装已有 buffer 字节输出流对象
        DataOutputStream out = new DataOutputStream(new BufferedOutputStream(
                new FileOutputStream(dataFile)));

        // 写数据到文件
        for (int i = 0; i < prices.length; i ++) {
            out.writeDouble(prices[i]);
            out.writeInt(units[i]);
            // 将 descs[i]以 UTF-8 编码的变化形式,写入文件
            out.writeUTF(descs[i]);
        }
        // 刷新缓冲区
        out.flush();

        // 读取文件
        // DataInputStream 包装已有字节流对象(包装的文件输入流)
        DataInputStream in = new DataInputStream(new
                BufferedInputStream(new FileInputStream(dataFile)));

        double price;
        int unit;
        String desc;
        double total = 0.0;

        try {
            while (true) {
                price = in.readDouble();
                unit = in.readInt();
                desc = in.readUTF();
                System.out.format("You ordered %d" + " units of %s at $%.2f%n",
                        unit, desc, price);
                total += unit * price;
            }
        } catch (EOFException e) {
            // 用异常来终止 while 循环(读取文件结束继续读取会抛出 EOFException 异常)
        }
    }
}
总结

Data Streams,是 Java I/O 提供的,给基本类型和 String 的二进制输入输出流。

数据流类都是实现的 DataInput 和 DataOutPut 接口。

上面只讲了最常用的 DataInputStream 和 DataOutputStream。它们都是包装已有的字节流对象。

Object Streams

object streams 支持 Object I/O,但是前提是对象所属的类已经实现 Serializable 接口

Object Streams 类是:ObjectInputStream、ObjectOutputStream。

ObjectInputStream 体系图:

可以看到它拥有 DataInput、ObjectInput 接口的的所有功能,所以 Data Streams 的例子,对于 Object Streams 仍然适用:

Object Streams 处理基本数据类型与 String 类型

DataOutputStream 换成 ObjectOutputStream :

         ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(
                new FileOutputStream(dataFile)));

DataInputStream 换成 ObjectIntputStream :

        ObjectInputStream in = new ObjectInputStream(new
                BufferedInputStream(new FileInputStream(dataFile)));

运行后结果与使用 Data Streams 一致。

完整代码:

import java.io.*;

public class ObjectStreamDemo {
    static final String dataFile = "invoicedata.txt";

    static final double[] prices = { 19.99, 9.99, 15.99, 3.99, 4.99 };
    static final int[] units = { 12, 8, 13, 29, 50 };
    static final String[] descs = {
            "Java T-shirt",
            "Java Mug",
            "Duke Juggling Dolls",
            "Java Pin",
            "Java Key Chain"
    };

    public static void main(String[] args) throws IOException {
        // DataOutputStream 包装已有 buffer 字节输出流对象
        ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(
                new FileOutputStream(dataFile)));

        // 写数据到文件
        for (int i = 0; i < prices.length; i ++) {
            out.writeDouble(prices[i]);
            out.writeInt(units[i]);
            // 将 descs[i]以 UTF-8 编码的变化形式,写入文件
            out.writeUTF(descs[i]);
        }
        // 刷新缓冲区
        out.flush();

        // 读取文件
        // ObjectInputStream 包装已有字节流对象(包装的文件输入流)
        ObjectInputStream in = new ObjectInputStream(new
                BufferedInputStream(new FileInputStream(dataFile)));

        double price;
        int unit;
        String desc;
        double total = 0.0;

        try {
            while (true) {
                price = in.readDouble();
                unit = in.readInt();
                desc = in.readUTF();
                System.out.format("You ordered %d" + " units of %s at $%.2f%n",
                        unit, desc, price);
                total += unit * price;
            }
        } catch (EOFException e) {
            // 用异常来终止 while 循环(读取文件结束继续读取会抛出 EOFException 异常)
        }
    }
}
Object Streams 处理复杂类型

通过 readObject 和 writeObject 处理复杂类型。在写入对象时,会读取对象及对象的引用对象,如图所示, a 包含 b 和 c,b 包含 d 和 e,在写入时会将这些都写入。在读取时也会将这些对象都读出来。

实体类 Student:

public class Student implements Serializable {
    private String name;

    public Student(String name) {
        this.name = name;
    }
}

要进行读写的对象所属类一定要实现 Serializable ,否则无法使用 readObject 和 writeObject 方法。

使用示例:

public class ObjectStreamDemo2 {
    static final String dataFile = "invoicedata.txt";

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(
                new FileOutputStream(dataFile)));

        Student stu = new Student("zhangsan");
        out.writeObject(stu);
        out.writeObject(stu);
        // 刷新缓冲区
        out.flush();

        // 读取文件
        ObjectInputStream in = new ObjectInputStream(new
                BufferedInputStream(new FileInputStream(dataFile)));

        Object stu1 = in.readObject();
        Object stu2 = in.readObject();

        // true
        System.out.println(stu1.equals(stu2));
    }
}

我们创建 Student 的对象 stu 写入文件两次,读取后,取出对象发现它们地址相同(是同一个对象)。

A stream can only contain one copy of an object, though it can contain any number of references to it.

上面的大概意思是:虽然一个流可以包含多个引用,但是它只是对对象做了拷贝。

我们从一个流写入两次,读取两次相同对象时,发现对象地址是一致的。

总结

Object Streams 可以处理实现 Serializable 接口的实现类的对象的读写。

它实现 ObjectInput 接口,而 ObjectInput 接口是 DataInput 接口的子接口,所以它也可以处理基本数据类型和 String 的读写。

在进行读写时,对象包含的引用对象也会一起进行读写。

Java IO 小结

Java IO ,有字节流、字符流、缓冲流、命令行 IO、Data Streams、Object Streams。他们实际上最终都是用字节流来调用物理 IO 进行读写操作。其他流是为了让我们更加方便、有效率的进行 IO 操作。

版权声明

本文仅代表作者观点,不代表博信信息网立场。

热门