>  기사  >  Java  >  Java 프로그래밍 필수 사항의 I/O 스트림에 대한 자세한 소개

Java 프로그래밍 필수 사항의 I/O 스트림에 대한 자세한 소개

黄舟
黄舟원래의
2017-03-22 10:42:591740검색

이 글에서는 Java I/O 스트림의 기본 사용법과 원리를 자세히 소개합니다.

바이트 스트림

바이트 스트림은 원시 바이너리 데이터 I/O를 처리합니다. 입력과 출력은 8비트 바이트이며, 관련 클래스는 InputStream과 OutputStream입니다.

바이트 스트림에는 많은 클래스가 있습니다. 바이트 스트림의 작동을 보여주기 위해 파일 I/O 바이트 스트림인 FileInputStream 및 FileOutputStream에 중점을 둘 것입니다. 다른 유형의 바이트 스트림도 유사하게 사용됩니다. 주요 차이점은 한 가지 예에서 추론할 수 있습니다.

사용법

다음 CopyBytes 예는 xanadu.txt 파일에서 outagain.txt로 복사하여 한 번에 1바이트만 복사합니다.

public class CopyBytes {
    /**
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        FileInputStream in = null;
        FileOutputStream out = null;

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

            while ((c = in.read()) != -1) {
                out.write(c);
            }
        } finally {
            if (in != null) {
                in.close();
            }
            if (out != null) {
                out.close();
            }
        }
    }
}

CopyBytes는 대부분의 작업을 수행합니다. time 간단한 루프에서는 그림에 표시된 대로 입력 스트림에서 출력 스트림으로 한 번에 한 바이트씩 읽습니다.

항상 스트림을 닫는 것을 기억하세요

더 이상 필요하지 않은 스트림은 닫는 것을 기억하는 것이 중요합니다. 따라서 CopyBytes는 finally 블록을 사용하여 오류가 발생하더라도 두 스트림을 모두 닫을 수 있도록 합니다. 이렇게 하면 심각한 리소스 누출을 방지하는 데 도움이 됩니다.

CopyBytes가 하나 또는 두 파일을 열 수 없는 오류가 발생할 수 있습니다. 이런 일이 발생하면 해결책은 파일의 스트림이 초기 null 값인지 확인하는 것입니다. 이것이 CopyBytes가 각 흐름 변수가 호출되기 전에 개체에 대한 참조를 포함하는지 확인하는 이유입니다.

바이트 스트림을 사용하지 않는 경우

CopyBytes는 일반적인 절차처럼 보일 수 있지만 실제로는 피해야 할 하위 수준 I/O 유형을 나타냅니다. xanadu.txt에는 문자 데이터가 포함되어 있으므로 아래에서 설명하는 것처럼 문자 스트림을 사용하는 것이 가장 좋은 방법입니다. 바이트 스트림은 원시 I/O에만 사용해야 합니다. 다른 모든 스트림 유형은 바이트 스트림 위에 구축됩니다.

문자 스트림

문자 스트림은 문자 데이터의 I/O를 처리하고 자동으로 로컬 문자 세트로의 변환을 처리합니다.

Java 플랫폼은 유니코드 규칙을 사용하여 문자 값을 저장합니다. 문자 스트림 I/O는 이 내부 형식을 로컬 문자 세트로 자동 변환합니다. 서부 로케일에서 기본 문자 집합은 일반적으로 ASCII의 8비트 상위 집합입니다.

대부분의 애플리케이션에서 문자 스트림 I/O는 바이트 스트림 I/O 작업보다 복잡하지 않습니다. 입력 및 출력 스트림 클래스는 기본 문자 집합으로 자동 변환됩니다. 바이트 스트림 대신 문자를 사용하는 프로그램은 프로그래머 측의 추가 작업을 요구하지 않고도 자동으로 로컬 문자 집합에 적응하고 국제화에 대비할 수 있습니다.

국제화가 우선순위가 아니라면 문자 스트림 클래스를 사용하고 문자 세트 문제에 너무 많은 관심을 기울이지 않아도 됩니다. 나중에 국제화가 우선순위가 되면 이러한 요구에 맞게 프로그램을 쉽게 확장할 수 있습니다. 자세한 내용은 국제화를 참조하세요.

사용법

문자 스트림 클래스는 Reader 및 Writer에 설명되어 있습니다. 파일 I/O의 경우 FileReader 및 FileWriter에서 다음은 CopyCharacters의 예입니다.

public class CopyCharacters {
    /**
     * @param args
     * @throws IOException 
     */
    public static void main(String[] args) throws IOException {
        FileReader inputStream = null;
        FileWriter outputStream = null;

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

            int c;
            while ((c = inputStream.read()) != -1) {
                outputStream.write(c);
            }
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        }
    }
}

CopyCharacters와 CopyBytes는 매우 유사합니다. 가장 중요한 차이점은 CopyCharacters는 입력 및 출력에 FileReader 및 FileWriter를 사용하는 반면 CopyBytes는 FileInputStream 및 FileOutputStream을 사용한다는 것입니다. CopyBytes와 CopyCharacters는 모두 int 변수를 사용하여 CopyCharacters에서 마지막 16비트 문자 값을 유지하고, int 변수는 마지막 8비트 바이트 값을 유지합니다.

문자 스트림은 바이트 스트림을 사용합니다

문자 스트림은 바이트 스트림의 "패키지"인 경우가 많습니다. 문자 스트림은 바이트 스트림을 사용하여 물리적 I/O를 수행하는 반면 문자 스트림은 문자와 바이트 간의 변환을 처리합니다. 예를 들어 FileReader는 FileInputStream을 사용하고 FileWriter는 FileOutputStream을 사용합니다.

두 가지 일반적인 바이트-문자 "브리지" 스트림이 있습니다: InputStreamReader 및 OutputStreamWriter. 사전 패키지된 문자 스트림 클래스를 사용할 수 없는 경우 이를 사용하여 문자 스트림을 생성합니다. 이 사용법은 소켓 장에서 설명됩니다.

라인 지향 I/O

문자 I/O는 일반적으로 단일 문자보다 더 큰 단위로 발생합니다. 일반적으로 사용되는 단위는 line: 라인 종결자로 종료됩니다. 줄 종결자는 캐리지 리턴/줄 바꿈 시퀀스("rn"), 캐리지 리턴("r") 또는 줄 바꿈("n")일 수 있습니다. 가능한 모든 줄 끝을 지원하는 이 프로그램은 널리 사용되는 운영 체제에서 생성된 텍스트 파일을 읽을 수 있습니다.

행 기반 I/O 사용을 보여주기 위해 CopyCharacters를 수정합니다. 이를 위해서는 BufferedReader와 PrintWriter라는 두 클래스를 사용해야 합니다. 버퍼링된 I/O 및 For매팅 장에서 이러한 클래스를 더 자세히 살펴보겠습니다.

CopyLines 예제에서는 BufferedReader.readLine 및 PrintWriter.println을 호출하여 줄의 입력과 출력을 모두 수행합니다.

public class CopyLines {
    /**
     * @param args
     * @throws IOException 
     */
    public static void main(String[] args) throws IOException {
        BufferedReader inputStream = null;
        PrintWriter outputStream = null;

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

            String l;
            while ((l = inputStream.readLine()) != null) {
                outputStream.println(l);
            }
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        }
    }
}

readLine을 호출하여 텍스트 줄을 한 줄씩 반환합니다. CopyLines는 println을 사용하여 현재 운영 체제에 대한 줄 종결자가 있는 각 줄을 출력합니다. 이는 입력 파일에 사용된 것과 동일한 줄 종결자가 아닐 수도 있습니다.

