Maison  >  Article  >  Java  >  Une introduction détaillée au flux d'E/S des éléments essentiels de la programmation Java

Une introduction détaillée au flux d'E/S des éléments essentiels de la programmation Java

黄舟
黄舟original
2017-03-22 10:42:591741parcourir

Cet article présente en détail l'utilisation et les principes de base des flux d'E/S Java.

Flux d'octets

Les flux d'octets gèrent les E/S de données binaires brutes. L'entrée et la sortie sont des octets de 8 bits, et les classes associées sont InputStream et OutputStream

Il existe de nombreuses classes de flux d'octets. Pour démontrer le fonctionnement des flux d'octets, nous nous concentrerons sur les flux d'octets d'E/S de fichiers FileInputStream et FileOutputStream. D'autres types de flux d'octets sont utilisés de la même manière. La principale différence réside dans la manière dont ils sont construits. Vous pouvez tirer des conclusions à partir d'un exemple.

Utilisation

L'exemple suivant de copies CopyBytes du fichier xanadu.txt vers outagain.txt, en copiant un seul octet à la fois :

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 coûte un beaucoup d'argent Une partie du temps se déroule dans une boucle simple, lisant un octet à la fois du flux d'entrée au flux de sortie, comme le montre la figure :

Rappelez-vous de toujours fermer le stream

Il est important de penser à fermer un stream lorsqu'il n'est plus nécessaire. Par conséquent, CopyBytes utilise un bloc final pour garantir que les deux flux peuvent toujours être fermés même si une erreur se produit. Cette pratique permet d’éviter de graves fuites de ressources.

Une erreur possible est que CopyBytes ne peut pas ouvrir un ou les deux fichiers. Lorsque cela se produit, la solution consiste à déterminer si le flux du fichier correspond à sa valeur nulle initiale. C'est pourquoi CopyBytes garantit que chaque variable de flux contient une référence à un objet avant d'être appelée.

Quand ne pas utiliser les flux d'octets

CopyBytes peut sembler une procédure normale, mais cela représente en fait un type d'E/S de bas niveau que vous devriez éviter. Étant donné que xanadu.txt contient des données de caractères, le meilleur moyen consiste à utiliser un flux de caractères, comme indiqué ci-dessous. Les flux d’octets ne doivent être utilisés que pour les E/S brutes. Tous les autres types de flux sont construits sur des flux d'octets.

Flux de caractères

Les flux de caractères gèrent les E/S des données de caractères et gèrent automatiquement la conversion en jeux de caractères locaux.

La plateforme Java stocke les valeurs de caractères en utilisant la convention Unicode. Les E/S du flux de caractères convertissent automatiquement ce format interne en jeu de caractères local. Dans les langues occidentales, le jeu de caractères natif est généralement un sur-ensemble de 8 bits de l'ASCII.

Pour la plupart des applications, les E/S de flux de caractères ne sont pas plus complexes que les opérations d'E/S de flux d'octets. Les classes de flux d'entrée et de sortie sont automatiquement converties en jeux de caractères natifs. Les programmes qui utilisent des caractères au lieu de flux d'octets peuvent automatiquement s'adapter au jeu de caractères local et être prêts pour l'internationalisation, le tout sans nécessiter de travail supplémentaire de la part du programmeur.

Si l'internationalisation n'est pas une priorité, vous pouvez simplement utiliser les classes de flux de caractères et ne pas trop prêter attention aux problèmes de jeu de caractères. Plus tard, si l’internationalisation devient une priorité, votre programme pourra facilement évoluer pour répondre à ce besoin. Voir Internationalisation pour plus d’informations.

Utilisation

Les classes de flux de caractères sont décrites dans Reader et Writer. Pour les E/S de fichiers, dans FileReader et FileWriter, voici un exemple de 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 et CopyBytes sont très similaires. La différence la plus importante est que CopyCharacters utilise FileReader et FileWriter pour l'entrée et la sortie, tandis que CopyBytes utilise FileInputStream et FileOutputStream. Veuillez noter que CopyBytes et CopyCharacters utilisent des variables int pour lire et écrire ; dans CopyCharacters, la variable int contient sa dernière valeur de caractère de 16 bits ; dans CopyBytes, la variable int contient sa dernière valeur d'octet de 8 bits.

Le flux de caractères utilise un flux d'octets

Le flux de caractères est souvent un "package" de flux d'octets. Les flux de caractères utilisent des flux d'octets pour effectuer des E/S physiques, tandis que les flux de caractères gèrent la conversion entre les caractères et les octets. Par exemple, FileReader utilise FileInputStream, tandis que FileWriter utilise FileOutputStream.

Il existe deux flux de « pont » octet-caractère courants : InputStreamReader et OutputStreamWriter. Utilisez-les pour créer des flux de caractères lorsque les classes de flux de caractères préemballées ne sont pas disponibles. Cette utilisation sera démontrée dans le chapitre socket.

E/S orientées ligne

Les E/S de caractères se produisent généralement dans des unités plus grandes que des caractères simples. Une unité couramment utilisée est la ligne : terminée par un terminateur de ligne. Le terminateur de ligne peut être une séquence de retour chariot/saut de ligne ("rn"), un retour chariot ("r") ou un saut de ligne ("n"). Prenant en charge toutes les fins de ligne possibles, le programme peut lire les fichiers texte créés par n'importe quel système d'exploitation largement utilisé.

Modifiez CopyCharacters pour démontrer l'utilisation des E/S orientées ligne. Pour ce faire, nous devons utiliser deux classes, BufferedReader et PrintWriter. Nous explorerons ces classes plus en profondeur dans les chapitres Buffered I/O et Formatting.

L'exemple CopyLines appelle BufferedReader.readLine et PrintWriter.println pour effectuer à la fois l'entrée et la sortie d'une ligne.

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();
            }
        }
    }
}

Appelez readLine pour renvoyer des lignes de texte ligne par ligne. CopyLines utilise println pour afficher chaque ligne avec le terminateur de ligne du système d'exploitation actuel. Il se peut qu'il ne s'agisse pas du même terminateur de ligne que celui utilisé dans le fichier d'entrée.

除字符和行之外,有许多方法来构造文本的输入和输出。欲了解更多信息,请参阅 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 都是相同对象的引用。

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

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn