Java IO 要点梳理与总结

2年前 (2022) 程序员胖胖胖虎阿
178 0 0

本篇文章主讲 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();
            }
        }
    }
}

读取图解

Java IO 要点梳理与总结

字节输入流读取数据 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());
    }
}

格式说明符:

Java IO 要点梳理与总结

最常用的格式说明符:

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.
    }
}

上面的程序步骤:

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

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

Java IO 要点梳理与总结

总结

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

  • Stardard Streams
  • Console

其中 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 体系图:

Java IO 要点梳理与总结

可以看到它拥有 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 处理复杂类型

Java IO 要点梳理与总结

通过 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 操作。

版权声明:程序员胖胖胖虎阿 发表于 2022年11月13日 上午4:56。
转载请注明:Java IO 要点梳理与总结 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...