除字符和行之外,有许多方法来构造文本的输入和输出。欲了解更多信息,请参阅 Scanning 和 Formatting。

缓冲流(Buffered Streams)

缓冲流通过减少调用本地 API 的次数来优化的输入和输出。

目前为止,大多数时候我们到看到使用非缓冲 I/O 的例子。这意味着每次读或写请求是由基础 OS 直接处理。这可以使一个程序效率低得多,因为每个这样的请求通常引发磁盘访问,网络活动,或一些其它的操作,而这些是相对昂贵的。

为了减少这种开销,所以 Java 平台实现缓冲 I/O 流。缓冲输入流从被称为缓冲区(buffer)的存储器区域读出数据;仅当缓冲区是空时,本地输入 API 才被调用。同样,缓冲输出流,将数据写入到缓存区,只有当缓冲区已满才调用本机输出 API。

程序可以转换的非缓冲流为缓冲流,这里用非缓冲流对象传递给缓冲流类的构造器。

inputStream = new BufferedReader(new FileReader("xanadu.txt"));
outputStream = new BufferedWriter(new FileWriter("characteroutput.txt"));

用于包装非缓存流的缓冲流类有4个:BufferedInputStream 和 BufferedOutputStream 用于创建字节缓冲字节流, BufferedReader 和BufferedWriter 用于创建字符缓冲字节流。

刷新缓冲流

刷新缓冲区是指在某个缓冲的关键点就可以将缓冲输出,而不必等待它填满。

一些缓冲输出类通过一个可选的构造函数参数支持 autoflush(自动刷新)。当自动刷新开启,某些关键事件会导致缓冲区被刷新。例如,自动刷新 PrintWriter 对象在每次调用 println 或者 format 时刷新缓冲区。查看 Formatting 了解更多关于这些的方法。

如果要手动刷新流,请调用其 flush 方法。flush 方法可以用于任何输出流,但对非缓冲流是没有效果的。

扫描(Scanning)和格式化(Formatting)

扫描和格式化允许程序读取和写入格式化的文本。

I/O 编程通常涉及对人类喜欢的整齐的格式化数据进行转换。为了帮助您与这些琐事,Java 平台提供了两个API。scanning API 使用分隔符模式将其输入分解为标记。formatting API 将数据重新组合成格式良好的,人类可读的形式。

扫描

将其输入分解为标记

默认情况下,Scanner 使用空格字符分隔标记。(空格字符包括空格,制表符和行终止符。为完整列表,请参阅Character.isWhitespace)。示例 ScanXan 读取 xanadu.txt 的单个词语并打印他们:

public class ScanXan {
    /**
     * @param args
     * @throws IOException 
     */
    public static void main(String[] args) throws IOException {
        Scanner s = null;

        try {
            s = new Scanner(new BufferedReader(new FileReader("resources/xanadu.txt")));

            while (s.hasNext()) {
                System.out.println(s.next());
            }
        } finally {
            if (s != null) {
                s.close();
            }
        }
    }
}

虽然 Scanner 不是流,但你仍然需要关闭它,以表明你与它的底层流执行完成。

调用 useDelimiter() ,指定一个正则表达式可以使用不同的标记分隔符。例如,假设您想要标记分隔符是一个逗号,后面可以跟空格。你会调用

s.useDelimiter(",\\s*");

转换成独立标记

该 ScanXan 示例是将所有的输入标记为简单的字符串值。Scanner 还支持所有的 Java 语言的基本类型(除 char),以及 BigInteger 和 BigDecimal 的。此外,数字值可以使用千位分隔符。因此,在一个美国的区域设置,Scanner 能正确地读出字符串“32,767”作为一个整数值。

这里要注意的是语言环境,因为千位分隔符和小数点符号是特定于语言环境。所以,下面的例子将无法正常在所有的语言环境中,如果我们没有指定 scanner 应该用在美国地区工作。可能你平时并不用关心,因为你输入的数据通常来自使用相同的语言环境。可以使用下面的语句来设置语言环境:

