基础(原始)I/O

前言

对于编程语言的设计者来说,实现良好的输入/输出(I/O)系统是一项比较艰难的任务,不同实现方案的数量就可以证明这点。其中的挑战在于要涵盖所有的可能性,你不仅要覆盖到不同的 I/O 源和 I/O 接收器(如文件、控制台、网络连接等),还要实现多种与它们进行通信的方式(如顺序、随机访问、缓冲、二进制、字符、按行和按字等)。
因此,要想充分理解 Java I/O 系统以便正确运用它,我们需要学习一定数量的类。另外,理解 I/O 类库的演化过程也很有必要,因为如果缺乏历史的眼光,很快我们就会对什么时候该使用哪些类,以及什么时候不该使用它们而感到困惑。
编程语言的 I/O 类库经常使用流这个抽象概念,它将所有数据源或者数据接收器表示为能够产生或者接收数据片的对象。
I/O 流屏蔽了实际的 I/O 设备中处理数据的细节:

  1. 字节流对应原生的二进制数据;
  2. 字符流对应字符数据,它会自动处理与本地字符集之间的转换;
  3. 缓冲流可以提高性能,通过减少底层 API 的调用次数来优化 I/O。

从 JDK 文档的类层次结构中可以看到,Java 类库中的 I/O 类分成了输入和输出两部分。在设计 Java 1.0 时,他们就决定让所有与输入相关的类都继承自 InputStream,所有与输出有关的类都继承自 OutputStream。
所有从 InputStream 或 Reader 派生而来的类都含有名为 read()的基本方法,用于读取单个字节或字节数组。同样,所有从 OutputStream 或 Writer 派生而来的类都含有名为write()的基本方法,用于写单个字节或字节数组。但是,我们通常用不到它们,之所以存在是因为别的类可以使用它们,以便提供更有用的接口
我们很少使用单一的类来创建流对象,而是通过叠合多个对象来提供所期望的功能(装饰器模式)。为了创建一个流,你却要创建多个对象,这也是 Java I/O 类库让人困惑的主要原因。
装饰器模式在编写代码的时候给我们带来了相当多的灵活性(我们可以很容易的对属性进行混搭),但它也同时增加了代码的复杂性。Java I/O的不便之处在于:我们必须创建许多类才能得到我们所希望的单个 I/O 对象。

1、输入流类型

InputStream 表示那些从不同数据源产生输入的类,这些数据源包括:

  1. 字节数组:ByteArrayInputStream
  2. String 对象:StringBufferInputStream
  3. 文件:FileInputStream
  4. “管道”,工作方式与实际生活中的管道类似:PipedInputStream(多线程)
  5. 一个由其他种类的类组成的序列,然后我们可以把它们汇聚成一个流:SequenceInputStream
  6. 其他数据源,如 Internet 连接

以上五种输入流都是作为数据源,都可以与 FilterInputStream 对象相连以提供更有用的接口。

2、输出流类型

输出类型和输入类型相对应,但是没有 StringBufferInputStream。

3、通过 FilterInputStream 从 InputStream 读取数据

FilterInputStream 类:DataInputStream 、BufferedInputStream(其他的基本不会用到)
FilterInputStream 类能完成两件截然不同的事情:1、其中 DataInputStream 允许我们读取不同的基本数据类型和 String 类型的对象(如readByte()readFloat())。使用对应的 DataOutputStream ,我们就可以通过数据流将基本数据类型的数据从一个地方迁移到另一个地方。2、其他 FilterInputStream 类则在内部修改 InputStream 的行为:是否缓冲,是否保留读取过的行等。
在实际应用中,不管连接的是什么 I/O 设备,我们基本都会对输入进行缓冲,所以让输入流能默认缓冲更合理,而不是像现在迫使我们每次手动添加。

4、通过 FilterOutputStream 向 OutputStream 写入数据

与 DataInputStream 对应的是 DataOutputStream,它可以将各种基本数据类型和 String 类型的对象格式化输出到“流”中。这样一来,任何机器上的任何 DataInputStream 都可以读出它们。所有方法都以 “write” 开头,例如 writeByte()、writeFloat() 等等。
PrintStream 内有两个重要方法:print()println()。它们被重载了,可以打印各种数据类型。
BufferedOutputStream 是一个修饰符,表明这个流使用了缓冲技术,因此每次向流写入的时候,不是每次都会执行物理写操作。(常用)

5、Reader 和 Writer

