PrintWriter форматирует данные так, чтобы их мог прочитать человек. Однако для вывода информации, предназначенной для другого потока, следует использовать классы DataOutputStream (для записи данных) и DataInputStream (для чтения данных). Конечно, природа этих потоков может быть любой, но в нашем случае открывается файл, буферизованный как для чтения, так и для записи. Надстройки DataOutputStream и DataInputStream ориентированы на посылку байтов, поэтому для них требуются потоки OutputStream и InputStream:
//: io/StoringAndRecoveringData.java
import java.io.*;
public class StoringAndRecoveringData {
public static void main(String[] args)
throws IOException {
DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream("Data.txt")));
out.writeDouble(3.14159);
out.writeUTF("That was pi");
out.writeDouble(1.41413);
out.writeUTF("Square root of 2");
out.close();
DataInputStream in = new DataInputStream(
new BufferedInputStream(
new FileInputStream("Data.txt")));
System.out.println(in.readDouble());
// Только readUTF() нормально читает
// строки в кодировке UTF для Java:
System.out.println(in.readUTF());
System.out.println(in.readDouble());
System.out.println(in.readUTF());
}
}
<spoiler text="Output:">
3.14159
That was pi
1.41413
Square root of 2
</spoiler> Если данные записываются в выходной поток DataOutputStream, язык Java гарантирует, что эти данные в точно таком же виде будут восстановлены входным потоком DataInputStream — невзирая на платформу, на которой производится запись или чтение. Это чрезвычайно ценно, и это знает любой, так или иначе соприкасавшийся с вопросами переносимости программ. Если Java поддерживается на обеих платформах, проблема исчезает сама собой.
Единственным надежным способом записать в поток DataOutputStream строку (String) так, чтобы ее можно было потом правильно считать потоком DataInputStream, является кодирование UTF-8, реализуемое методами readUTF() и writeUTF(). UTF-8 — это разновидность кодировки Юникод, в которой каждый символ хранится в двух байтах. Если вы работаете только с кодировкой ASCII, «удвоение» данных в Юникоде приводит к неоправданным затратам дискового пространства и (или) нагрузке на сеть. Поэтому UTF-8 кодирует символы ASCII одним байтом, а символы из других кодировок записывает двумя или тремя байтами. Вдобавок в первых двух байтах строки хранится ее длина. Впрочем, методы readUTF() и writeUTF() используют специальную модификацию UTF-8 для Java (она описана в документации JDK), и для правильного считывания из другой программы (не на Java) строки, записанной методом writeUTF(), вам придется добавить в нее специальный код, позволяющий верно ее считать.
Методы readUTF() и writeUTF() позволяют смешивать строки и другие типы данных, записываемые потоком DataOutputStream, так как вы знаете, что строки будут правильно сохранены в Юникоде и их будет просто воспроизвести потоком DataInputStream.
Метод writeDouble() записывает число double в поток, а соответствующий ему метод readDouble() затем восстанавливает его (для других типов также существуют подобные методы). Но, чтобы правильно интерпретировать любые данные, вы должны точно знать их расположение в потоке; при наличии такой информации прочитать число double как какую-то последовательность байтов или символов не представляет сложности. Поэтому данные в файле должны иметь определенный формат, или вам придется использовать дополнительную информацию, показывающую, какие именно данные находятся в определенных местах. Заметьте, что сериализация объектов (описанная в этой главе чуть позже) часто предоставляет простейший способ записи и восстановления сложных структур данных.