s.useLocale(Locale.US);

该 ScanSum 示例是将读取的 double 值列表进行相加:

public class ScanSum {
    /**
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        Scanner s = null;
        double sum = 0;

        try {
            s = new Scanner(new BufferedReader(new FileReader("resources/usnumbers.txt")));
            s.useLocale(Locale.US);

            while (s.hasNext()) {
                if (s.hasNextDouble()) {
                    sum += s.nextDouble();
                } else {
                    s.next();
                }
            }
        } finally {
            s.close();
        }

        System.out.println(sum);
    }
}

输出为:1032778.74159

格式化

实现格式化流对象要么是 字符流类的 PrintWriter 的实例,或为字节流类的 PrintStream 的实例。

注:对于 PrintStream 对象,你很可能只需要 System.out 和 System.err。 (请参阅命令行I/O)当你需要创建一个格式化的输出流,请实例化 PrintWriter,而不是 PrintStream。

像所有的字节和字符流对象一样,PrintStream 和 PrintWriter 的实例实现了一套标准的 write 方法用于简单的字节和字符输出。此外,PrintStream 和 PrintWriter 的执行同一套方法,将内部数据转换成格式化输出。提供了两个级别的格式:

  • print 和 println 在一个标准的方式里面格式化独立的值 。

  • format 用于格式化几乎任何数量的格式字符串值,且具有多种精确选择。

print 和 println 方法

调用 print 或 println 输出使用适当 toString 方法变换后的值的单一值。我们可以看到这 Root 例子:

public class Root {
    /**
     * @param args
     */
    public static void main(String[] args) {
            int i = 2;
        double r = Math.sqrt(i);

        System.out.print("The square root of ");
        System.out.print(i);
        System.out.print(" is ");
        System.out.print(r);
        System.out.println(".");

        i = 5;
        r = Math.sqrt(i);
        System.out.println("The square root of " + i + " is " + r + ".");
    }
}

输出为:

The square root of 2 is 1.4142135623730951.
The square root of 5 is 2.23606797749979.

在 i 和 r 变量格式化了两次:第一次在重载的 print 使用代码,第二次是由Java编译器转换码自动生成,它也利用了 toString。您可以用这种方式格式化任意值,但对于结果没有太多的控制权。

format 方法

该 format 方法用于格式化基于 format string(格式字符串) 多参。格式字符串包含嵌入了 format specifiers (格式说明)的静态文本;除非使用了格式说明,否则格式字符串输出不变。

格式字符串支持许多功能。在本教程中,我们只介绍一些基础知识。有关完整说明,请参阅 API 规范关于格式字符串语法。

Root2 示例在一个 format 调用里面设置两个值:

public class Root2 {
    /**
     * @param args
     */
    public static void main(String[] args) {
        int i = 2;
        double r = Math.sqrt(i);

        System.out.format("The square root of %d is %f.%n", i, r);
    }
}

输出为:The square root of 2 is 1.414214.

像本例中所使用的格式为:

  • d 格式化整数值为小数

  • f 格式化浮点值作为小数

  • n 输出特定于平台的行终止符。

这里有一些其他的转换格式:

  • x 格式化整数为十六进制值

  • s 格式化任何值作为字符串

  • tB 格式化整数作为一个语言环境特定的月份名称。

还有许多其他的转换。

注意:除了 %% 和 %n,其他格式符都要匹配参数,否则抛出异常。在 Java 编程语言中,\ n转义总是产生换行符(\u000A)。不要使用除非你特别想要一个换行符。为了针对本地平台得到正确的行分隔符,请使用%n

除了用于转换,格式说明符可以包含若干附加的元素,进一步定制格式化输出。下面是一个 Format 例子,使用一切可能的一种元素。