InputStream 和 OutputStream 在面向字节 I/O (8比特)这方面仍然发挥着极其重要的作用,而 Reader 和 Writer 则提供兼容 Unicode (16比特)和面向字符 I/O 的功能。
有时我们必须把来自“字节”层级结构中的类和来自字符级的类结合起来使用。为了达到这个目的,需要用到适配器类InputStreamReader:它可以把 InputStream 转为 Reader,而OutputStreamWriter 可以把 OutputStream 转换为 Writer

5.1 更改流的行为

对于 InputStream 和OutputStream 来说,我们会使用 FilterInputStream 和 FilterOutputStream 的装饰器子类来修改流,以满足特殊需要。Reader 和 Writer 的类继承体系沿用了相同的思想,只是稍微不同。
InputStream -> FilterInputStream -> BufferedInputStream ->DataInputStream
OutputStream -> FilterOutputStream -> BufferedOutputStream
Reader -> FilterReader ->BufferedReader
Writer -> FilterWriter -> BufferedWriter
DataInputStream 是我们在 I/O 类库的首选成员,当我们需要使用readLine()时,就不能在使用它了(会报过时方法警告),而应该使用 BufferedReader。

6、I/O 流典型用途

尽管我们可以用不同的方式来组合 I/O 流类,但常用的也就其中几种。当你无法用最新的 Files 解决问题后再使用下面的参照方法。
在这些示例代码中,异常处理都被简化为将异常传递给控制台,但是这样只适合小型的示例和工具。在你自己的代码中,你需要考虑更加复杂的错误处理方式。

6.1 缓冲输入文件

如果想要打开一个文件进行字符输入,我们可以使用一个 FileInputStream 对象,然后传入一个 String 或者 File 对象作为对象名。
为了提高速度,我们可以对文件缓冲,并将产生的引用传递给一个 BufferedReader 构造器。BufferedReader 提供了 line() 方法,它会产生一个Stream<String>对象:

public class BufferedInputFile {
    public static String read(String filename) {
        try(BufferedReader input = new BufferedReader(new FileReader(filename))) {
            return input.lines()
                    .collect(Collectors.joining("\n"));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        System.out.println(read("Cheese.txt"));
    }
}
/*
outputs:
// streams/Cheese.dat
Not much of a cheese shop really, is it?
Finest in the district, sir.
And what leads you to that conclusion?
Well, it's so clean.
It's certainly uncontaminated by cheese.
 */

Collectors.joining("\n")在其内部使用了一个 StringBuilder 来累加其运行结果。该文件会通过 try-with-resources 子句自动关闭。

6.2 从内存输入

下面示例中,从 BufferedInpuTFile.read()读入的 String 被用来创建一个 StringReader 对象。然后调用其 read()方法,每次读取一个字符,并把它显示在控制台:

public class MemoryInput {
    public static void main(String[] args) throws IOException {
        StringReader sr = new StringReader(BufferedInputFile.read("src\\main\\java\\com\\gui\\demo\\thingInJava\\Files\\appendixIO\\MemoryInput.java"));
        int c;
        while ((c = sr.read()) != -1) {
            System.out.println((char) c);
        }
    }
}

注意:int 形式输出的只能是数字,所以必须转为 char 才能正确打印。

6.3 格式化内存输入

要读取格式化数据,我们可以使用 DataInputStream,它是一个面向字节的 io(不是面向字符的)。这样我们必须使用 InputStream 类而不是 Reader 类。我们可以使用 InputStream 以字节形式读取任何数据。

public class FormattedMemoryInput {
    public static void main(String[] args) {
        //DataInputStream 必须接受一个 InputStream
        try (DataInputStream in = new DataInputStream(
                //ByteArrayInputStream 必须接受一个字节数组,所以调用了 String.getByte()
                new ByteArrayInputStream(
                BufferedInputFile.read("src\\main\\java\\com\\gui\\demo\\thingInJava\\Files\\appendixIO\\FormattedMemoryInput.java")
                        .getBytes(StandardCharsets.UTF_8)
        ))) {
            while (true) {
                System.out.write((char) in.readByte());
            }
        } catch (EOFException e) {
            //当读取完文件后,会通过这个异常告知文件已经读完,这里不选择打印异常,而是用一句话替代
            System.out.println("\nEnd of stream");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
/*
outputs:
package com.gui.demo.thingInJava.Files.appendixIO;

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;

......
......

            throw new RuntimeException(e);
        }
    }
}
End of stream
 */

如果我们用 readByte()从 DataInputStream 一次一个字节地读取字符,那么任何字节的值都是合法的,因此返回值不能用来检测输入是否结束。所以,更好的方案是,我们可以使用 available()得到剩余可用字符的数量
下面例子演示了怎么一次一个字符的读取文件:

public class TestEOF {
    public static void main(String[] args) {
        try (DataInputStream in = new DataInputStream(
                new BufferedInputStream(
                        new FileInputStream("src\\main\\java\\com\\gui\\demo\\thingInJava\\Files\\appendixIO\\TestEOF.java")
                )
        )) {
            //available() 返回in的数量
            while (in.available() != 0) {
                System.out.write(in.readByte());
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

对于文件,available 可以读取整个文件,但是对于其他类型的“流”,可能就不是这样,所以要谨慎使用它。

6.4 基本文件的输出

FilterWriter 对象用于向文件写入数据。实际使用时,我们通常都会用 BufferedWriter 将其包装起来以增加缓冲的功能,缓冲可以显著的增加 I/O 的性能。

public class BasicFileOutput {
    static String file = "BasicFileOutput.txt";

    public static void main(String[] args) {
        try (BufferedReader in = new BufferedReader(
                new StringReader(
                        BufferedInputFile.read("src\\main\\java\\com\\gui\\demo\\thingInJava\\Files\\appendixIO\\BasicFileOutput.java")
                ));
             //PrintWriter 提供了格式化功能,它创建的数据文件可作为普通文本来读取
             PrintWriter out = new PrintWriter(
                     new BufferedWriter(new FileWriter(file))
             )) {
            in.lines().forEach(out::println);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        //显示存储的文件
        System.out.println(BufferedInputFile.read(file));
    }
}

6.5 文本文件输出快捷方式

Java 5 在 PrintWriter 中添加了辅助构造器,有了它,我们在创建并写入文件时,就不必每次手动执行一些装饰。
下面的示例使用这种快捷方式重写了 BasicFileOutput.java :

public class FileOutputShortcut {
    static String file = "FileOutputShortcut.txt";

    public static void main(String[] args) throws IOException {
        try (BufferedReader in = new BufferedReader(
                new StringReader(
                        BufferedInputFile.read("src\\main\\java\\com\\gui\\demo\\thingInJava\\Files\\appendixIO\\FileOutputShortcut.java")
                ));
             //PrintWriter 提供了格式化功能,它创建的数据文件可作为普通文本来读取。且它可以自动添加缓冲
             PrintWriter out = new PrintWriter(file)
        ) {
            in.lines().forEach(out::println);
        } catch (IOException ioException) {
            throw new RuntimeException(ioException);
        }
        System.out.println(BufferedInputFile.read(file));

    }
}

6.6 存储和恢复数据

PrintWriter 是用来对可读的数据进行格式化。但如果要输出可供另一个“流”恢复的数据,我们可以用 DataOutputStream 写入数据,然后用 DataInputStream 恢复数据。

public class StoringAndRecoveringData {
    public static void main(String[] args) {
        try (DataOutputStream out = new DataOutputStream(
                new BufferedOutputStream(
                        new FileOutputStream("Data.txt")
                ))
        ){
            out.writeDouble(3.1415926);
            out.writeUTF("That was pi");
            out.writeDouble(1.41413);
            out.writeUTF("Square root of 2");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        try (DataInputStream in = new DataInputStream(
                new BufferedInputStream(
                        new FileInputStream("Data.txt")))
        ){
            System.out.println(in.readDouble());
            //只有readUTF()可以正确读出数据
            System.out.println(in.readUTF());
            System.out.println(in.readDouble());
            System.out.println(in.readUTF());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
/*
outputs:
3.1415926
That was pi
1.41413
Square root of 2
 */

有了writeUTF()readUTF(),我们就可以在 DataOutputStream 中把字符串和其他数据类型混合使用。因为字符串可以作为 Unicode 格式存储,且可以很容易用 DataInputStream 来恢复它。
要想准确恢复,我们必须自动流中数据项的确切位置。因此,要么为文件中的数据采用固定的格式;要么将额外的信息保存在文件中。
注意:对象序列化和 XML 是读取存储和读取复杂结构更简单的方式。

6.7 读写随机访问文件

使用 RandomAccessFile 就像是使用了一个 DataInputStream 和 DataOutputStream 的结合体(它同时实现了它们的接口)。RandomAccessFile其中的seek()可以移动指针并修改对应位置的值。
在使用RandomAccessFile时,你必须清除文件的结构,否则没办法正确使用它。

public class UsingRandomAccessFile {
    static String file = "rtest.dat";

    /**
     * 打开一个文件,并以 double 值的形式显示了其中七个元素
     */
    public static void display() {
        try (
                //构造器第二个必选参数:我们可以指定让 RandomAccessFile 以“只读”(r)方式或“读写” (rw)方式打开文件
                RandomAccessFile rf = new RandomAccessFile(file, "r");
        ) {
            for (int i = 0; i < 7; i++) {
                System.out.println("Value " + i + ": " + rf.readDouble());
            }
            System.out.println(rf.readUTF());
        }catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        //创建文件并修改它
        try (RandomAccessFile tf = new RandomAccessFile(file, "rw")) {
            for (int i = 0; i < 7; i++) {
                tf.writeDouble(i * 1.414);
            }
            tf.writeUTF("The end of the file");
            tf.close();
            display();
        }catch (IOException e) {
            throw new RuntimeException(e);
        }
        try (RandomAccessFile rf = new RandomAccessFile(file, "rw")) {
            //double 都是8字节,所以用seek定位到第5个double值,所以传入的值为5*8
            rf.seek(5 * 8);
            rf.writeDouble(47.0001);
            rf.close();
            display();
        }catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
/*
outputs:
Value 0: 0.0
Value 1: 1.414
Value 2: 2.828
Value 3: 4.242
Value 4: 5.656
Value 5: 7.069999999999999
Value 6: 8.484
The end of the file
Value 0: 0.0
Value 1: 1.414
Value 2: 2.828
Value 3: 4.242
Value 4: 5.656
Value 5: 47.0001
Value 6: 8.484
The end of the file
 */

RandomAccessFile虽然实现了 DataInput 和DataOutput 的接口,但是实际上它和 I/O 继承体系没有关系。所以没办法给它加缓冲。

7、标准 I/O

程序的所有输入都可以来自于标准输入,其所有输出都可以流向标准输出,并且其所有错误信息均可以发送到标准错误。标准I/O 的意义在于程序之间可以很容易地连接起来,一个程序的标准输出可以作为另一个程序的标准输入。这是一个非常强大的工具。
标准 I/O 包括标准输入流System.in、标准输出流System.out、标准错误流System.err

7.1 从标准输入中读取

System.outSystem.err预先包装为了 PrintStream对象,但是System.in是没有经过包装的 InputStream,所以我们使用它必须对其包装。

public class Echo {
    public static void main(String[] args) {
        TimedAbort abort = new TimedAbort(2);
        new BufferedReader(
                new InputStreamReader(System.in))
                .lines()//通常一次一行的进行读取,所以上面使用Reader来封装
                .peek(ln -> abort.restart())//重启 TimeAbort,只要保证每隔两秒有输入就能够使程序保持开启状态
                .forEach(System.out::println);
    }
}
/*
outputs:
TimedAbort 2.0
 */

7.2 将 System.out 转化为 PrintWriter

public class ChangeSystemOut {
    public static void main(String[] args) {
        //System.out 是一个 PrintStream,也就是 OutputStream。而PrintWriter有一个将 outputStream 作为
        // 参数的构造器。第二个参数设置为true是为了可以看到打印输出
        PrintWriter out = new PrintWriter(System.out, true);
        out.println("Hello,World");
    }
}

7.3 重定向标准 I/O

Java 的 System 类提供了简单的 static 方法调用,从而能够重定向标准输入流、标准输出流和标准错误流:

  • setIn(InputStream)
  • setOut(PrintStream)
  • setErr(PrintStream)

如果我们需要在显示器上创建大量的输出,而这些输出滚动的速度太快以至于无法阅读时,重定向输出就显得格外有用,可把输出内容重定向到文件中供后续查看。但是重定向的是字节流而不是字符流。

public class Redirecting {
    public static void main(String[] args) {
        PrintStream console = System.out;
        try (BufferedInputStream in = new BufferedInputStream(
                new FileInputStream("src\\main\\java\\com\\gui\\demo\\thingInJava\\Files\\standardio\\Redirecting.java"));
             PrintStream out = new PrintStream(
                     new BufferedOutputStream(
                             new FileOutputStream("Redirecting.txt")))
        ) {
            System.setIn(in);//将文件中内容载入到标准输入
            System.setOut(out);//重定向标准输出
            System.setErr(out);//重定向标准错误
            new BufferedReader(
                    new InputStreamReader(System.in))
                    .lines()
                    .forEach(System.out::println);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            System.setOut(console);//在程序结束时将系统输出恢复到了该对象
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值