public class Format {
    /**
     * @param args
     */
    public static void main(String[] args) {
         System.out.format("%f, %1$+020.10f %n", Math.PI);
    }
}

输出为:3.141593, +00000003.1415926536

附加元素都是可选的。下图显示了长格式符是如何分解成元素

元件必须出现在显示的顺序。从合适的工作,可选的元素是:

  • Precision(精确)。对于浮点值,这是格式化值的数学精度。对于 s 和其他一般的转换,这是格式化值的最大宽度;该值右截断,如果有必要的。

  • Width(宽度)。格式化值的最小宽度;如有必要,该值被填充。默认值是左用空格填充。

  • Flags(标志)指定附加格式设置选项。在 Format 示例中,+ 标志指定的数量应始终标志格式,以及0标志指定0是填充字符。其他的标志包括 – (垫右侧)和(与区域特定的千位分隔符格式号)。请注意,某些标志不能与某些其他标志或与某些转换使用。

  • Argument Index(参数索引)允许您指定的参数明确匹配。您还可以指定<到相同的参数作为前面的说明一致。这样的例子可以说:System.out.format(“%F,%<+ 020.10f%N”,Math.PI);

命令行 I/O

命令行 I/O 描述了标准流(Standard Streams)和控制台(Console)对象。

Java 支持两种交互方式:标准流(Standard Streams)和通过控制台(Console)。

标准流

标准流是许多操作系统的一项功能。默认情况下,他们从键盘读取输入和写出到显示器。它们还支持对文件和程序之间的 I/O,但该功能是通过命令行解释器,而不是由程序控制。

Java平台支持三种标准流:标准输入(Standard Input, 通过 System.in 访问)、标准输出(Standard Output, 通过System.out 的访问)和标准错误( Standard Error, 通过System.err的访问)。这些对象被自动定义,并不需要被打开。标准输出和标准错误都用于输出;错误输出允许用户转移经常性的输出到一个文件中,仍然能够读取错误消息。

您可能希望标准流是字符流,但是,由于历史的原因,他们是字节流。 System.out 和System.err 定义为 PrintStream 的对象。虽然这在技术上是一个字节流,PrintStream 利用内部字符流对象来模拟多种字符流的功能。

相比之下,System.in 是一个没有字符流功能的字节流。若要想将标准的输入作为字符流,可以包装 System.in 在 InputStreamReader

InputStreamReader cin = new InputStreamReader(System.in);

Console (控制台)

更先进的替代标准流的是 Console 。这个单一,预定义的 Console 类型的对象,有大部分的标准流提供的功能,另外还有其他功能。Console 对于安全的密码输入特别有用。Console 对象还提供了真正的输入输出字符流,是通过 reader 和 writer 方法实现的。

若程序想使用 Console ,它必须尝试通过调用 System.console() 检索 Console 对象。如果 Console 对象存在,通过此方法将其返回。如果返回 NULL,则 Console 操作是不允许的,要么是因为操作系统不支持他们或者是因为程序本身是在非交互环境中启动的。

Console 对象支持通过读取密码的方法安全输入密码。该方法有助于在两个方面的安全。第一,它抑制回应,因此密码在用户的屏幕是不可见的。第二,readPassword 返回一个字符数组,而不是字符串,所以,密码可以被覆盖,只要它是不再需要就可以从存储器中删除。

Password 例子是一个展示了更改用户的密码原型程序。它演示了几种 Console 方法

public class Password {
    /**
     * @param args
     */
    public static void main(String[] args) {
        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&#39;t match. Try again.%n");
                } else {
                    change(login, newPassword1);
                    c.format("Password for %s changed.%n", login);
                }
                Arrays.fill(newPassword1, &#39; &#39;);
                Arrays.fill(newPassword2, &#39; &#39;);
            } while (noMatch);
        }

        Arrays.fill(oldPassword, &#39; &#39;);
    }

    // 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 对象。如果对象是不可用,中止。

  • 调用 Console.readLine 提示并读取用户的登录名。

  • 调用 Console.readPassword 提示并读取用户的现有密码。

  • 调用 verify 确认该用户被授权可以改变密码。(在本例中,假设 verify 是总是返回true )

  • 重复下列步骤,直到用户输入的密码相同两次:

    • 调用 Console.readPassword 两次提示和读一个新的密码。

    • 如果用户输入的密码两次,调用 change 去改变它。 (同样,change 是一个虚拟的方法)

    • 用空格覆盖这两个密码。

  • 用空格覆盖旧的密码。

数据流(Data Streams)

Data Streams 处理原始数据类型和字符串值的二进制 I/O。

支持基本数据类型的值((boolean, char, byte, short, int, long, float, 和 double)以及字符串值的二进制 I/O。所有数据流实现 DataInput 或DataOutput 接口。本节重点介绍这些接口的广泛使用的实现,DataInputStream 和 DataOutputStream 类。

DataStreams 例子展示了数据流通过写出的一组数据记录到文件,然后再次从文件中读取这些记录。每个记录包括涉及在发票上的项目,如下表中三个值:

记录中顺序 数据类型 数据描述 输出方法 输入方法 示例值
1 double Item price DataOutputStream.writeDouble DataInputStream.readDouble 19.99
2 int Unit count DataOutputStream.writeInt DataInputStream.readInt 12
3 String Item description DataOutputStream.writeUTF DataInputStream.readUTF “Java T-Shirt”

首先,定义了几个常量,数据文件的名称,以及数据。

static final String dataFile = "invoicedata";

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"
};

DataStreams 打开一个输出流,提供一个缓冲的文件输出字节流:

out = new DataOutputStream(new BufferedOutputStream(
              new FileOutputStream(dataFile)))

DataStreams 写出记录并关闭输出流:

for (int i = 0; i < prices.length; i ++) {
    out.writeDouble(prices[i]);
    out.writeInt(units[i]);
    out.writeUTF(descs[i]);
}

该 writeUTF 方法写出以 UTF-8 改进形式的字符串值。

现在,DataStreams 读回数据。首先,它必须提供一个输入流,和变量来保存的输入数据。像 DataOutputStream 、DataInputStream 类,必须构造成一个字节流的包装器。

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

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

现在,DataStreams 可以读取流里面的每个记录,并在遇到它时将数据报告出来:

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) {
}

请注意,DataStreams 通过捕获 EOFException 检测文件结束的条件而不是测试无效的返回值。所有实现了 DataInput 的方法都使用 EOFException 类来代替返回值。

还要注意的是 DataStreams 中的各个 write 需要匹配对应相应的 read。它需要由程序员来保证。

DataStreams 使用了一个非常糟糕的编程技术:它使用浮点数来表示的货币价值。在一般情况下,浮点数是不好的精确数值。这对小数尤其糟糕,因为共同值(如 0.1),没有一个二进制的表示。

正确的类型用于货币值是 java.math.BigDecimal 的。不幸的是,BigDecimal 是一个对象的类型,因此它不能与数据流工作。然而,BigDecimal 将与对象流工作,而这部分内容将在下一节讲解。

对象流(Object Streams)

对象流处理对象的二进制 I/O。

正如数据流支持的是基本数据类型的 I/O,对象流支持的对象 I/O。大多数,但不是全部,标准类支持他们的对象的序列化,都需要实现 Serializable 接口。

对象流类包括 ObjectInputStream 和 ObjectOutputStream 的。这些类实现的 ObjectInput 与 ObjectOutput 的,这些都是 DataInput 和DataOutput 的子接口。这意味着,所有包含在数据流中的基本数据类型 I/O 方法也在对象流中实现了。这样一个对象流可以包含基本数据类型值和对象值的混合。该ObjectStreams 例子说明了这一点。ObjectStreams 创建与 DataStreams 相同的应用程序。首先,价格现在是 BigDecimal 对象,以更好地代表分数值。其次,Calendar 对象被写入到数据文件中,指示发票日期。

public class ObjectStreams {
    static final String dataFile = "invoicedata";

    static final BigDecimal[] prices = { 
        new BigDecimal("19.99"), 
        new BigDecimal("9.99"),
        new BigDecimal("15.99"),
        new BigDecimal("3.99"),
        new BigDecimal("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, ClassNotFoundException {

        ObjectOutputStream out = null;
        try {
            out = new ObjectOutputStream(new
                    BufferedOutputStream(new FileOutputStream(dataFile)));

            out.writeObject(Calendar.getInstance());
            for (int i = 0; i < prices.length; i ++) {
                out.writeObject(prices[i]);
                out.writeInt(units[i]);
                out.writeUTF(descs[i]);
            }
        } finally {
            out.close();
        }

        ObjectInputStream in = null;
        try {
            in = new ObjectInputStream(new
                    BufferedInputStream(new FileInputStream(dataFile)));

            Calendar date = null;
            BigDecimal price;
            int unit;
            String desc;
            BigDecimal total = new BigDecimal(0);

            date = (Calendar) in.readObject();

            System.out.format ("On %tA, %<tB %<te, %<tY:%n", date);

            try {
                while (true) {
                    price = (BigDecimal) in.readObject();
                    unit = in.readInt();
                    desc = in.readUTF();
                    System.out.format("You ordered %d units of %s at $%.2f%n",
                            unit, desc, price);
                    total = total.add(price.multiply(new BigDecimal(unit)));
                }
            } catch (EOFException e) {}
            System.out.format("For a TOTAL of: $%.2f%n", total);
        } finally {
            in.close();
        }
    }
}

如果的 readObject() 不返回预期的对象类型,试图将它转换为正确的类型可能会抛出一个 ClassNotFoundException。在这个简单的例子,这是不可能发生的,所以我们不要试图捕获异常。相反,我们通知编译器,我们已经意识到这个问题,添加 ClassNotFoundException 到主方法的 throws 子句中的。

复杂对象的 I/O

writeObject 和 readObject 方法简单易用,但它们包含了一些非常复杂的对象管理逻辑。这不像 Calendar 类,它只是封装了原始值。但许多对象包含其他对象的引用。如果 readObject 从流重构一个对象,它必须能够重建所有的原始对象所引用的对象。这些额外的对象可能有他们自己的引用,依此类推。在这种情况下,writeObject 遍历对象引用的整个网络,并将该网络中的所有对象写入流。因此,writeObject 单个调用可以导致大量的对象被写入流。

如下图所示,其中 writeObject 调用名为 a 的单个对象。这个对象包含对象的引用 b和 c,而 b 包含引用 d 和 e。调用 writeObject(a) 写入的不只是一个 a,还包括所有需要重新构成的这个网络中的其他4个对象。当通过 readObject 读回 a 时,其他四个对象也被读回,同时,所有的原始对象的引用被保留。

如果在同一个流的两个对象引用了同一个对象会发生什么?流只包含一个对象的一个拷贝,尽管它可以包含任何数量的对它的引用。因此,如果你明确地写一个对象到流两次,实际上只是写入了2此引用。例如,如果下面的代码写入一个对象 ob 两次到流:

Object ob = new Object();
out.writeObject(ob);
out.writeObject(ob);

每个 writeObject 都对应一个 readObject, 所以从流里面读回的代码如下:

Object ob1 = in.readObject();
Object ob2 = in.readObject();

ob1 和 ob2 都是相同对象的引用。

然而,如果一个单独的对象被写入到两个不同的数据流,它被有效地复用 – 一个程序从两个流读回的将是两个不同的对象。

위 내용은 Java 프로그래밍 필수 사항의 I/O 스트림에 대한 자세한 소개의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.