Thinking in Java, 2nd edition, Revision 11

©2000 by Bruce Eckel

[ Предыдущая глава ] [ Оглавление ] [ Содержание ] [ Индекс ] [ Следующая глава ]

11: Система ввода/вывода в Java

Создание хорошей системы ввода/вывода (I/O) является одной из наиболее сложных задач для разработчиков языка.

Доказательством этому служит наличие множества различных подходов. Сложность задачи видится в охвате всех возможностей. Не только различный исходный код и виды ввода/вывода, с которыми вы можете общаться (файлы, консоль, сетевые соединения), но так же вам необходимо общаться с ними большим числом способов (последовательный, в случайном порядке, буферный, бинарный, посимвольный, построчный, пословный и т.п.).

Разработчики библиотеки Java атаковали эту проблему путем создания множества классов. Фактически, существует так много классов для системы ввода/вывода в Java, что это может сначала испугать (по иронии, дизайн ввода/вывода Java I/O на самом деле предотвращает взрыв классов). Также произошли значительные изменения в библиотеке ввода/вывода после версии Java 1.0, когда изначально byte-ориентированная библиотека была пополнена char-ориентированными, основанными на Unicode I/O классами. Как результат, есть некоторое количество классов, которые необходимо изучить прежде, чем вы поймете достаточно хорошо картину ввода/вывода Java и ее правильно использовать. Кроме того, достаточно важно понимать историю эволюции библиотеки ввода/вывода, даже если вашей первой реакцией было: “не надоедайте мне историей, просто покажите мне, как использовать это!” Проблема в том, что без исторической перспективы вы постоянно будете смущаться некоторыми классами, определяя, когда вы должны, а когда не должны использовать их.

Эта глава даст вам введение в различные классы ввод/вывода стандартной библиотеки Java и расскажет о том, как их использовать.

Класс File

Прежде чем перейти к классам, которые действительно читают и записывают данные в поток, мы посмотрим на утилиты, обеспечивающиеся библиотекой в помощь вам в обработке директории файлов.

Класс File имеет обманчивое имя — вы можете подумать, что он ссылается на файл, но это не так. Он может представлять либо имя определенного файла, либо имя набора файлов в директории. Если это набор файлов, вы можете опросить набор с помощью метода list( ), который вернет массив String. Есть смысл возвращать массив, а не гибкий класс контейнера, потому что число элементов фиксировано, и если вам нужен список другого директория, вы просто создаете другой объект File. Фактически, “FilePath” был бы лучшим именем для класса. Этот раздел покажет пример использования этого класса, включая ассоциированный FilenameFilter interface.

Список директории

Предположим, вы хотите получить список директории. Объект File может выдать его двумя способами. Если вы вызовите list( ) без аргументов, вы получите полный список, содержащийся в объекте File. Однако если вы хотите ограничить список, например, если вы хотите получить все файлы с расширением .java, то вам нужно использовать “фильтр директории”, который является классом, который определяет, как использовать объект File для отображения.

Здесь приведен код примера. Обратите внимание, что результат без труда будет храниться (в алфавитном порядке) при использовании метода java.utils.Array.sort( ) и AlphabeticComparator, определенного в Главе 9:

//: c11:DirList.java
// Отображение списка директории.
import java.io.*;
import java.util.*;
import com.bruceeckel.util.*;

public class DirList {
  public static void main(String[] args) {
    File path = new File(".");
    String[] list;
    if(args.length == 0)
      list = path.list();
    else 
      list = path.list(new DirFilter(args[0]));
    Arrays.sort(list,
      new AlphabeticComparator());
    for(int i = 0; i < list.length; i++)
      System.out.println(list[i]);
  }
}

class DirFilter implements FilenameFilter {
  String afn;
  DirFilter(String afn) { this.afn = afn; }
  public boolean accept(File dir, String name) {
    // Получение информации о пути:
    String f = new File(name).getName();
    return f.indexOf(afn) != -1;
  }
} ///:~

Класс DirFilter “реализует” interface FilenameFilter. Полезно посмотреть, насколько прост FilenameFilter interface:

public interface FilenameFilter {
  boolean accept(File dir, String name);
}

Это говорит о том, что этот тип объекта должен обеспечивать метод, называемый accept( ). Главная цель создания этого класса заключается в обеспечении метода accept( ) для метода list( ), так как list( ) может выполнять “обратный вызов” accept( ) для определения, какое имя файла должно включаться в список. Эта техника часто называется обратным вызовом или иногда функтором (то есть, DirFilter - это функтор, потому что он выполняет работу по поддержанию метода) или Командой Заполнения. Потому что list( ) принимает объект FilenameFilter в качестве аргумента, это означает, что вы можете передать объект любого класса, который реализует FilenameFilter для выбора (даже во время выполнения) поведения метода list( ). Назначение обратного вызова заключается в обеспечении гибкого поведения кода.

DirFilter показывает, что из-за того, что interface содержит только набор методов, вы не ограничены в написании только этих методов. (Однако вы должны как минимум обеспечить определение для всех методов интерфейса.) В этом случае также создается конструктор DirFilter.

Метод accept( ) должен принимать объект File, представляющий директорий, в котором находится определенный файл, а String содержит имя этого файла. Вы можете выбрать использовать или игнорировать любой из этих аргументов, но вы, вероятно, как минимум, должны использовать имя файла. Помните, что метод list( ) вызывает метод accept( ) для каждого имени файла в директории, чтобы проверить, какой из них должен быть включен — на это указывает тип boolean результата, возвращаемого accept( ).

Чтобы убедится, что элемент, с которым вы работаете, является всего лишь именем файла и не содержит информации о пути, все, что вам нужно сделать, это получить объект String и создать из него объект File, затем вызвать getName( ), который отсекает всю информацию о пути (платформонезависимым способом). Затем accept( ) использует метод indexOf( ) класса String, чтобы убедится, что искомая строка afn присутствует в любом месте имени файла. Если afn найдено в строке, возвращаемым значением является начальный индекс afn, а если не найдено, возвращаемым значением является -1. Имейте в виду, что это простой поиск строк и не имеет “глобальных” выражений подстановочных символов, таких как fo?.b?r*”, которые являются более сложными в реализации.

Метод list( ) возвращает массив. Вы можете опросить этот массив о его длине, а затем пройтись по нему, выбирая элементы массива. Эта способность легкого прохода по массиву вне методов и в методах является значительным улучшением по сравнению с поведением C и C++.

Анонимные внутренние классы

Это пример идеален для того, чтобы быть переписанным с использованием анонимных внутренних классов (описанных в Главе 8). В качестве первой пробы, создадим метод filter( ), который возвращает ссылку на FilenameFilter:

//: c11:DirList2.java
// Использование анонимных внутренних классов.
import java.io.*;
import java.util.*;
import com.bruceeckel.util.*;

public class DirList2 {
  public static FilenameFilter 
  filter(final String afn) {
    // Создание анонимного внутреннего класса:
    return new FilenameFilter() {
      String fn = afn;
      public boolean accept(File dir, String n) {
        // Получаем информацию о пути:
        String f = new File(n).getName();
        return f.indexOf(fn) != -1;
      }
    }; // Конец анонимного внутреннего класса
  }
  public static void main(String[] args) {
    File path = new File(".");
    String[] list;
    if(args.length == 0)
      list = path.list();
    else 
      list = path.list(filter(args[0]));
    Arrays.sort(list,
      new AlphabeticComparator());
    for(int i = 0; i < list.length; i++)
      System.out.println(list[i]);
  }
} ///:~

Обратите внимание, что аргумент для filter( ) должен быть final. Это требуется анонимному внутреннему классу, так как он использует объект внешней части кода, по отношению к нему.

Это лучший дизайн, потому что класс FilenameFilter теперь тесно связан с DirList2. Однако вы можете выбрать этот подход на один шаг раньше, и определить анонимный внутренний класс как аргумент list( ), в этом случае программа будет даже меньше:

//: c11:DirList3.java
// Построение анонимного внутреннего класса "на месте".
import java.io.*;
import java.util.*;
import com.bruceeckel.util.*;

public class DirList3 {
  public static void main(final String[] args) {
    File path = new File(".");
    String[] list;
    if(args.length == 0)
      list = path.list();
    else 
      list = path.list(new FilenameFilter() {
        public boolean 
        accept(File dir, String n) {
          String f = new File(n).getName();
          return f.indexOf(args[0]) != -1;
        }
      });
    Arrays.sort(list,
      new AlphabeticComparator());
    for(int i = 0; i < list.length; i++)
      System.out.println(list[i]);
  }
} ///:~

Теперь аргумент у main( ) является final, так как анонимный внутренний класс напрямую использует args[0].

Здесь показано как анонимный внутренний класс позволяет создать быстрые и грязные классы для решения проблемы. Так как все в Java вертится вокруг классов, это может быть полезной техникой написания программ. Одна из выгод в том, что программа содержит код, который решает определенную проблему, изолированную в одном месте. С другой стороны, это не всегда легче для чтения, так что вы должны использовать это с умом.

Поиск и создание директориев

Класс File - это больше, чем просто представление существующего файла или директория. Вы также можете использовать объект File для создания новой директории или целого пути директорий, если этот путь не существует. Вы можете также взглянуть на характеристики файлов (размер, дату последней модификации, доступ на чтение/запись), посмотреть, представляет ли объект File файл или директорий, и удалить файл. Эта программа показывает некоторые методы, поддерживаемые классом File (смотрите HTML документацию на java.sun.com чтобы увидеть полный набор):

//: c11:MakeDirectories.java
// Демонстрация использования класса File
// для создания и манипулирования файлами.
import java.io.*;

public class MakeDirectories {
  private final static String usage =
    "Usage:MakeDirectories path1 ...\n" +
    "Creates each path\n" +
    "Usage:MakeDirectories -d path1 ...\n" +
    "Deletes each path\n" +
    "Usage:MakeDirectories -r path1 path2\n" +
    "Renames from path1 to path2\n";
  private static void usage() {
    System.err.println(usage);
    System.exit(1);
  }
  private static void fileData(File f) {
    System.out.println(
      "Absolute path: " + f.getAbsolutePath() +
      "\n Can read: " + f.canRead() +
      "\n Can write: " + f.canWrite() +
      "\n getName: " + f.getName() +
      "\n getParent: " + f.getParent() +
      "\n getPath: " + f.getPath() +
      "\n length: " + f.length() +
      "\n lastModified: " + f.lastModified());
    if(f.isFile())
      System.out.println("it's a file");
    else if(f.isDirectory())
      System.out.println("it's a directory");
  }
  public static void main(String[] args) {
    if(args.length < 1) usage();
    if(args[0].equals("-r")) {
      if(args.length != 3) usage();
      File 
        old = new File(args[1]),
        rname = new File(args[2]);
      old.renameTo(rname);
      fileData(old);
      fileData(rname);
      return; // Выход из main
    }
    int count = 0;
    boolean del = false;
    if(args[0].equals("-d")) {
      count++;
      del = true;
    }
    for( ; count < args.length; count++) {
      File f = new File(args[count]);
      if(f.exists()) {
        System.out.println(f + " exists");
        if(del) {
          System.out.println("deleting..." + f);
          f.delete();
        }
      } 
      else { // Не существует
        if(!del) {
          f.mkdirs();
          System.out.println("created " + f);
        }
      }
      fileData(f);
    }  
  }
} ///:~

В fileData( ) вы можете видеть различные способы исследования файла для отображения информации о файле или о пути директории.

Первый метод, который вызывается main( ) - это renameTo( ), который позволяет вам переименовать (или переместить) файл по введенному новому путь, представленному аргументом, который является другим объектом типа File. Это так же работает с директориями любой длины.

Если вы поэкспериментируете с приведенной выше программой, вы обнаружите, что вы можете создать путь директорий любой сложности, потому что mkdirs( ) будет делать всю работу за вас.

Ввод и вывод

Библиотеки ввода/вывода часто используют абстракцию потока, который представляется любым источником данных или представляется как объект, способный производить или принимать кусочки данных. Поток прячет детали того, что случается с данными внутри реального устройства ввода/вывода.

Библиотечные классы Java для ввода/вывода делятся на классы ввода и вывода, как вы можете увидеть, взглянув на иерархию Java классов в онлайн документации с помощью вашего Web броузера. При наследовании, все, что наследуется от классов InputStream или Reader, имеет основной метод, называемый read( ) для чтения единичного байта или массива байт. Точно так же, все, что наследуется от классов OutputStream или Writer, имеет основной метод, называемый write( ) для записи единичного байта или массива байт. Однако чаще всего вы не можете использовать эти методы; они существуют для того, чтобы другие классы могли использовать их — эти другие классы обеспечивают более полезные интерфейсы. Таким образом, вы редко будете создавать ваш объект потока, используя единственный класс, вместо этого вы будите располагать множеством объектом для обеспечения желаемой функциональности. Факт в том что вы создаете более, чем один объект для создания единственного результирующего потока, это главная причина, по которой потоки Java являются запутанными.

Полезно распределить классы по категориям, исходя из их функциональности. В Java 1.0 разработчики библиотеки начали с решения, что все классы, которые могут что-то делать с вводом, должны наследоваться от InputStream, а все классы, которые ассоциируются с выводом, должны наследоваться от OutputStream.

Типы InputStream

Работа InputStream состоит в представлении классов, которые производят ввод от различных источников. Источниками могут быть:

  1. Массив байт.
  2. Объект String.
  3. Файл.
  4. “Труба”, которая работает так же, как и физическая труба: вы помещаете вещи в один конец, а они выходят из другого.
  5. Последовательность других потоков, так что вы можете собрать их вместе в единый поток.
  6. Другие источники, такие как Internet соединение. (Это будет обсуждено в одной из следующих глав.)

Каждый из них имеет ассоциированный подкласс InputStream. Кроме того, FilterInputStream также имеет тип InputStream, для обеспечения базового класса для "декоративных" классов, которые присоединяют атрибуты или полезные интерфейсы для входного потока. Это будет обсуждаться дальше.

Таблица 11-1. Типы InputStream

Класс Функция Аргументы конструктора
Как его использовать
ByteArray-InputStream
Позволяет использовать буфер в памяти в качестве InputStream Буфер, их которого извлекаются байты.
Как источник данных. Соединить его с объектом FilterInputStream для обеспечения полезного интерфейса.
StringBuffer-InputStream Конвертирует String в InputStream String. Лежащая в основе реализация на самом деле использует StringBuffer.
Как источник данных. Соединить его с объектом FilterInputStream для обеспечения полезного интерфейса.
File-InputStream Для чтения информации из файла. String, представляющий имя файла, или объекты File или FileDescriptor.
Как источник данных. Соединить его с объектом FilterInputStream для обеспечения полезного интерфейса.
Piped-InputStream
Производит данные, которые были записаны в ассоциированный PipedOutput-Stream. Реализует концепцию “трубопровода”. PipedOutputStream
Как источник данных при нескольких нитях процессов. Соединить его с объектом FilterInputStream для обеспечения полезного интерфейса.
Sequence-InputStream Преобразует два или более объектов InputStream в единый InputStream. Два объекта InputStream или Enumeration для контейнера из InputStream.
Как источник данных. Соединить его с объектом FilterInputStream для обеспечения полезного интерфейса.
Filter-InputStream
Абстрактный класс, который является интерфейсом для декоратора, который обеспечивает полезную функциональность для других классов InputStream. Смотрите таблицу11-3. Смотрите таблицу 11-3.
Смотрите таблицу 11-3.

Типы OutputStream

Эта категория включает классы, которые решают, куда будет производиться вывод: в массив байт (но не String; возможно, вы можете создать его, используя массив байт), в файл, или в “трубу”.

Кроме того, FilterOutputStream обеспечивает базовый класс для "декорирования" классов, которые присоединяют атрибуты или полезные интерфейсы для выходного потока. Это будет обсуждаться позже.

Таблица 11-2. Типы OutputStream

Класс Функция Аргументы конструктора
Как его использовать
ByteArray-OutputStream Создает буфер в памяти. Все данные, которые вы будете посылать в поток, помещаются в этот буфер. необязательный начальный размер буфера.
Для определения места назначения ваших данных. Соедините его с объектом FilterOutputStream для обеспечения полезного интерфейса.
File-OutputStream Для отсылки информации в файл. Строка, представляющая имя файла, или объекты File или FileDescriptor.
Для определения места назначения ваших данных. Соедините его с объектом FilterOutputStream для обеспечения полезного интерфейса.

Piped-OutputStream

Любая информация, записанная сюда, автоматически становится вводом ассоциированного PipedInput-Stream. Реализует концепцию “трубопровода”. PipedInputStream
Для определения назначения ваших данных со многими нитями процессов. Соедините его с объектом FilterOutputStream для обеспечения полезного интерфейса.
Filter-OutputStream Абстрактный класс, который является интерфейсом для декоратора, который обеспечивает полезную функциональность другим классам OutputStream. Смотрите Таблицу 11-4. Смотрите Таблицу 11-4.
Смотрите Таблицу 11-4.

Добавление атрибутов и полезных интерфейсов

Использование многослойных объектов для динамического и прозрачного добавления ответственности индивидуальным объектам, называется шаблоном декорации. (Шаблоны [57] являются предметом обсуждения Thinking in Patterns with Java, доступной на www.BruceEckel.com.) Шаблон декорации определяет, что все объекты, которые крутятся вокруг вашего начального объекта, имеют один и тот же интерфейс. Это делает основное использование декораторов прозрачным — вы посылаете объекту одни и те же с сообщения не зависимо от того, был он декорирован или нет. Это причина существования “фильтрующих” классов в библиотеке ввода/вывода в Java: абстрактный “фильтрующий” класс - это базовый класс для всех декораторов. (Декоратор должен иметь такой же интерфейс, что и объект, который он декорирует, но декоратор так же может расширить интерфейс, что случается в некоторых “фильтрующих” классах.

Декорирование часто используется, когда простое использование подклассов в результате приводит к большому числу подклассов, способных удовлетворить каждую возможную необходимую комбинацию, что становится непрактично. Библиотека ввода/вывода Java требует много различных комбинаций особенностей, которые являются причиной использования шаблона декоратора. Однако для шаблона декоратора есть препятствие. Декораторы дают вам много больше гибкости, когда вы пишите программу (так как вы можете легко смешивать и сравнивать атрибуты), но они привносят сложность в ваш код. Причина того, что библиотека Java неудобна в использовании, состоит в том, что вы должны создавать много классов — “центральные” типы ввода/вывода, плюс все декораторы — для того, чтобы создать единственный объект ввода/вывода, который вам нужен.

К классам, обеспечивающим интерфейс декоратора для управления определенным InputStream или OutputStream, относятся FilterInputStream и FilterOutputStream — которые не имеют интуитивно понятных имен. FilterInputStream и FilterOutputStream являются абстрактными классами, наследованными от базовых классов библиотеки ввода/вывода InputStream и OutputStream, которые являются ключевым требованием декоратора (так как он обеспечивает общий интерфейс для всех объектов, которые будут декорироваться).

Чтение из InputStream с помощью FilterInputStream

Классы FilterInputStream совершают две значительные вещи. DataInputStream позволяет вам читать различные типы примитивных данных, наряду с объектами типа String. (Все методы начинаются со слова “read”, например: readByte( ), readFloat( ), и т.п.) Таким образом, наряду со своим компаньоном DataOutputStream, это позволяет вам перемещать примитивные данные из одного места в другое через поток. Эти “места” определяются классами в таблице 11-1.

Оставшиеся классы изменяют способ внутреннего поведения InputStream: будет ли он буферизированный или нет, будет ли он хранить историю прочитанных строк (позволяя вам спрашивать номер строки или множества номеров строк), и сможете ли вы поместить назад единичный символ. Последние два класса выглядят так, как будто они предназначены для поддержки работы компилятора (то есть, они были добавлены для поддержки конструкций Java компилятора), так что вы, вероятно, не захотите использовать их в обычном программировании.

Вероятно, вам необходимо будет буферизировать ваш ввод почти каждый раз, в зависимости от устройства ввода/вывода, к которому вы подсоединяетесь, так что имеет больше смысла для библиотеки ввода/вывода сделать особый случай (или простой вызов метода) для не буферизированного ввода, в отличие от буферизированного ввода.

Таблица 11-3. Типы FilterInputStream

Класс Функция Аргументы конструктора
Как его использовать
Data-InputStream Используется в согласии с DataOutputStream, так что вы можете читать примитивные типы (int, char, long, и т.п.) из потока портативным способом. InputStream
Содержит полный интерфейс, чтобы позволить вам читать примитивные типы.
Buffered-InputStream Используйте это для предотвращения физического чтения каждый раз, когда вам необходимы дополнительные данные. Вы говорить “Использовать буфер”. InputStream с необязательным размером буфера.
Сам по себе не обеспечивает интерфейс, просто требует, чтобы использовался буфер. Присоединяет объект интерфейса.
LineNumber-InputStream Сохраняет историю номеров строк входного потока; вы можете вызвать getLineNumber( ) и setLineNumber(
int).
InputStream
Это просто добавляет нумерацию строк, так что вы, вероятно, присоедините объект интерфейса.
Pushback-InputStream Имеет буфер для возврата одного символа, так что вы можете поместить обратно один прочитанный символ. InputStream
Обычно используется в сканерах для компилятора и, вероятно, включено потому, что Java компилятор нуждается в нем. Вы, вероятно, не захотите использовать его.

Запись в OutputStream с помощью FilterOutputStream

Дополнением к DataInputStream является DataOutputStream, который форматирует каждый из примитивных типов и объекты String в поток, таким образом, которым любой DataInputStream на любой машине смог бы прочесть его. Все методы начинаются со слова “write”, например writeByte( ), writeFloat( ) и т.п.

Изначальное предназначение PrintStream было в печати всех примитивных типов данных и объектов String в удобочитаемом формате. Он отличается от DataOutputStream, чья цель состоит в помещении элементов данных в поток таким способом, чтобы DataInputStream мог без труда реконструировать их.

Двумя важнейшими методами PrintStream являются print( ) и println( ), которые перегружены для печати всех различных типов. Различия между print( ) и println( ) в том, что последний метод добавляет символ новой строки, когда завершен вывод.

PrintStream может быть проблематичным, поскольку он ловит все IOException (вы должны явно проверять статус ошибки с помощью checkError( ), который возвращает true, если возникла ошибка). Так же PrintStream не интернацианализован полностью и не обрабатывает переводы строки платформонезависимым способом (эти проблемы решаются с помощью PrintWriter).

BufferedOutputStream является модификатором и говорит потоку, что нужно использовать буферизацию, так что вы не получите физической записи при каждой записи в поток. Вы, вероятно, всегда захотите использовать это с файлами, и, возможно, при консольном вводе/выводе.

Таблица 11-4. Типы FilterOutputStream

Класс Функции Аргументы конструктора
Как это использовать
Data-OutputStream Используется совместно с DataInputStream, так что вы можете писать примитивные типы (int, char, long и т.п.) в поток портативным образом.
OutputStream
Содержит полный интерфейс, чтобы позволить вам записывать примитивные типы.
PrintStream Для произведения форматированного вывода. В то время как DataOutputStream обрабатывает хранилище данных, PrintStream обрабатывает отображение. OutputStream, с необязательным boolean, указывающим, что буфер будет принудительно освобождаться с каждой новой строкой.
Должен быть в финале оборачивать ваш объект OutputStream. Вы, вероятно, часто будете использовать его.
Buffered-OutputStream Используйте это для предотвращения физической записи при каждой посылке данных. Вы говорите “Используй буфер”. Вы вызываете flush( ) для очистки буфера. OutputStream, с необязательным размером буфера.
Это не обеспечивает сам по себе интерфейс, просто является требованием использования буфера. Присоединяется к объекту интерфейса.

Читающие и пишущие

В Java 1.1 сделаны некоторые значительные модификации в фундаментальной библиотеке потоков ввода/вывода (однако Java 2 не внесла фундаментальных модификаций). Когда вы видите классы Reader и Writer, вы сначала можете подумать (как и я), что они предназначены для замены классов InputStream и OutputStream. Но не в этом случае. Хотя некоторые аспекты начальной библиотеки потоков устарели и были заменены (если вы используете их, вы должны получать предупреждение компилятора), классы InputStream и OutputStream все еще обеспечивают ценную функциональность в форме байт-ориентированных систем ввода/вывода, в то время как классы Reader и Writer обеспечивают Unicode-совместимый, символьно ориентированный ввод/вывод. Кроме того:

  1. Java 1.1 добавил новые классы в иерархию InputStream и OutputStream, так что, очевидно, что эти классы не заменены.
  2. Иногда возникают ситуации, когда вы должны использовать классы из “byte” иерархии в комбинации с классами в “символьной” иерархии. Чтобы выполнить это, существуют классы - “мосты”: InputStreamReader преобразует InputStream к Reader, и OutputStreamWriter преобразует OutputStream к Writer.

Наиболее важная причина во введении иерархии Reader и Writer состоит в интернационализации. Старая иерархия потоков ввода/вывода поддерживает только 8-битные байтовые потоки и не обрабатывает 16 битные Unicode символы. Так как Unicode используется для интернационализации (и родной тип char в Java - это 16-bit Unicode), иерархия Reader и Writer были добавлены для поддержки Unicode и всех операций ввода/вывода. Кроме того, новые библиотеки были разработаны для ускорения операций по сравнению со старыми.

Как практикуется в этой книге, я попробую выполнить обзор классов, но вы должны принять во внимание, что вы будете использовать онлайн документацию для определения всех деталей, таких как исчерпывающий список методов.

Источники и приемники данных

Почти все оригинальные классы потоков ввода/вывода имеют соответствующие классы Reader и Writer для обеспечения родных манипуляций в Unicode. Однако есть некоторые места, где байт-ориентированные InputStream и OutputStream являются корректным решением; на практике библиотеки из java.util.zip скорее байт-ориентированные, чем символьно-ориентированные. Так что наиболее разумным подходом будет попытка использования классов Reader и Writer там, где это возможно, и вы обнаружите ситуации, когда будете вынуждены использовать байт-ориентированные библиотеки, потому что ваш код не будет компилироваться.

Здесь приведена таблица, которая показывает соответствие между источниками и приемниками информации (то есть, куда данные приходят на физическом уровне или куда они уходят) в двух иерархиях.

Источники и приемники: класс Java 1.0 Соответствующий класс Java 1.1
InputStream Reader
конвертер: InputStreamReader
OutputStream Writer
конвертер: OutputStreamWriter
FileInputStream FileReader
FileOutputStream FileWriter
StringBufferInputStream StringReader
(соответствующего класса нет) StringWriter
ByteArrayInputStream CharArrayReader
ByteArrayOutputStream CharArrayWriter
PipedInputStream PipedReader
PipedOutputStream PipedWriter

В общем случае вы обнаружите, что интерфейсы для этих двух различных иерархий сходны, если не идентичны.

Модификация поведения потока

Потоки InputStream и OutputStream адаптируются под определенные требования с использованием “декорирующих” подклассов FilterInputStream и FilterOutputStream. Классы иерархии Reader и Writer продолжают использовать эту идею — но не точно.

В приведенной таблице соответствие произведено с еще большим приближением, чем в предыдущей таблице. Различия происходят из-за организации классов: в то время как BufferedOutputStream является подклассом FilterOutputStream, BufferedWriter не является подклассом FilterWriter (который, не смотря на то, что он является абстрактным, не имеет подклассов и, таким образом, появляется помещенным в любой объект, а здесь упомянуть просто для того, чтобы вы не удивились, увидев его). Однако интерфейсы классов достаточно близки при сравнении.

Фильтры:
классы Java 1.0
Соответствующий класс Java 1.1
FilterInputStream FilterReader
FilterOutputStream FilterWriter (абстрактный класс без подклассов)
BufferedInputStream BufferedReader
(так же имеет readLine( ))
BufferedOutputStream BufferedWriter
DataInputStream Используйте DataInputStream
(За исключением случаев, когда вам нужно использовать readLine( ), в этом случае вы должны использовать BufferedReader)
PrintStream PrintWriter
LineNumberInputStream LineNumberReader
StreamTokenizer StreamTokenizer
(Используйте конструктор, принимающий Reader)
PushBackInputStream PushBackReader

Есть одно направление, которое достаточно понятно: Когда вы хотите использовать readLine( ), вы не должны более использовать DataInputStream (при этом вы встретитесь с сообщении об использовании устаревших методов во время компиляции), а вместо этого использовать BufferedReader. Тем не менее, DataInputStream все еще остается “привилегированным” членом библиотеки ввода/вывода.

Чтобы сделать переход к использованию PrintWriter более легким, он имеет конструктор, который принимает любой объект типа OutputStream, наряду с объектами Writer. Однако PrintWriter более не поддерживает форматирование, которое поддерживал PrintStream; интерфейс, фактически, остался тем же.

Конструктор PrintWriter также имеет необязательную опцию для выполнения автоматической выгрузки буферов, которая случается после каждого вызова println( ), если установлен флаг в конструкторе.

Неизмененные классы

Некоторые классы остались неизменными при переходе от Java 1.0 к Java 1.1:

Классы Java 1.0 не имеющие соответствующих классов в Java 1.1
DataOutputStream
File
RandomAccessFile
SequenceInputStream

DataOutputStream, в основном, используется без изменений, так что для хранения и получения данных в транспортабельном формате, используй те иерархии InputStream и OutputStream.

Сам по себе: RandomAccessFile

RandomAccessFile используется для файлов, содержащих записи известного размера, так что вы можете переместиться от одной записи к другой, используя seek( ), затем прочесть или изменить запись. Записи могут и не быть одинакового размера; вы просто способны определить их размер и их положение в файле.

Сначала немного трудно поверить, что RandomAccessFile не является частью иерархии InputStream или OutputStream. Однако он не имеет ассоциаций с этими иерархиями, за исключением того, что он реализует интерфейсы DataInput и DataOutput (которые так же реализуются DataInputStream и DataOutputStream). Он даже не использует любую функциональность существующих классов InputStream или OutputStream — это полностью отдельный класс, написанный для поиска, имеющий все свои собственные (в большинстве своем родные) методы. Объяснением этого может быть то, что RandomAccessFile имеет во многом отличающееся поведение по сравнению с остальными типами ввода/вывода, так как вы можете перемещаться вперед и назад в пределах файла. В любом случае, он стоит отдельно, как прямой потомок от Object.

По существу, RandomAccessFile работает как DataInputStream совмещенный с DataOutputStream, благодаря использованию методов getFilePointer( ) для нахождения местоположения в файле, seek( ) для перемещения в новую точку в файле и length( ) для определения максимального размера файла. Кроме того, конструктор требует второй аргумент (что идентично fopen( ) в C), указывающий будите ли вы производить только чтение в произвольном порядке (“r”) или чтение и запись (“rw”). Нет поддержки для файлов только для чтения, что может сказать о том, что RandomAccessFile мог бы хорошо работать, если он наследовался бы от DataInputStream.

Метод поиска есть только у RandomAccessFile, который работает только с файлами. BufferedInputStream позволяет вам выполнять маркировку позиции с помощью метода mark( ) (чье значение содержится в единственной внутренней переменной) и сброс этой позиции методом reset( ), но это ограничено и не очень полезно.

Типичное использование потоков ввода/вывода

Хотя вы можете комбинировать классы потоков ввода/вывода многими различными способами, вы, вероятно, будете использовать несколько комбинаций. Следующий пример может быть использован как отправная точка; он показывает создание и использование типичной конфигурации ввода/вывода. Обратите внимание, что каждая конфигурация начинается с порядкового номера и заголовка, который оглавляет соответствующее объяснение в следующем за ним тексте.

//: c11:IOStreamDemo.java
// Типичные конфигурации потоков ввода/вывода.
import java.io.*;

public class IOStreamDemo {
  // Выбрасывание исключения на консоль:
  public static void main(String[] args) 
  throws IOException {
    // 1. Чтение ввода по строкам:
    BufferedReader in =
      new BufferedReader(
        new FileReader("IOStreamDemo.java"));
    String s, s2 = new String();
    while((s = in.readLine())!= null)
      s2 += s + "\n";
    in.close();

    // 1b. Чтение стандартного ввода:
    BufferedReader stdin =
      new BufferedReader(
        new InputStreamReader(System.in));      
    System.out.print("Enter a line:");
    System.out.println(stdin.readLine());

    // 2. Ввод из памяти
    StringReader in2 = new StringReader(s2);
    int c;
    while((c = in2.read()) != -1)
      System.out.print((char)c);

    // 3. Форматированный ввод из памяти
    try {
      DataInputStream in3 =
        new DataInputStream(
          new ByteArrayInputStream(s2.getBytes()));
      while(true)
        System.out.print((char)in3.readByte());
    } catch(EOFException e) {
      System.err.println("End of stream");
    }

    // 4. Вывод в файл
    try {
      BufferedReader in4 =
        new BufferedReader(
          new StringReader(s2));
      PrintWriter out1 =
        new PrintWriter(
          new BufferedWriter(
            new FileWriter("IODemo.out")));
      int lineCount = 1;
      while((s = in4.readLine()) != null )
        out1.println(lineCount++ + ": " + s);
      out1.close();
    } catch(EOFException e) {
      System.err.println("End of stream");
    }

    // 5. Хранение и перекрытие данных
    try {
      DataOutputStream out2 =
        new DataOutputStream(
          new BufferedOutputStream(
            new FileOutputStream("Data.txt")));
      out2.writeDouble(3.14159);
      out2.writeChars("That was pi\n");
      out2.writeBytes("That was pi\n");
      out2.close();
      DataInputStream in5 =
        new DataInputStream(
          new BufferedInputStream(
            new FileInputStream("Data.txt")));
      BufferedReader in5br =
        new BufferedReader(
          new InputStreamReader(in5));
      // Необходимо использовать DataInputStream для данных:
      System.out.println(in5.readDouble());
      // Теперь можно использовать "правильный" readLine():
      System.out.println(in5br.readLine());
      // Но выводимая строка забавна.
      // Строка, созданная с помощью writeBytes, в порядке:
      System.out.println(in5br.readLine());
    } catch(EOFException e) {
      System.err.println("End of stream");
    }

    // 6. Чтение/запись файлов в произвольном порядке
    RandomAccessFile rf =
      new RandomAccessFile("rtest.dat", "rw");
    for(int i = 0; i < 10; i++)
      rf.writeDouble(i*1.414);
    rf.close();

    rf =
      new RandomAccessFile("rtest.dat", "rw");
    rf.seek(5*8);
    rf.writeDouble(47.0001);
    rf.close();

    rf =
      new RandomAccessFile("rtest.dat", "r");
    for(int i = 0; i < 10; i++)
      System.out.println(
        "Value " + i + ": " +
        rf.readDouble());
    rf.close();
  }
} ///:~

Здесь приведено описание для нумерованных разделов программы:

Потоки ввода

Части с 1 по 4 демонстрируют создание и использование потоков ввода. Часть 4 также показывает простое использование потока вывода.

1. Буферизированный ввод из файла

Для открытия файла для ввода символов вы используете FileInputReader с объектом String или File в качестве имени файла. Для быстрой работы вы можете захотеть, чтобы файл был буферизированный, поэтому вы передаете результирующую ссылку в конструктор BufferedReader. BufferedReader также обеспечивает метод readLine( ), так что это ваш конечный объект и интерфейс, из которого вы читаете. Когда вы достигаете конца файла, readLine( ) возвращает null, что используется для окончания цикла while.

String s2 использует для аккумулирования всего содержимого файла (включая символы новой строки, которые должны добавляться, поскольку readLine( ) отбрасывает их). s2 далее используется в следующих частях этой программы. В конце вызывается close( ) для закрытия файла. Технически, close( ) будет вызвано при запуске finalize( ), а это произойдет (не зависимо от того произойдет или нет сборка мусора) при выходе из программы. Однако это было реализовано неустойчиво, поэтому безопасным подходом является явный вызов close( ) для файлов.

Раздел 1b показывает, как вы можете использовать System.in для чтения консольного ввода. System.in является DataInputStream и для BufferedReader необходим аргумент Reader, так что InputStreamReader вовлекается для выполнения перевода.

2. Ввод из памяти

Эта секция берет String s2, которая теперь включает все содержимое файла, и использует его для создания StringReader. Затем используется read( ) для чтения каждого символа, один символ за обращение, который посылается на консоль. Обратите, что read( ) возвращает следующий байт как int, поэтому он должен быть приведен к типу char для правильной печати.

3. Форматированный ввод из памяти

Для чтения “форматированных” данных вы используете DataInputStream, который является байт-ориентированным классом ввода/вывода (а не символьно-ориентированным). Таким образом, вы должны использовать все классы InputStream, а не классы Reader. Конечно, вы можете читать все, что угодно (также как и файл) байтами, используя классы InputStream, но здесь используется String. Для преобразования String в массив байт, который является подходящим для ByteArrayInputStream, String имеет метод getBytes( ), чтобы сделать эту работу. В этой точке вы имеете соответствующий InputStream для управления DataInputStream.

Если вы читаете символы из DataInputStream по одному байту, используя readByte( ), любое байтовое значение является допустимым результатом, так что возвращаемое значение не может использоваться для обнаружения конца ввода. Вместо этого вы можете использовать метод available( ) для нахождения как много символов доступно. Вот пример, который показывает, как читать файл по одному байту:

//: c11:TestEOF.java
// Проверка на конец файла
// при чтении по одному байту.
import java.io.*;

public class TestEOF {
  // Выбрасывается исключение на консоль:
  public static void main(String[] args) 
  throws IOException {
    DataInputStream in = 
      new DataInputStream(
       new BufferedInputStream(
        new FileInputStream("TestEof.java")));
    while(in.available() != 0)
      System.out.print((char)in.readByte());
  }
} ///:~

Обратите внимание, что available( ) работает по разному в зависимости от сорта носителя, из которого вы читаете; буквально - “это число байт, которые могут быть прочитаны без блокировки”. Для файлов это означает весь файл, но для другого вида потоков это может не быть правдой, так что используйте его осторожно.

Вы также можете определить конец ввода в таком случае, как здесь, при поимке исключения. Однако использование исключений для управления выполнением программы, рассматривается как злоупотребление этой особенностью.

4. Вывод в файл

Этот пример также показывает, как писать данные в файл. Сначала создается FileWriter для соединения с файлом. Фактически, вы всегда будете буферизировать вывод, обернув его с помощью BufferedWriter (попробуйте удалить эту обертку, чтобы посмотреть влияние на производительность — буферизация позволяет значительно увеличить производительность операций ввода/вывода). Затем, для форматирование объект включен в PrintWriter. Файл данных, созданный этим способом, читаем, как обычный текстовый файл.

Когда строки записываются в файл, добавляются номера строк. Обратите внимание, что LineNumberInputStream не используется, потому что он слабый и вам не нужен. Как показано здесь, достаточно просто хранить свою историю номеров строк.

Когда входной поток исчерпан, readLine( ) возвращает null. Вы увидите явный вызов close( ) для out1, в противном случае, если вы не вызовите close( ) для всех своих выходных файлов, вы можете обнаружить, что данные из буферов не вытолкнуты, поэтому файлы не завершенные.

Выходные потоки

Два первичных вида потоков вывода делятся по способу записи данных: одни пишут их для потребления людей, а другие пишут данные для повторного использования с DataInputStream. RandomAccessFile стоит в стороне, хотя его формат данных совместим с DataInputStream и DataOutputStream.

5. Сохранение и возврат

PrintWriter форматирует данные так, чтобы их читали люди. Однако для вывода данных в виде, чтобы они могли быть возвращены в другой поток, используйте DataOutputStream для записи данных, а DataInputStream для обратного получения данных. Конечно, эти потоки могут быть всем, что угодно, но здесь используется файл, буферизируемый и для чтения, и для записи. DataOutputStream и DataInputStream являются байт-ориентированными и поэтому требуют потоков InputStream и OutputStream.

Если вы используете DataOutputStream для записи данных, то Java гарантирует, что вы можете безошибочно повторно задействовать данные, используя DataInputStream — не зависимо от различий платформ для записи и чтения данных. Это невероятно ценно, как могут подтвердить те, кто потратил время, заботясь о платформозависимых путях движения данными. Эти проблемы снимаются, если вы имеете Java на обеих платформах. [58]

Обратите внимание, что строки символов записываются с использованием как writeChars( ), так и writeBytes( ). Когда вы запустите программу, вы обнаружите, что выводит 16-битные символы Unicode. Когда вы читаете строки, используя readLine( ), вы увидите, что есть пространство между символами, потому что каждый дополнительный байт вставляется из-за Unicode. Так как нет дополнительного метода “readChars” для DataInputStream, вы вынуждены вытягивать символы по одному с помощью readChar( ). Так что для ASCII легче написать символы байтами, за которым следует новая строка, а затем использовать readLine( ) для чтения байтов, как обычной ASCII cтроки.

writeDouble( ) сохраняет числа типа double в потоке, а дополнительный метод readDouble( ) получает их обратно (есть аналогичные методы для чтения и записи остальных типов). Но для корректной работы с любым читающим методом вы должны знать точное положение элемента данных в потоке, чтобы было одинаково возможно читать хранимое double, как простую последовательность байт, или как char, и т.п. Таким образом, вы должны либо иметь фиксированный формат для данных в файле, или в файле должна хранится дополнительная информация, которую вы обработаете для определения местоположения данных.

6. Чтение и запись файлов произвольного доступа

Как было замечено ранее, RandomAccessFile почти полностью изолирован от оставшейся иерархии ввода/вывода, и подтвержден тот факт, что он реализует интерфейсы DataInput и DataOutput. Поэтому вы не можете комбинировать его с любыми другими аспектами подклассов InputStream и OutputStream. Даже при том, что имело бы смысл трактовать ByteArrayInputStream, как элемент произвольного доступа, вы можете использовать RandomAccessFile только для открытия файла. Вы должны иметь в виду, что RandomAccessFile буферизирован должным образом, так что вам не нужно заботится об этом.

Одну из настроек вы имеете во втором конструкторе аргумента: вы можете открыть RandomAccessFile для чтения (“r”) или для чтения и записи (“rw”).

Использование RandomAccessFile аналогично использования комбинации DataInputStream и DataOutputStream (потому что он реализует эквивалентные интерфейсы). Кроме того, вы можете видеть, что seek( ) используется для перемещения в файле и изменения одного значения на другое.

Ошибка?

Если вы взглянете на раздел 5, вы увидите, что данные записываются перед текстом. Дело в том, что эта проблема была представлена в Java 1.1 (и сохранилась в Java 2), я был уверен, что это ошибка. Когда я сообщил об этом людям, занимающимся ошибками в JavaSoft, они сказали мне, что это, Проблема показана в следующем коде:

//: c11:IOProblem.java
// Java 1.1 и высшая проблема ввода/вывода.
import java.io.*;

public class IOProblem {
  // Исключение выбрасывается на консоль:
  public static void main(String[] args) 
  throws IOException {
    DataOutputStream out =
      new DataOutputStream(
        new BufferedOutputStream(
          new FileOutputStream("Data.txt")));
    out.writeDouble(3.14159);
    out.writeBytes("That was the value of pi\n");
    out.writeBytes("This is pi/2:\n");
    out.writeDouble(3.14159/2);
    out.close();

    DataInputStream in =
      new DataInputStream(
        new BufferedInputStream(
          new FileInputStream("Data.txt")));
    BufferedReader inbr =
      new BufferedReader(
        new InputStreamReader(in));
    // Double, записанное ПЕРЕД текстом
    // считывается правильно:
    System.out.println(in.readDouble());
    // Читаем строки текста:
    System.out.println(inbr.readLine());
    System.out.println(inbr.readLine());
    // Попытка читать double после строки
    // производит исключение конца файла:
    System.out.println(in.readDouble());
  }
} ///:~

Кажется что все, что вы пишите после вызова writeBytes( ) не возвращаемо. Ответ, очевидно, тот же, что и в случае старой шутки водителя: “Доктор, мне больно, когда я делаю это!” “Так не делайте этого!”

Потоки в виде трубопровода

PipedInputStream, PipedOutputStream, PipedReader и PipedWriter будут упомянуты только вскользь в этой главе. Это не означает, что они бесполезны, но их значение не будет очевидно, пока вы не поймете многонитевые процессы, так как потоки в виде трубопровода используются для общения между нитями. Это будет освещено в примере Главы 14.

Стандартный ввод/вывод

Термин стандартный ввод/вывод относится к концепции Unix (которая в некоторой форме была воспроизведена в Windows и многих других операционных системах) единого потока информации, который используется программой. Весь ввод программы может вестись через стандартный ввод, весь вывод может идти в стандартный вывод, а все сообщения об ошибках могут посылаться в стандартный поток ошибок. Значение стандартного ввода/вывода в том, что программы легко могут представлять цепочку вместе, и стандартный вывод одной программы может стать стандартным вводом для другой. Это достаточно мощный инструмент.

Чтение из стандартного ввода

Стандартная модель ввода/вывода в Java имеет System.in, System.out и System.err. На протяжении всей этой книге вы видели, как писать в стандартный вывод, используя System.out, который представляет собой объект PrintStream. System.err аналогичен PrintStream, а System.in является производной InputStream без каких-либо включений. Это означает, что в то время, когда вы можете использовать System.out и System.err как они есть, System.in должен куда-то включаться (быть обернут), прежде, чем вы сможете прочесть из него.

Обычно вы захотите читать ввод построчно, используя readLine( ), так что вы захотите поместить System.in в BufferedReader. Чтобы сделать это, вы можете конвертировать System.in в Reader, используя InputStreamReader. Вот пример, который просто повторяет каждую строку, которую вы печатаете:

//: c11:Echo.java
// Как читать стандартный ввод.
import java.io.*;

public class Echo {
  public static void main(String[] args)
  throws IOException {
    BufferedReader in =
        new BufferedReader(
          new InputStreamReader(System.in));
    String s;
    while((s = in.readLine()).length() != 0)
      System.out.println(s);
    // Пустая строка прерывает выполнение программы
  }
} ///:~

Причина указания исключения в том, что readLine( ) может выбросить IOException. Обратите внимание, что System.in обычно должен быть буферизирован, как и большинство потоков.

Замена System.out на PrintWriter

System.out - это PrintStream, который является OutputStream. PrintWriter имеет конструктор, который принимает в качестве аргумента OutputStream. Таким образом, если вы хотите конвертировать System.out в PrintWriter, используйте этот конструктор:

//: c11:ChangeSystemOut.java
// Перевод System.out в PrintWriter.
import java.io.*;

public class ChangeSystemOut {
  public static void main(String[] args) {
    PrintWriter out = 
      new PrintWriter(System.out, true);
    out.println("Hello, world");
  }
} ///:~

Важно использовать двухаргументную версию конструктора PrintWriter и установить второй аргумент в true, чтобы позволить автоматическое освобождение буфера, в противном случае вы можете не увидеть вывода.

Перенаправление стандартного ввода/вывода

Класс Java System позволяет вам перенаправлять стандартный ввод, вывод и поток вывода ошибок, используя простой вызов статического метода:

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

Перенаправление вывода особенно полезно, если вы неожиданно начнете создание большого объема для вывода на экран, а он будет скроллироваться гораздо быстрее, чем выбудете успевать читать.[59] Перенаправление ввода важно для программ командной строки, в которых вы захотите протестировать определенные последовательности пользовательского ввода несколько раз. Вот пример, показывающий использование этих методов:

//: c11:Redirecting.java
// Демонстрация перенаправления стандартного ввода/вывода.
import java.io.*;

class Redirecting {
  // Исключение выбрасывается на консоль:
  public static void main(String[] args) 
  throws IOException {
    BufferedInputStream in = 
      new BufferedInputStream(
        new FileInputStream(
          "Redirecting.java"));
    PrintStream out =
      new PrintStream(
        new BufferedOutputStream(
          new FileOutputStream("test.out")));
    System.setIn(in);
    System.setOut(out);
    System.setErr(out);

    BufferedReader br = 
      new BufferedReader(
        new InputStreamReader(System.in));
    String s;
    while((s = br.readLine()) != null)
      System.out.println(s);
    out.close(); // Помните об этом!
  }
} ///:~

Эта программа соединяет стандартный ввод с файлом и перенаправляет стандартный вывод и стандартные ошибки в другой файл.

Перенаправление ввода/вывода управляет потоками байт, а не потоками символов, то есть, скорее, используются InputStream и OutputStream, чем Reader и Writer.

Компрессия

Библиотека ввода/вывода Java содержит классы, поддерживающие чтение и запись потоков в компрессированном формате. Они являются оберткой для существующих классов ввода/вывода для обеспечения возможности компрессирования.

Эти классы не наследуются от классов Reader и Writer, а вместо этого они являются частью иерархии InputStream и OutputStream. Это происходит потому, что библиотека компрессии работает с байтами, а не с символами. Однако вы можете иногда встретить необходимость смешивания двух типов потоков. (Помните, что вы можете использовать InputStreamReader и OutputStreamWriter для обеспечения простой конвертации одного типа в другой.)

Классы компрессии Функция
CheckedInputStream GetCheckSum( ) производит контрольную сумму для любого InputStream (только не декомпрессию).
CheckedOutputStream GetCheckSum( ) производит контрольную сумму для любого OutputStream (только не декомпрессию).
DeflaterOutputStream Базовый класс для классов компрессии.
ZipOutputStream DeflaterOutputStream, который компрессирует данные в файл формата Zip.
GZIPOutputStream DeflaterOutputStream, который компрессирует данные в файл формата GZIP.
InflaterInputStream Базовый класс для классов декомпрессии.
ZipInputStream InflaterInputStream, который декомпрессирует данные, хранящиеся в файле формата Zip.
GZIPInputStream InflaterInputStream, который декомпрессирует данные, хранящиеся в файле формата GZIP.

Хотя существует много алгоритмов компрессии, Zip и GZIP, вероятно, наиболее часто используемые. Поэтому вы можете легко манипулировать вашими компрессированными данными с помощью многих инструментов, существующих для чтения и записи этих форматов.

Простая компрессия с помощью GZIP

Интерфейс GZIP прост, и поэтому он является более подходящим, когда вы имеете единственный поток данных, которые хотите компрессировать (в отличие от случая, когда вы имеете кусочки разнородных данных). Здесь приведен пример компрессии единичного файла:

//: c11:GZIPcompress.java
// Использование GZIP компрессии для компрессирования
// файла, имя которого получается из командной строки.
import java.io.*;
import java.util.zip.*;

public class GZIPcompress {
  // Исключение выбрасываются на консоль:
  public static void main(String[] args) 
  throws IOException {
    BufferedReader in =
      new BufferedReader(
        new FileReader(args[0]));
    BufferedOutputStream out =
      new BufferedOutputStream(
        new GZIPOutputStream(
          new FileOutputStream("test.gz")));
    System.out.println("Writing file");
    int c;
    while((c = in.read()) != -1)
      out.write(c);
    in.close();
    out.close();
    System.out.println("Reading file");
    BufferedReader in2 =
      new BufferedReader(
        new InputStreamReader(
          new GZIPInputStream(
            new FileInputStream("test.gz"))));
    String s;
    while((s = in2.readLine()) != null)
      System.out.println(s);
  }
} ///:~

Использование классов компрессии достаточно понятно — вы просто оборачиваете ваш поток вывода в GZIPOutputStream или ZipOutputStream, а ваш поток ввода в GZIPInputStream или ZipInputStream. Все остальное - это обычные операции чтения и записи. Это пример смешивания символьно-ориентированных потоков и байт-ориентированных потоков: in использует класс Reader, несмотря на то, что конструктор GZIPOutputStream может принимать только объекты OutputStream, а не объекты Writer. Когда файл будет открыт, GZIPInputStream конвертируется в Reader.

Многофайловое хранение с использованием Zip

Библиотека, поддерживающая Zip формат, более обширная. С ее помощью вы можете легко хранить множественные файлы, есть даже отдельные файлы, которые делают легким процесс чтения Zip файла. Библиотека использует стандартный Zip формат, так что он может работать совместно со всеми инструментами, которые доступны в Internet. Следующий пример имеет ту же форму, что и предыдущий, но он обрабатывает столько аргументов командной строки, сколько вы захотите. Кроме того, он показывает использование классов Checksum для подсчета и проверки контрольной суммы для файла. Есть два типа Checksum : Adler32 (который быстрее) и CRC32 (который медленнее, но немного более аккуратный).

//: c11:ZipCompress.java
// Использует компрессию Zip для компрессии любого
// числа файлов, переданных из командной строки.
import java.io.*;
import java.util.*;
import java.util.zip.*;

public class ZipCompress {
  // Исключение выбрасывается на консоль:
  public static void main(String[] args) 
  throws IOException {
    FileOutputStream f =
      new FileOutputStream("test.zip");
    CheckedOutputStream csum =
      new CheckedOutputStream(
        f, new Adler32());
    ZipOutputStream out =
      new ZipOutputStream(
        new BufferedOutputStream(csum));
    out.setComment("A test of Java Zipping");
    // Хотя нет соответствующего getComment().
    for(int i = 0; i < args.length; i++) {
      System.out.println(
        "Writing file " + args[i]);
      BufferedReader in =
        new BufferedReader(
          new FileReader(args[i]));
      out.putNextEntry(new ZipEntry(args[i]));
      int c;
      while((c = in.read()) != -1)
        out.write(c);
      in.close();
    }
    out.close();
    // Контрольная сумма действительна только после
    // того, как файл будет закрыт!
    System.out.println("Checksum: " +
      csum.getChecksum().getValue());
    // Теперь вытянем файлы:
    System.out.println("Reading file");
    FileInputStream fi =
       new FileInputStream("test.zip");
    CheckedInputStream csumi =
      new CheckedInputStream(
        fi, new Adler32());
    ZipInputStream in2 =
      new ZipInputStream(
        new BufferedInputStream(csumi));
    ZipEntry ze;
    while((ze = in2.getNextEntry()) != null) {
      System.out.println("Reading file " + ze);
      int x;
      while((x = in2.read()) != -1)
        System.out.write(x);
    }
    System.out.println("Checksum: " +
      csumi.getChecksum().getValue());
    in2.close();
    // Альтернативный способ для открытия и чтения
    // zip файлов:
    ZipFile zf = new ZipFile("test.zip");
    Enumeration e = zf.entries();
    while(e.hasMoreElements()) {
      ZipEntry ze2 = (ZipEntry)e.nextElement();
      System.out.println("File: " + ze2);
      // ... и вытягиваем данные, как и раньше
    }
  }
} ///:~

Для каждого файла, добавляемого в архив, вы должны вызвать putNextEntry( ) и передать ему объект ZipEntry. Объект ZipEntry содержит обширный интерфейс, который позволит вам получить и установить все данные, доступные для этого конкретного включения в ваш Zip файл: имя, компрессированный и не компрессированный размеры, дата, CRC контрольная сумма, дополнительное поле данных, комментарий, метод компрессии и есть ли включаемые директории. Однако, хотя формат Zip имеет возможность установки пароля, это не поддерживается в Zip библиотеке Java. И хотя CheckedInputStream и CheckedOutputStream поддерживают обе контрольные суммы Adler32 и CRC32, класс ZipEntry поддерживает только интерфейс для CRC. Это является ограничением лежащего в основе Zip формата, и это может ограничить вас в использовании быстрого Adler32.

Для извлечения файлов ZipInputStream имеет метод getNextEntry( ), который возвращает следующий ZipEntry, если он существует. В качестве более краткой альтернативы, вы можете читать файл, используя объект ZipFile, который имеет метод entries( ), возвращающий Enumeration из ZipEntries.

Для чтения контрольной суммы вы должны как-то получить доступ к ассоциированному объекту Checksum. Здесь получается ссылка на объекты CheckedOutputStream и CheckedInputStream, но вы также могли просто владеть ссылкой на объект Checksum.

Трудным методом для потоков Zip является setComment( ). Как показано выше, вы можете установить комментарий, когда вы записываете файл, но нет способа получить коментарий в ZipInputStream. Комментарии появились для полной поддержки базиса включение-за-включением только через ZipEntry.

Конечно, вы не ограничены в файлах, когда используете библиотеки GZIP или Zip — вы можете компрессировать все, что угодно, включая данные для посылки через сетевое соединение.

Java архивы (JAR'ы)

Формат Zip также используется в файле, формата JAR (Java ARchive), который является способом сбора группы файлов в один компрессированный файл, так же как и Zip. Однако, как и все остальное в Java, JAR файлы являются кроссплатформенными, так что вам не нужно беспокоится о возможностях платформы. Вы также можете включить звуковой и графический файл наряду с файлами классов.

JAR файлы обычно полезны, когда вы имеете дело с Internet. До появления JAR файлов ваш Web броузер делал повторяющиеся запросы к Web серверу для загрузки всех файлов, из которых состоит апплет. Кроме того, каждый из этих файлов был не компрессирован. При сборе всех этих файлов для определенного апплета в единый JAR файл необходим только один запрос к серверу, а передача пройдет быстрее из-за компрессии. А каждое включение в JAR файл может иметь цифровую подпись для безопасности (обратитесь за деталями к документации по Java).

JAR файл состоит из единого файла, содержащего набор файлов, упакованных с помощью Zip, наряду с “манифестом”, который описывает их. (Вы можете создать свой собственный файл манифеста; в противном случае программа jar сделает это за вас.) Вы можете найти больше информации о файлах манифеста JAR в HTML документации для JDK.

Утилита jar, пришедшая вместе с JDK от Sun, автоматически компрессирует файлы по вашему выбору. Вы можете вызвать ее из командной строки:

jar [options] destination [manifest] inputfile(s)

Опции - это просто набор символов (не нужно ни дефисов, ни другой индикации). Пользователи Unix/Linux заметят сходство с опциями tar. Вот они:

c Создает новый или пустой архив.
t Список содержания.
x Извлечь все файлы.
x file Извлекает указанный файл.
f Говорит: “Я дам тебе имя файла”. Если вы не используете это, jar поймет, что ввод должен идти через стандартный ввод или, если создается файл, вывод происходит через стандартный вывод.
m Говорит о том, что первый аргумент будет именем файла манифеста, созданного пользователем.
v Генерирует подробный вывод, описывающий то, что делает jar.
0 Только хранение файлов; не компрессирует файлы (используйте для создания JAR файла, который вы можете поместить в ваш classpath).
M

Не выполняется автоматическое создание файла манифеста.

Если поддиректории включаются в файлы, помещаемые в JAR файл, эти поддиректории добавляются автоматически, включая все вложенные поддиректории и т.д. Информация о пути тоже сохраняется.

Вот типичный способ вызова jar:

jar cf myJarFile.jar *.class

Это создает JAR файл, называемый myJarFile.jar, содержащий все файлы классов из текущей директории наряду с автоматически сгенерированным файлом манифеста.

jar cmf myJarFile.jar myManifestFile.mf *.class

Как и в предыдущем примере, но добавляется файл манифеста, созданный пользователем. Он называется myManifestFile.mf.

jar tf myJarFile.jar

Производится содержание файла myJarFile.jar.

jar tvf myJarFile.jar

Добавляет флаг “verbose”, чтобы получить более детальную информацию о файлах в myJarFile.jar.

jar cvf myApp.jar audio classes image

Принимая во внимание, что audio, classes и image являются поддиректориями, таким образом, все собирается в файл myApp.jar. Также включен флаг “verbose”, чтобы иметь обратную связь, пока работает программа jar.

Если вы создаете JAR файл, используя опцию 0, такой файл может быть помещен в ваш CLASSPATH:

CLASSPATH="lib1.jar;lib2.jar;"

После этого Java может искать файлы lib1.jar и lib2.jar.

Инструмент jar не является таким же полезным, как утилита zip. Например, вы не можете добавить или обновить файлы существующего JAR файла; вы можете создать JAR файл только с самого начала. Также вы не можете переместить файл в JAR файл и стереть его сразу, как только он будет перемещен. Однако JAR файл, созданный на одной платформе, может быть прочитан инструментом jar на любой другой платформе (проблема, которая иногда надоедает с утилитой zip).

Как вы увидите в Главе 13, JAR файлы также используются для упаковки JavaBeans.

Сериализация объектов

Сериализация объектов Java позволяет вам взять любой объект, который реализует интерфейс Serializable и включит его в последовательность байт, которые могут быть полностью восстановлены для регенерации оригинального объекта. Это также выполняется при передаче по сети, что означает, что механизм сериализации автоматически поддерживается на различных операционных системах. То есть, вы можете создать объект на машине с Windows, сериализовать его и послать по сети на Unix машину, где он будет корректно реконструирован. Вам не нужно будет беспокоиться о представлении данных на различных машинах, порядке следования байт и любых других деталях.

Сама по себе сериализация объектов очень интересна, потому что это позволяет вам реализовать устойчивую живучесть. Помните, что живучесть означает, что продолжительность жизни объектов не определяется тем, выполняется ли программа — объекты живут в промежутках между вызовами программы. Вы берете сериализованный объект и записываете его на диск, затем восстанавливаете объект при новом вызове программы, таким образом, вы способны обеспечить эффективную живучесть. Причина названия “устойчивая” в том, что вы не можете просто определить объект, используя какой-либо вид ключевого слова “устойчивый”, и позволить системе заботиться о деталях (хотя это может случиться в будущем). Вместо этого вы должны явно сериализовать и десериализовать объекты в вашей программе.

Сериализация объектов была добавлена в язык для поддержки двух главных особенностей. Удаленный вызов методов (RMI) в Java позволяет объектам существовать на другой машине и вести себя так, как будто они существуют на вашей машине. Когда посылается сообщение удаленному объекту, необходима сериализация объекта для транспортировки аргументов и возврата значений. RMI обсуждается в Главе 15.

Сериализация объектов так же необходима для JavaBeans, описанных в Главе 13. Когда используется компонент (Bean), информация о его состоянии обычно конфигурируется во время дизайна. Эта информации о состоянии должна сохранятся, а затем восстанавливаться, когда программа запускается; cериализация объектов выполняет эту задачу.

Сериализация объекта достаточно проста, если объект реализует интерфейс Serializable (этот интерфейс похож на флаг и не имеет методов). Когда сериализация была добавлена в язык, многие стандартные библиотеки классов были изменены, чтобы сделать их сериализованными, включая все оболочки примитивных типов, все контейнерные классы и многие другие. Даже объект Class может быть сериализован. (Смотрите Главу 12 о реализации этого.)

Для сериализации объекта вы создаете определенный сорт объекта OutputStream, а затем вкладываете его в объект ObjectOutputStream. После этого вам достаточно вызвать writeObject( ) и ваш объект будет сериализован и послан в OutputStream. Чтобы провести обратный процесс, вы вкладываете InputStream внутрь ObjectInputStream и вызываете readObject( ). То, что приходит, обычно это ссылка на родительский Object, так что вы должны выполнить обратное приведение, чтобы сделать вещи правильными.

Особенно полезное свойство сериализации объектов состоит в том, что при этом сохраняется не только образ объекта, а за ним также следуют все ссылки, содержащиеся в вашем объекте. Эти объекты также сохраняются, а за ними следуют все ссылки из каждого объекта, и т.д. Иногда это называется “паутиной объектов”, так как единственный объект может быть присоединен к чему-то, и может содержать массив ссылок на объекты точно так же, как и на члены объектов. Если вы поддерживаете собственную схему сериализации объектов, код, поддерживающий все эти ссылки, может свести с ума. Однако сериализация объектов в Java, по видимому, осуществляет это безупречно, используя, несомненно, оптимизированный алгоритм, который исследует всю паутину объектов. Следующий пример проверяет механизм сериализации, создавая “цепочку” связанных объектов, каждый из которых имеет ссылку на следующий сегмент цепочки точно так же, как и массив ссылок указывает на объекты различных классов:

//: c11:Worm.java
// Демонстрация сериализации объектов.
import java.io.*;

class Data implements Serializable {
  private int i;
  Data(int x) { i = x; }
  public String toString() {
    return Integer.toString(i);
  }
}

public class Worm implements Serializable {
  // Генерируется случайно значение типа int:
  private static int r() {
    return (int)(Math.random() * 10);
  }
  private Data[] d = {
    new Data(r()), new Data(r()), new Data(r())
  };
  private Worm next;
  private char c;
  // Значение i == Номеру сегмента
  Worm(int i, char x) {
    System.out.println(" Worm constructor: " + i);
    c = x;
    if(--i > 0)
      next = new Worm(i, (char)(x + 1));
  }
  Worm() {
    System.out.println("Default constructor");
  }
  public String toString() {
    String s = ":" + c + "(";
    for(int i = 0; i < d.length; i++)
      s += d[i].toString();
    s += ")";
    if(next != null)
      s += next.toString();
    return s;
  }
  // Исключение выбрасывается на консоль:
  public static void main(String[] args) 
  throws ClassNotFoundException, IOException {
    Worm w = new Worm(6, 'a');
    System.out.println("w = " + w);
    ObjectOutputStream out =
      new ObjectOutputStream(
        new FileOutputStream("worm.out"));
    out.writeObject("Worm storage");
    out.writeObject(w);
    out.close(); // Также очищается вывод
    ObjectInputStream in =
      new ObjectInputStream(
        new FileInputStream("worm.out"));
    String s = (String)in.readObject();
    Worm w2 = (Worm)in.readObject();
    System.out.println(s + ", w2 = " + w2);
    ByteArrayOutputStream bout =
      new ByteArrayOutputStream();
    ObjectOutputStream out2 =
      new ObjectOutputStream(bout);
    out2.writeObject("Worm storage");
    out2.writeObject(w);
    out2.flush();
    ObjectInputStream in2 =
      new ObjectInputStream(
        new ByteArrayInputStream(
          bout.toByteArray()));
    s = (String)in2.readObject();
    Worm w3 = (Worm)in2.readObject();
    System.out.println(s + ", w3 = " + w3);
  }
} ///:~

Чтобы сделать пример интереснее, массив объектов Data внутри Worm инициализируется случайными числами. (Этот способ не дает компилятору представление о типе хранимой мета информации.) Каждый сегмент цепочки (Worm) помечается символом (char), который генерируется автоматически в процессе рекурсивной генерации связанного списка Worm. Когда вы создаете Worm, вы говорите конструктору необходимую вам длину. Чтобы сделать следующую ссылку (next), вызывается конструктор Worm с длиной на единичку меньше, и т.д. Последняя ссылка next остается равной null, указывая на конец цепочки Worm.

Все это сделано для создания чего-то достаточно сложного, что не может быть легко сериализовано. Однако действия, направленные на сериализацию, достаточно просты. Как только создается объект ObjectOutputStream из некоторого другого потока, writeObject( ) сериализует объект. Обратите внимание, что вызов writeObject( ) для String такой же. Вы также можете записать все примитивные типы, используя тот же метод DataOutputStream (они задействуют тот же интерфейс).

Здесь есть две различные секции кода, которые выглядят одинаково. Первая пишет и читает файл, а вторая, для разнообразия, пишет и читает ByteArray. Вы можете прочесть и записать объект, используя сериализацию для любого DataInputStream или DataOutputStream, включая, как вы увидите в Главе 15, сеть. Вывод после одного запуска имеет вид:

Worm constructor: 6
Worm constructor: 5
Worm constructor: 4
Worm constructor: 3
Worm constructor: 2
Worm constructor: 1
w = :a(262):b(100):c(396):d(480):e(316):f(398)
Worm storage, w2 = :a(262):b(100):c(396):d(480):e(316):f(398)
Worm storage, w3 = :a(262):b(100):c(396):d(480):e(316):f(398)

Вы можете видеть, что десериализованный объект на самом деле содержит все ссылки, которые были в оригинальном объекте.

Обратите внимание, что в процессе десериализации объекта Serializable не вызывается ни конструктор, ни даже конструктор по умолчанию.

Сериализация объектов является byte-ориентированной, и поэтому используется иерархия InputStream и OutputStream.

Нахождение класса

Вы можете задаться вопросом, что необходимо для восстановления объекта из его сериализованного состояния. Например, предположим, вы сериализовали объект и послали его в файл или по сети на другую машину. Может ли программа на другой машине реконструировать объект, используя только содержимое файла?

Лучшим способом для ответа на этот вопрос (как обычно) будет проведение эксперимента. Следующий файл содержится в поддиректории для этой главы:

//: c11:Alien.java
// Сериализуемый класс.
import java.io.*;

public class Alien implements Serializable {
} ///:~

Файл, который создает и сериализует объект Alien, содержится в том же директории:

//: c11:FreezeAlien.java
// Создает файл сериализации.
import java.io.*;

public class FreezeAlien {
  // Выбрасывает исключение на консоль:
  public static void main(String[] args) 
  throws IOException {
    ObjectOutput out = 
      new ObjectOutputStream(
        new FileOutputStream("X.file"));
    Alien zorcon = new Alien();
    out.writeObject(zorcon); 
  }
} ///:~

С точки зрения поимки и обработки исключений, эта программа выбрала быстрый и грязный способ передачи исключений наружу main( ), так что информация о них будет помещаться в командной строке.

Как только программа будет скомпилирована и запущена, скопируйте результирующий файл X.file в поддиректорий, под названием xfiles, где имеется следующий код:

//: c11:xfiles:ThawAlien.java
// Пробуем восстановить сериализованный файл
// без объекта класса, хранимого в файле.
import java.io.*;

public class ThawAlien {
  public static void main(String[] args) 
  throws IOException, ClassNotFoundException {
    ObjectInputStream in =
      new ObjectInputStream(
        new FileInputStream("X.file"));
    Object mystery = in.readObject();
    System.out.println(mystery.getClass());
  }
} ///:~

Эта программа открывает файл и успешно читает объект mystery. Однако, как только вы попробуете найти что-нибудь об объекте — что требует Class объекта для Alien — виртуальная машина Java (JVM) не сможет найти Alien.class (если он не будет указан в Classpath, чего не должно случится в этом примере). Вы получите ClassNotFoundException. (Еще раз: все свидетельства иной жизни исчезнут прежде, чем доказательства ее существования могут быть проверены!)

Если вы хотите многое сделать после восстановления объекта, который был сериализован, вы должны убедится, что JVM может найти соответствующий .class либо по локальному пути классов, либо где-то в Internet.

Управление сериализацией

Как вы можете видеть, стандартный механизм сериализации тривиален в использовании. Но что, если вам нужны специальные требования? Может быть, вы имеете особые требования по безопасности и вы не хотите сериализовать часть вашего объекта, или, может быть, не имеет смысла сериализовать один из подобъектов, если эта часть будет вновь создана при восстановлении объекта.

Вы можете управлять процессом сериализации, реализовав интерфейс Externalizable вместо интерфейса Serializable. Интерфейс Externalizable расширяет интерфейс Serializable и добавляет два метода: writeExternal( ) и readExternal( ), которые автоматически вызываются для вашего объекта во время сериализации и десериализации, так что вы можете выполнить специальные операции.

Следующий пример показывает простую реализацию методов интерфейса Externalizable. Обратите внимание, что Blip1 и Blip2 почти идентичны, за исключением тонких различий (проверьте, сможете ли вы найти их в коде):

//: c11:Blips.java
// Простое использование Externalizable & ловушка.
import java.io.*;
import java.util.*;

class Blip1 implements Externalizable {
  public Blip1() {
    System.out.println("Blip1 Constructor");
  }
  public void writeExternal(ObjectOutput out)
      throws IOException {
    System.out.println("Blip1.writeExternal");
  }
  public void readExternal(ObjectInput in)
     throws IOException, ClassNotFoundException {
    System.out.println("Blip1.readExternal");
  }
}

class Blip2 implements Externalizable {
  Blip2() {
    System.out.println("Blip2 Constructor");
  }
  public void writeExternal(ObjectOutput out)
      throws IOException {
    System.out.println("Blip2.writeExternal");
  }
  public void readExternal(ObjectInput in)
     throws IOException, ClassNotFoundException {
    System.out.println("Blip2.readExternal");
  }
}

public class Blips {
  // Исключения выбрасываются на консоль:
  public static void main(String[] args) 
  throws IOException, ClassNotFoundException {
    System.out.println("Constructing objects:");
    Blip1 b1 = new Blip1();
    Blip2 b2 = new Blip2();
    ObjectOutputStream o =
      new ObjectOutputStream(
        new FileOutputStream("Blips.out"));
    System.out.println("Saving objects:");
    o.writeObject(b1);
    o.writeObject(b2);
    o.close();
    // Теперь получаем их обратно:
    ObjectInputStream in =
      new ObjectInputStream(
        new FileInputStream("Blips.out"));
    System.out.println("Recovering b1:");
    b1 = (Blip1)in.readObject();
    // OOPS! Выброшено исключение:
//! System.out.println("Recovering b2:");
//! b2 = (Blip2)in.readObject();
  }
} ///:~

Вывод для этой программы:

Constructing objects:
Blip1 Constructor
Blip2 Constructor
Saving objects:
Blip1.writeExternal
Blip2.writeExternal
Recovering b1:
Blip1 Constructor
Blip1.readExternal

Причина того, что объект Blip2 не восстановлен в том, что происходит попытка сделать нечто, что является причиной исключения. Вы нашли различия между Blip1 и Blip2? Конструктор для Blip1 является public, в то время как конструктор для Blip2 не такой, и поэтому появляется исключение во время восстановления. Попробуйте сделать конструктор Blip2 public и удалите комментарии //!, чтобы увидеть корректный результат.

Когда восстанавливается b1, вызывается конструктор по умолчанию для Blip1. Это отличается от восстановления объекта с Serializable, в котором конструирование целиком происходит из сохраненных бит без вызова конструктора. Для объектов Externalizable проявляется обычное поведение конструктора по умолчанию (включая инициализацию в точке определения полей), а затем вызывается readExternal( ). Вы должны осознавать это — в частности, тот факт, что все конструкторы по умолчанию занимают свое место — для производства корректного поведения вашего объекта с Externalizable.

Вот пример, который показывает, что вы должны сделать для полного хранение и восстановления объекта с Externalizable:

//: c11:Blip3.java
// Реконструирование externalizable объекта.
import java.io.*;
import java.util.*;

class Blip3 implements Externalizable {
  int i;
  String s; // Без инициализации
  public Blip3() {
    System.out.println("Blip3 Constructor");
    // s, i не инициализируется
  }
  public Blip3(String x, int a) {
    System.out.println("Blip3(String x, int a)");
    s = x;
    i = a;
    // s & i инициализируются только в
    // конструкторе не по умолчанию.
  }
  public String toString() { return s + i; }
  public void writeExternal(ObjectOutput out)
  throws IOException {
    System.out.println("Blip3.writeExternal");
    // Вы обязаны сделать это:
    out.writeObject(s); 
    out.writeInt(i);
  }
  public void readExternal(ObjectInput in)
  throws IOException, ClassNotFoundException {
    System.out.println("Blip3.readExternal");
    // Вы обязаны сделать это:
    s = (String)in.readObject(); 
    i =in.readInt();
  }
  public static void main(String[] args)
  throws IOException, ClassNotFoundException {
    System.out.println("Constructing objects:");
    Blip3 b3 = new Blip3("A String ", 47);
    System.out.println(b3);
    ObjectOutputStream o =
      new ObjectOutputStream(
        new FileOutputStream("Blip3.out"));
    System.out.println("Saving object:");
    o.writeObject(b3);
    o.close();
    // Теперь получим обратно:
    ObjectInputStream in =
      new ObjectInputStream(
        new FileInputStream("Blip3.out"));
    System.out.println("Recovering b3:");
    b3 = (Blip3)in.readObject();
    System.out.println(b3);
  }
} ///:~

Поля s и i инициализируются только во втором конструкторе, но не в конструкторе по умолчанию. Это значит, что если вы не инициализируете s и i в readExternal( ), они будут равны null (так как хранилище объектов заполняется нулями при первом шаге создания объектов). Если вы закомментируете две строки кода, следующих за фразой “Вы обязаны сделать это”, и запустите программу, вы увидите, что при восстановлении объекта s равно null, а i равно нулю.

Если вы наследуете от объекта с интерфейсом Externalizable, обычно вы будете вызывать методы writeExternal( ) и readExternal( ) базового класса для обеспечения правильного хранения и восстановления компонент базового класса.

Таким образом, чтобы сделать все правильно, вы должны не только записать важные данные из объекта в методе writeExternal( ) (здесь нет стандартного поведения, при котором записывается любой член объекта с интерфейсом Externalizable), но вы также должны восстановить эти данные в методе readExternal( ). Сначала это может немного смущать, потому что поведение конструктора по умолчанию объекта с интерфейсом Externalizable может представить все, как некоторый вид автоматического сохранения и восстановления. Но это не так.

Ключевое слово transient

Когда вы управляете сериализацией, возможно появление определенных подобъектов, для который вы не захотите применять механизм сериализации Java для автоматического сохранения и восстановления. В основном это происходит в том случае, если такой подобъект представляет важную информацию, которую вы не хотите сериализовать, например, пароль. Даже если такая информация имеет модификатор private в объекте, так как она сериализуется, то кто-либо может получить доступ для чтения файла или перехватить сетевую передачу.

Один способ предохранения важной части вашего объекта от сериализации, заключающий в реализации Externalizable, показан в предыдущем разделе. Но при этом ничего автоматически не сериализуется и вы должны явно сеарилизовать только нужные вам части внутри writeExternal( ).

Однако если вы работаете с Serializable объектом, вся сериализация происходит автоматически. Для управления этим, вы можете выключить сериализацию полей индивидуально, используя ключевое слово transient, которое говорит: “Не беспокойтесь о сохранении и восстановлении этого — я позабочусь об этом”.

В качестве примера рассмотрим объект Login, хранящий информацию об определенной сессии подключения. Предположим, что как только вы проверили имя пользователя, вы хотите сохранить данные, но без пароля. Простейшим способом является реализация Serializable и пометка поля password ключевым словом transient. Вот как это выглядит:

//: c11:Logon.java
// Демонстрация ключевого слова "transient".
import java.io.*;
import java.util.*;

class Logon implements Serializable {
  private Date date = new Date();
  private String username;
  private transient String password;
  Logon(String name, String pwd) {
    username = name;
    password = pwd;
  }
  public String toString() {
    String pwd =
      (password == null) ? "(n/a)" : password;
    return "logon info: \n   " +
      "username: " + username +
      "\n   date: " + date +
      "\n   password: " + pwd;
  }
  public static void main(String[] args)
  throws IOException, ClassNotFoundException {
    Logon a = new Logon("Hulk", "myLittlePony");
    System.out.println( "logon a = " + a);
      ObjectOutputStream o =
        new ObjectOutputStream(
          new FileOutputStream("Logon.out"));
    o.writeObject(a);
    o.close();
    // Задержка:
    int seconds = 5;
    long t = System.currentTimeMillis()
           + seconds * 1000;
    while(System.currentTimeMillis() < t)
      ;
    // Теперь получаем его обратно:
    ObjectInputStream in =
      new ObjectInputStream(
        new FileInputStream("Logon.out"));
    System.out.println(
      "Recovering object at " + new Date());
    a = (Logon)in.readObject();
    System.out.println( "logon a = " + a);
  }
} ///:~

Вы можете видеть, что поля date и username являются обычными (не transient), и поэтому сериализуются автоматически. Однако поле password является transient, и поэтому не сохраняется на диске; так же механизм сериализации не делает попытку восстановить его. На выходе получаем:

logon a = logon info:
   username: Hulk
   date: Sun Mar 23 18:25:53 PST 1997
   password: myLittlePony
Recovering object at Sun Mar 23 18:25:59 PST 1997
logon a = logon info:
   username: Hulk
   date: Sun Mar 23 18:25:53 PST 1997
   password: (n/a)

Когда объект восстанавливается, поле password заполняется значением null. Обратите внимание, что toString( ) должна проверять значение на равенство null поля password, потому что если вы попробуете собрать объект String, используя перегруженный оператор ‘+’, а этот оператор обнаружит ссылку, равную null, вы получите NullPointerException. (Новые версии Java могут содержать код для предотвращения этой проблемы.)

Вы также можете заметить, что поле date сохраняется и восстановления с диска и не генерируется заново.

Так как объекты с интерфейсом Externalizable не сохраняют никакие из своих полей автоматически, поэтому ключевое слово transient используется только для объектов с интерфейсом Serializable.

Альтернатива Externalizable

Если вы не достаточно сильны в реализации интерфейса Externalizable, существует другой подход. Вы можете реализовать интерфейс Serializable и добавить (обратите внимание, я сказал “добавить”, а не “перекрыть” или “реализовать”) методы, называемые writeObject( ) и readObject( ), которые будут автоматически вызваны, когда объект будет, соответственно, сериализоваться и десериализоваться. То есть, если вы обеспечите эти два метода, они будут использоваться взамен сериализации по умолчанию.

Методы должны иметь следующие точные сигнатуры:

private void 
  writeObject(ObjectOutputStream stream)
    throws IOException;

private void 
  readObject(ObjectInputStream stream)
    throws IOException, ClassNotFoundException

С точки зрения дизайна, это мистические вещи. Прежде всего, вы можете подумать так, потому что эти методы не являются частью базового класса или интерфейса Serializable, следовательно, они не будут определены в своем собственном интерфейсе. Но обратите внимание, что они объявлены как private, что означает, что они будут вызываться только другим членом этого класса. Однако на самом деле вы не вызываете их из других членов этого класса, а вместо этого методы writeObject( ) и readObject( ), принадлежащие объекту ObjectOutputStream и ObjectInputStream, вызывают методы writeObject( ) и readObject( ) вашего объекта. (Обратите внимание на мою невероятную сдержанность, из-за которой я не пускаюсь в пространные обличительные речи по поводу использования одних и тех же имен методов здесь. Я просто скажу: путаница.) Вы можете быть удивлены, как объекты ObjectOutputStream и ObjectInputStream получают доступ к private методам вашего класса. Мы можем только иметь в виду, что эта часть составляет магию сериализации.

В любом случае, все, что определено в интерфейсе, автоматически становится public, поэтому, если writeObject( ) и readObject( ) должны быть private, то они не могут быть частью интерфейса. Так как вы должны следовать точным сигнатурам, получаемый эффект тот же самые, как если бы вы реализовали interface.

Может показаться, что когда вы вызываете ObjectOutputStream.writeObject( ), объект с интерфейсом Serializable, который вы передаете, опрашивается (используя рефлексию, не имеет значения) на предмет реализации своего собственного writeObject( ). Если это так, то нормальный процесс сериализации пропускается, и вызывается writeObject( ). Аналогичная ситуация наблюдается и для readObject( ).

Есть еще один поворот. Внутри вашего writeObject( ) вы можете выбрать выполнение стандартного действия writeObject( ), вызвав defaultWriteObject( ). Точно так же, внутри readObject( ) вы можете вызвать defaultReadObject( ). Вот пример, который демонстрирует, как вы можете управлять хранением и восстановлением объектов с интерфейсом Serializable:

//: c11:SerialCtl.java
// Управление сериализацией, путем добавления
// собственных методов writeObject() и readObject().
import java.io.*;

public class SerialCtl implements Serializable {
  String a;
  transient String b;
  public SerialCtl(String aa, String bb) {
    a = "Not Transient: " + aa;
    b = "Transient: " + bb;
  }
  public String toString() {
    return a + "\n" + b;
  }
  private void 
    writeObject(ObjectOutputStream stream)
      throws IOException {
    stream.defaultWriteObject();
    stream.writeObject(b);
  }
  private void 
    readObject(ObjectInputStream stream)
      throws IOException, ClassNotFoundException {
    stream.defaultReadObject();
    b = (String)stream.readObject();
  }
  public static void main(String[] args)
  throws IOException, ClassNotFoundException {
    SerialCtl sc = 
      new SerialCtl("Test1", "Test2");
    System.out.println("Before:\n" + sc);
    ByteArrayOutputStream buf = 
      new ByteArrayOutputStream();
    ObjectOutputStream o =
      new ObjectOutputStream(buf);
    o.writeObject(sc);
    // Теперь получим это назад:
    ObjectInputStream in =
      new ObjectInputStream(
        new ByteArrayInputStream(
          buf.toByteArray()));
    SerialCtl sc2 = (SerialCtl)in.readObject();
    System.out.println("After:\n" + sc2);
  }
} ///:~

В этом примере есть одно обычное поле String, а другое имеет модификатор transient, для обеспечения возможности сохранения не transient поля с помощью метода defaultWriteObject( ), а transient поля сохраняются и восстанавливаются явно. Поля инициализируются внутри конструктора, а не в точке определения, чтобы удостоверится, что они не инициализируются каким-либо автоматическим механизмом во время десериализации.

Если вы будете использовать стандартный механизм записи не transient частей вашего объекта, вы должны вызвать defaultWriteObject( ), как первое действие writeObject( ) и defaultReadObject( ), как первое действие readObject( ). Это странный вызов методов. Он может показать, например, что вы вызываете defaultWriteObject( ) для ObjectOutputStream и не передаете ему аргументов, но все же как-то происходит включение и узнавание ссылки на ваш объект и способа записи всех не transient частей. Мираж.

Для хранения и восстановления transient объектов используется более знакомый код. И еще, подумайте о том, что происходит тут. В main( ) создается объект SerialCtl, а затем он сериализуется в ObjectOutputStream. (Обратите внимание, что в этом случае используется буфер вместо файла — это все тот же ObjectOutputStream.) Сериализация происходит в строке:

o.writeObject(sc);

Метод writeObject( ) должен проверить sc на предмет существования собственного метода writeObject( ). (Не с помощью проверки интерфейса — здесь нет его — или типа класса, а реальной охотой за методом, используя рефлексию.) Если метод существует, он используется. Аналогичный подход используется для readObject( ). Возможно это чисто практический способ, которым можно решить проблему, но он, несомненно, странен.

Работа с версиями

Возможно, что вам захочется изменить версию сериализованного класса (объекты оригинального класса могут храниться, например, в базе данных). Это допустимо, но вы, вероятно, будете делать это только в специальных случаях, так как это требует дополнительного глубокого понимания, которого мы не достигнем здесь. Документация по JDK в формате HTML, доступная на java.sun.com, описывает эту тему достаточно полно.

Вы также должны обратить внимание, что в HTML документация JDK многие комментарии начинаются с предупреждения:

Внимание: Сериализованные объекты этого класса не будут совместимы с будущими выпусками Swing. Существующая поддержка сериализации подходит для кратковременного хранения или для RMI между приложениями. ...

Это происходит потому, что механизм работы с версиями слишком прост для надежной работы во всех ситуациях, особенно с JavaBeans. Он работает корректно для дизайна и это то, о чем говорит это предупреждение.

Использование устойчивости

Достаточно привлекательно использовать технологию сериализации для хранения некоторых состояний вашей программы, чтобы в последствии вы могли легко восстановить программу до текущего состояния. Но прежде, чем сделать это, необходимо ответить на некоторые вопросы. Что случится, если вы сериализуете два объекта, оба из которых имеют ссылки на один объект? Когда вы восстановите эти два объекта из их сериализованного состояния, будите ли вы иметь только один экземпляр третьего объекта? Что, если вы сериализуете два объекта в различные файлы, а десериализуете их в различных частях кода?

Вот пример, показывающий эту проблему:

//: c11:MyWorld.java
import java.io.*;
import java.util.*;

class House implements Serializable {}

class Animal implements Serializable {
  String name;
  House preferredHouse;
  Animal(String nm, House h) { 
    name = nm; 
    preferredHouse = h;
  }
  public String toString() {
    return name + "[" + super.toString() + 
      "], " + preferredHouse + "\n";
  }
}

public class MyWorld {
  public static void main(String[] args)
  throws IOException, ClassNotFoundException {
    House house = new House();
    ArrayList  animals = new ArrayList();
    animals.add(
      new Animal("Bosco the dog", house));
    animals.add(
      new Animal("Ralph the hamster", house));
    animals.add(
      new Animal("Fronk the cat", house));
    System.out.println("animals: " + animals);

    ByteArrayOutputStream buf1 = 
      new ByteArrayOutputStream();
    ObjectOutputStream o1 =
      new ObjectOutputStream(buf1);
    o1.writeObject(animals);
    o1.writeObject(animals); // Запись второго класса
    // Запись в другой поток:
    ByteArrayOutputStream buf2 = 
      new ByteArrayOutputStream();
    ObjectOutputStream o2 =
      new ObjectOutputStream(buf2);
    o2.writeObject(animals);
    // Теперь получаем назад:
    ObjectInputStream in1 =
      new ObjectInputStream(
        new ByteArrayInputStream(
          buf1.toByteArray()));
    ObjectInputStream in2 =
      new ObjectInputStream(
        new ByteArrayInputStream(
          buf2.toByteArray()));
    ArrayList animals1 = 
      (ArrayList)in1.readObject();
    ArrayList animals2 = 
      (ArrayList)in1.readObject();
    ArrayList animals3 = 
      (ArrayList)in2.readObject();
    System.out.println("animals1: " + animals1);
    System.out.println("animals2: " + animals2);
    System.out.println("animals3: " + animals3);
  }
} ///:~

Одна вещь, которая интересна здесь, состоит в возможности использовать сериализацию объекта через массив байт, как способ выполнения “глубокого копирования” любого объекта с интерфейсом Serializable. (Глубокое копирование означает, что вы дублируете всю паутину объектов, а не просто основной объект и принадлежащие ему ссылки.) Более глубоко копирование освещено в Приложении А.

Объекты Animal содержат поля типа House. В main( ) создается ArrayList из этих Animal, и он сериализуется дважды в один поток, а затем снова в другой поток. Когда это десериализуется и распечатается, вы увидите следующий результат одного запуска (объекты будут располагаться в разных участках памяти при каждом запуске):

animals: [Bosco the dog[Animal@1cc76c], House@1cc769
, Ralph the hamster[Animal@1cc76d], House@1cc769
, Fronk the cat[Animal@1cc76e], House@1cc769
]
animals1: [Bosco the dog[Animal@1cca0c], House@1cca16
, Ralph the hamster[Animal@1cca17], House@1cca16
, Fronk the cat[Animal@1cca1b], House@1cca16
]
animals2: [Bosco the dog[Animal@1cca0c], House@1cca16
, Ralph the hamster[Animal@1cca17], House@1cca16
, Fronk the cat[Animal@1cca1b], House@1cca16
]
animals3: [Bosco the dog[Animal@1cca52], House@1cca5c
, Ralph the hamster[Animal@1cca5d], House@1cca5c
, Fronk the cat[Animal@1cca61], House@1cca5c
]

Конечно, вы ожидаете, что десериализованные объекты имеют адреса, отличные от первоначальных. Но обратите внимание, что в animals1 и animals2 появляется один и тот же адрес, включая ссылки на объект House, который они оба разделяют. С другой стороны, когда восстанавливается animals3, у системы нет способа узнать, что объекты в этом потоке являются алиасами объектов первого потока, так что при этом создается полностью отличная паутина объектов.

Если вы сериализовали что-то в единственный поток, вы будете способны восстановить ту же паутину объектов, которую вы записали, без случайного дублирования объектов. Конечно, вы можете изменить состояние ваших объектов в промежутке между временем первой и последней записи, но это ваше дело — объекты будут записаны не зависимо от того, в каком бы состоянии они не были (и со всеми соединениями, которые они имеют с другими объектами) в то время, когда вы сериализуете их.

Самым безопасным для сохранение состояния системы является сериализация, как “атомная” операция. Если вы сериализуете какие-то вещи, выполняете какую-то работу и сериализуйте еще, и т.д., то вы не будете держать систему в безопасности. Вместо этого поместите все объекты, которые относятся к состоянию вашей системы, в единственный контейнер и просто запишите этот контейнер в одной операции. Затем вы можете восстановить его так же единственным вызовом метода.

Следующий пример относится к мнимой вспомогательной системе компьютерного дизайна (CAD), который демонстрирует такой подход. Кроме того, здесь примешана проблема полей static — если вы взглянете на документацию, вы увидите, что если Class является сериализуемым, то должно быть легким хранение static поля простой сериализацией объекта Class. Тем не менее, такой подход выглядит достаточно важным.

//: c11:CADState.java
// Запись и восстановление состояния
// симулятора системы CAD.
import java.io.*;
import java.util.*;

abstract class Shape implements Serializable {
  public static final int 
    RED = 1, BLUE = 2, GREEN = 3;
  private int xPos, yPos, dimension;
  private static Random r = new Random();
  private static int counter = 0;
  abstract public void setColor(int newColor);
  abstract public int getColor();
  public Shape(int xVal, int yVal, int dim) {
    xPos = xVal;
    yPos = yVal;
    dimension = dim;
  }
  public String toString() {
    return getClass() + 
      " color[" + getColor() +
      "] xPos[" + xPos +
      "] yPos[" + yPos +
      "] dim[" + dimension + "]\n";
  }
  public static Shape randomFactory() {
    int xVal = r.nextInt() % 100;
    int yVal = r.nextInt() % 100;
    int dim = r.nextInt() % 100;
    switch(counter++ % 3) {
      default: 
      case 0: return new Circle(xVal, yVal, dim);
      case 1: return new Square(xVal, yVal, dim);
      case 2: return new Line(xVal, yVal, dim);
    }
  }
}

class Circle extends Shape {
  private static int color = RED;
  public Circle(int xVal, int yVal, int dim) {
    super(xVal, yVal, dim);
  }
  public void setColor(int newColor) { 
    color = newColor;
  }
  public int getColor() { 
    return color;
  }
}

class Square extends Shape {
  private static int color;
  public Square(int xVal, int yVal, int dim) {
    super(xVal, yVal, dim);
    color = RED;
  }
  public void setColor(int newColor) { 
    color = newColor;
  }
  public int getColor() { 
    return color;
  }
}

class Line extends Shape {
  private static int color = RED;
  public static void 
  serializeStaticState(ObjectOutputStream os)
      throws IOException {
    os.writeInt(color);
  }
  public static void 
  deserializeStaticState(ObjectInputStream os)
      throws IOException {
    color = os.readInt();
  }
  public Line(int xVal, int yVal, int dim) {
    super(xVal, yVal, dim);
  }
  public void setColor(int newColor) { 
    color = newColor;
  }
  public int getColor() { 
    return color;
  }
}

public class CADState {
  public static void main(String[] args) 
  throws Exception {
    ArrayList shapeTypes, shapes;
    if(args.length == 0) {
      shapeTypes = new ArrayList();
      shapes = new ArrayList();
      // Добавляем ссылку в объект класса:
      shapeTypes.add(Circle.class);
      shapeTypes.add(Square.class);
      shapeTypes.add(Line.class);
      // Создаем какие-то образы:
      for(int i = 0; i < 10; i++)
        shapes.add(Shape.randomFactory());
      // Устанавливаем все статические цвета в GREEN:
      for(int i = 0; i < 10; i++)
        ((Shape)shapes.get(i))
          .setColor(Shape.GREEN);
      // Запись вектора состояния:
      ObjectOutputStream out =
        new ObjectOutputStream(
          new FileOutputStream("CADState.out"));
      out.writeObject(shapeTypes);
      Line.serializeStaticState(out);
      out.writeObject(shapes);
    } else { // Есть аргументы командной строки
      ObjectInputStream in =
        new ObjectInputStream(
          new FileInputStream(args[0]));
      // Читаем в том же порядке, в котором была запись:
      shapeTypes = (ArrayList)in.readObject();
      Line.deserializeStaticState(in);
      shapes = (ArrayList)in.readObject();
    }
    // Отображаем образы:
    System.out.println(shapes);
  }
} ///:~

Класс Shape реализует интерфейс Serializable, так что все, что наследуется от Shape, автоматически реализует Serializable. Каждый Shape содержит данные, а каждый наследуемый от Shape класс содержит статическое поле, определяющее цвет всех этих Shape. (Помещение статического поля в базовый класс приведет к тому, что будет существовать только одно поле, так как статическое поле не дублируется для наследуемых классов.) Методы базового класса могут быть перекрыты для установки цвета для различных типов (статические методы не имеют динамических ограничений, так что это обычные методы). Метод randomFactory( ) создает различные объекты Shape при каждом вызове, используя случайные значения для данных Shape.

Circle и Square являются прямым расширением Shape; отличия только в том, что Circle инициализирует color в точке определения, а Square инициализирует его в конструкторе. Дискуссию относительно Line пока отложим.

В main( ) используется один ArrayList для хранения объектов Class, а другой для хранения образов. Если вы не задействовали аргумент командной строки, создается shapeTypes ArrayList, и добавляются объекты Class, а затем создается ArrayList shapes, и в него добавляются объекты Shape. Далее, все значения static color устанавливаются равными GREEN, и все сериализуется в файл CADState.out.

Если вы укажите аргумент командной строки (предположительно CADState.out), этот файл будет открыт и использован для восстановления состояния программы. В обеих ситуациях распечатывается результирующий ArrayList из Shape. Вот результат одного запуска:

>java CADState
[class Circle color[3] xPos[-51] yPos[-99] dim[38]
, class Square color[3] xPos[2] yPos[61] dim[-46]
, class Line color[3] xPos[51] yPos[73] dim[64]
, class Circle color[3] xPos[-70] yPos[1] dim[16]
, class Square color[3] xPos[3] yPos[94] dim[-36]
, class Line color[3] xPos[-84] yPos[-21] dim[-35]
, class Circle color[3] xPos[-75] yPos[-43] dim[22]
, class Square color[3] xPos[81] yPos[30] dim[-45]
, class Line color[3] xPos[-29] yPos[92] dim[17]
, class Circle color[3] xPos[17] yPos[90] dim[-76]
]

>java CADState CADState.out
[class Circle color[1] xPos[-51] yPos[-99] dim[38]
, class Square color[0] xPos[2] yPos[61] dim[-46]
, class Line color[3] xPos[51] yPos[73] dim[64]
, class Circle color[1] xPos[-70] yPos[1] dim[16]
, class Square color[0] xPos[3] yPos[94] dim[-36]
, class Line color[3] xPos[-84] yPos[-21] dim[-35]
, class Circle color[1] xPos[-75] yPos[-43] dim[22]
, class Square color[0] xPos[81] yPos[30] dim[-45]
, class Line color[3] xPos[-29] yPos[92] dim[17]
, class Circle color[1] xPos[17] yPos[90] dim[-76]
]

Вы можете видеть, что значения xPos, yPos и dim были успешно сохранены и восстановлены, но при восстановлении статической информации произошли какие-то ошибки. Везде на входе имели “3”, но на выходе этого не получили. Circle имеет значение 1 (RED, как это определено), а Square имеет значение 0 (Помните, что он инициализировался в конструкторе). Это похоже на то, что static не сериализовался совсем! Это верно, несмотря на то, что класс Class реализует интерфейс Serializable, он не делает того, что вы от него ожидаете. Так что если вы хотите сериализовать statics, вы должны сделать это сами.

Это то, для чего нужны статические методы serializeStaticState( ) и deserializeStaticState( ) в Line. Вы можете видеть, что они явно вызываются как часть процесса сохранения и восстановления. (Обратите внимание, что порядок записи в файл сериализации и чтения из него должен сохранятся). Таким образом, чтобы CADState.java работал корректно, вы должны:

  1. Добавить serializeStaticState( ) и deserializeStaticState( ) к образам.
  2. Удалить ArrayList shapeTypes и весь код, относящийся к нему.
  3. Добавить вызов новых статических методов сериализации и десериализации образов.

Другую проблему вы можете получить, думая о безопасности, так как сериализация сохраняет данные с модификатором private. Если вы имеете проблемы безопасности, эти поля должны помечаться, как transient. Затем вы должны разработать безопасный способ для хранения такой информации, чтобы когда вы делали восстановление, вы могли установить эти private переменные.

Токенизация(Tokenizing) ввода

Tokenizing - это процесс разбивания последовательности символов на последовательность значащих элементов (“tokens”), которые являются кусочками текста, разделенных чем-либо по вашему выбору. Например, ваши значащие элементы могут быть словами, разделенными пробелом и пунктуацией. Есть два класса, обеспечиваемых стандартной библиотекой Java, которые могут использоваться для токенизации: StreamTokenizer и StringTokenizer.

StreamTokenizer

Хотя StreamTokenizer не наследуется от InputStream или OutputStream, он работает только с объектами InputStream, так что он по праву принадлежит библиотеке ввода/вывода.

Рассмотрим программу, подсчитывающую встречающихся слов в текстовом файле:

//: c11:WordCount.java
// Подсчет слов в файле, выводит
// результат в отсортированном порядке.
import java.io.*;
import java.util.*;

class Counter {
  private int i = 1;
  int read() { return i; }
  void increment() { i++; }
}

public class WordCount {
  private FileReader file;
  private StreamTokenizer st;
  // TreeMap хранит ключи в отсортированном порядке:
  private TreeMap counts = new TreeMap();
  WordCount(String filename)
    throws FileNotFoundException {
    try {
      file = new FileReader(filename);
      st = new StreamTokenizer(
        new BufferedReader(file));
      st.ordinaryChar('.');
      st.ordinaryChar('-');
    } catch(FileNotFoundException e) {
      System.err.println(
        "Could not open " + filename);
      throw e;
    }
  }
  void cleanup() {
    try {
      file.close();
    } catch(IOException e) {
      System.err.println(
        "file.close() unsuccessful");
    }
  }
  void countWords() {
    try {
      while(st.nextToken() !=
        StreamTokenizer.TT_EOF) {
        String s;
        switch(st.ttype) {
          case StreamTokenizer.TT_EOL:
            s = new String("EOL");
            break;
          case StreamTokenizer.TT_NUMBER:
            s = Double.toString(st.nval);
            break;
          case StreamTokenizer.TT_WORD:
            s = st.sval; // Уже String
            break;
          default: // единственный символ в ttype
            s = String.valueOf((char)st.ttype);
        }
        if(counts.containsKey(s))
          ((Counter)counts.get(s)).increment();
        else
          counts.put(s, new Counter());
      }
    } catch(IOException e) {
      System.err.println(
        "st.nextToken() unsuccessful");
    }
  }
  Collection values() {
    return counts.values();
  }
  Set keySet() { return counts.keySet(); }
  Counter getCounter(String s) {
    return (Counter)counts.get(s);
  }
  public static void main(String[] args) 
  throws FileNotFoundException {
    WordCount wc =
      new WordCount(args[0]);
    wc.countWords();
    Iterator keys = wc.keySet().iterator();
    while(keys.hasNext()) {
      String key = (String)keys.next();
      System.out.println(key + ": "
               + wc.getCounter(key).read());
    }
    wc.cleanup();
  }
} ///:~

Представление слов в сортированном виде проще выполнить при хранении данных в TreeMap, который автоматически организует ключи в сортированном порядке (смотрите Главу 9). Когда вы получите набор ключей, используя keySet( ), они также будут отсортированы.

Для открытия файла используется FileReader, а для деления файла на слова, создается StreamTokenizer из FileReader, помещенного в BufferedReader. Для StreamTokenizer, существует стандартный список разделителей, и вы можете добавить еще с помощью нескольких методов. Здесь используется ordinaryChar( ) для того, чтобы сказать: “Этот символ не является тем, чем я интересуюсь”, так что синтаксический анализатор не будет включать его, как часть любого слова, которые он создает. Например, фраза st.ordinaryChar('.') означает, что точка не будет включаться, как часть анализируемого слова. Вы можете найти более подробную информацию в HTML документации по JDK на java.sun.com.

В countWords( ) значащие элементы извлекаются по одному, далее используется информация ttype для определения, что нужно делать с каждым значащим элементом, так как он может быть переводом строки, числом, строкой или единичным символом.

Как только значащий элемент будет найден, опрашивается TreeMap counts на предмет проверки, содержится ли этот элемент как ключевое значение. Если это так, инкрементируется соответствующий объект Counter, указывающий что был найден еще один экземпляр найденного слова. Если нет, создается новый Counter — так как конструктор Counter инициализирует свое значение единицей, то при этом также происходит подсчет слов.

WordCount не является типом TreeMap, так как она не была унаследована. Она выполняет определенный тип функциональности, так что даже хотя методы keys( ) и values( ) должны быть открытыми, это все еще не означает, что должно использоваться наследование, так как некоторые методы TreeMap здесь не подходят. Кроме того, другие методы, такие как getCounter( ), возвращающие Counter для определенной String, и sortedKeys( ), производящие Iterator, завершают изменения в интерфейсе WordCount.

В main( ) вы можете видеть использование WordCount для открытия и подсчета слов в файле — это занимает всего две строчки кода. Затем извлекается итератор сортированного списка ключей (слов), который используется для получения каждого ключа и ассоциированного Count. Вызов cleanup( ) необходим, чтобы быть уверенным в закрытии файла.

StringTokenizer

Хотя он не является частью библиотеки ввода/вывода, StringTokenizer имеет во многом сходную функциональность, что и описанный здесь StreamTokenizer.

StringTokenizer возвращает значащие элементы из строки по одной. Эти значащие элементы являются последовательностью символов, разделенных символами табуляции, пробелами и символами перевода строки. Таким образом, значащими элементами строки “Куда делась моя кошка?” являются “Куда”, “делась”, “моя” и “кошка?”. Как и в случае StreamTokenizer, вы можете настроить StringTokenizer, чтобы он разбивал ввод любым способом, который вам нужен, но с помощью StringTokenizer вы можете сделать это, передав второй аргумент в конструктор, который имеет тип String и является разделителем, который вы хотите использовать. В общем, если вам нужна большая изощренность, используйте StreamTokenizer.

Вы запрашиваете у объекта StringTokenizer следующий значащий элемент строки, используя метод nextToken( ), который возвращает либо следующий значащий элемент, либо пустую строку, которая указывает, что более элементов не осталось.

В качестве примера рассмотрим программу, которая выполняет ограниченный анализ предложения, ища ключевые фразы, указывающие на выражения счастья или огорчения.

//: c11:AnalyzeSentence.java
// Поиск определенных последовательностей в предложении.
import java.util.*;

public class AnalyzeSentence {
  public static void main(String[] args) {
    analyze("I am happy about this");
    analyze("I am not happy about this");
    analyze("I am not! I am happy");
    analyze("I am sad about this");
    analyze("I am not sad about this");
    analyze("I am not! I am sad");
    analyze("Are you happy about this?");
    analyze("Are you sad about this?");
    analyze("It's you! I am happy");
    analyze("It's you! I am sad");
  }
  static StringTokenizer st;
  static void analyze(String s) {
    prt("\nnew sentence >> " + s);
    boolean sad = false;
    st = new StringTokenizer(s);
    while (st.hasMoreTokens()) {
      String token = next();
      // Поиск идет до тех пор, пока вы
      // не найдете одну из двух начальных элементов:
      if(!token.equals("I") &&
         !token.equals("Are"))
        continue; // В начала цикла while
      if(token.equals("I")) {
        String tk2 = next();
        if(!tk2.equals("am")) // Должно быть после Я
          break; // Выход из цикла while
        else {
          String tk3 = next();
          if(tk3.equals("sad")) {
            sad = true;
            break; // Выход из цикла while
          }
          if (tk3.equals("not")) {
            String tk4 = next();
            if(tk4.equals("sad"))
              break; // Leave sad false
            if(tk4.equals("happy")) {
              sad = true;
              break;
            }
          }
        }
      }
      if(token.equals("Are")) {
        String tk2 = next();
        if(!tk2.equals("you"))
          break; // Должно быть после Are
        String tk3 = next();
        if(tk3.equals("sad"))
          sad = true;
        break; // Выход из цикла while
      }
    }
    if(sad) prt("Sad detected");
  }
  static String next() {
    if(st.hasMoreTokens()) {
      String s = st.nextToken();
      prt(s);
      return s;
    } 
    else
      return "";
  }
  static void prt(String s) {
    System.out.println(s);
  }
} ///:~

Анализ происходит для каждой строки, происходит вход в цикл while и из строки извлекается значащий элемент. Обратите внимание, что первая инструкция if, которая командует continue (вернуться назад к началу цикла и начать его заново), если значащий элемент не является ни словом "I", ни “Are”. Это означает, что будут извлекаться значащие элементы до тех пор, пока не будет найдено “I” или “Are”. Вы можете решить, что нужно использовать == вместо метода equals( ), но этот оператор не будет работать корректно, так как == сравнивает значения ссылок, а метод equals( ) сравнивает содержимое.

Логика оставшейся части метода analyze( ) заключается в поиске шаблона, с которого начинается фраза “I am sad”, “I am not happy” или “Are you sad?”. Без использования инструкции break этот код был бы еще грязнее, чем он есть. Вы должны знать, что типичный синтаксический анализатор (это примитивный пример одного из них) обычно имеет таблицу таких значащих элементов и часть кода, проходящую по всем состояниям таблицы, после чтения каждого элемента.

Вы должны думать, что StringTokenizer является стенографическим упрощением для определенного вида StreamTokenizer. Однако если вы имеете String, которую вы хотите разбить на элементы, StringTokenizer является слишком ограниченным, все, что вам нужно сделать - это перевести строку в StringBufferInputStream, а затем использовать его для создания более мощного StreamTokenizer.

Проверка стиля капитализации

В этом разделе мы взглянем на более сложный пример использования ввода/вывода в Java, который также использует токенизацию. Этот проект весьма полезен, потому что он выполняет проверку стиля, чтобы убедится, что ваша капитализация соответствует стилю Java, который можно найти на java.sun.com/docs/codeconv/index.html. Он открывает .java файл в текущем директории и извлекает все имена классов и идентификаторов, затем показывает, если какой-то из них не соответствует стилю Java.

Для тех программ, которые откроются корректно, вы сначала должны построить хранилище имен классов для хранения всех имен классов из стандартной библиотеки Java. Вы делаете это путем прохождения по всем поддиректориям с исходным кодом стандартной библиотеки Java и запуском ClassScanner в каждой поддиректории. В качестве аргумента получается файл хранилища (каждый раз используется один и тот же путь и одно и тоже имя), к опция командной строки -a указывает, что имена классов должны добавляться в хранилище.

Используя программу для проверки вашего кода, передайте ей имя хранилища для использования. Она проверит все классы и идентификаторы в текущем директории, и скажет вам, какие из них не следуют типичному стилю капитализации Java.

Вы должны знать, что программа не является точной; есть несколько моментов, когда она будет указывать на то, что она считает проблемой, но, взглянув на код, вы увидите, что ничего не нужно менять. Это немного раздражает, но это гораздо легче, чем пытаться найти все эти случаи, пристально вглядываясь в код.

//: c11:ClassScanner.java
// Сканирует все файлы в директории в поисках
// классов и идентификаторов для проверки капитализации.
// Принимает правильно составленные списки кода.
// Не все делает правильно, но достаточно хороший помощник.
import java.io.*;
import java.util.*;

class MultiStringMap extends HashMap {
  public void add(String key, String value) {
    if(!containsKey(key))
      put(key, new ArrayList());
    ((ArrayList)get(key)).add(value);
  }
  public ArrayList getArrayList(String key) {
    if(!containsKey(key)) {
      System.err.println(
        "ERROR: can't find key: " + key);
      System.exit(1);
    }
    return (ArrayList)get(key);
  }
  public void printValues(PrintStream p) {
    Iterator k = keySet().iterator();
    while(k.hasNext()) {
      String oneKey = (String)k.next();
      ArrayList val = getArrayList(oneKey);
      for(int i = 0; i < val.size(); i++)
        p.println((String)val.get(i));
    }
  }
}

public class ClassScanner {
  private File path;
  private String[] fileList;
  private Properties classes = new Properties();
  private MultiStringMap 
    classMap = new MultiStringMap(),
    identMap = new MultiStringMap();
  private StreamTokenizer in;
  public ClassScanner() throws IOException {
    path = new File(".");
    fileList = path.list(new JavaFilter());
    for(int i = 0; i < fileList.length; i++) {
      System.out.println(fileList[i]);
      try {
        scanListing(fileList[i]);
      } catch(FileNotFoundException e) {
        System.err.println("Could not open " +
          fileList[i]);
      }
    }
  }
  void scanListing(String fname) 
  throws IOException {
    in = new StreamTokenizer(
        new BufferedReader(
          new FileReader(fname)));
    // Кажется, не работает:
    // in.slashStarComments(true);
    // in.slashSlashComments(true);
    in.ordinaryChar('/');
    in.ordinaryChar('.');
    in.wordChars('_', '_');
    in.eolIsSignificant(true);
    while(in.nextToken() != 
          StreamTokenizer.TT_EOF) {
      if(in.ttype == '/')
        eatComments();
      else if(in.ttype == 
              StreamTokenizer.TT_WORD) {
        if(in.sval.equals("class") || 
           in.sval.equals("interface")) {
          // Получаем имя класса:
             while(in.nextToken() != 
                   StreamTokenizer.TT_EOF
                   && in.ttype != 
                   StreamTokenizer.TT_WORD)
               ;
             classes.put(in.sval, in.sval);
             classMap.add(fname, in.sval);
        }
        if(in.sval.equals("import") ||
           in.sval.equals("package"))
          discardLine();
        else // Это идентификатор или ключевое слово
          identMap.add(fname, in.sval);
      }
    }
  }
  void discardLine() throws IOException {
    while(in.nextToken() != 
          StreamTokenizer.TT_EOF
          && in.ttype != 
          StreamTokenizer.TT_EOL)
      ; // Выбрасываем элемент в конец строки
  }
  // Кажется, что метод удаления комментариев StreamTokenizer
  // сломан. Это извлекает комментарии:
  void eatComments() throws IOException {
    if(in.nextToken() != 
       StreamTokenizer.TT_EOF) {
      if(in.ttype == '/')
        discardLine();
      else if(in.ttype != '*')
        in.pushBack();
      else 
        while(true) {
          if(in.nextToken() == 
            StreamTokenizer.TT_EOF)
            break;
          if(in.ttype == '*')
            if(in.nextToken() != 
              StreamTokenizer.TT_EOF
              && in.ttype == '/')
              break;
        }
    }
  }
  public String[] classNames() {
    String[] result = new String[classes.size()];
    Iterator e = classes.keySet().iterator();
    int i = 0;
    while(e.hasNext())
      result[i++] = (String)e.next();
    return result;
  }
  public void checkClassNames() {
    Iterator files = classMap.keySet().iterator();
    while(files.hasNext()) {
      String file = (String)files.next();
      ArrayList cls = classMap.getArrayList(file);
      for(int i = 0; i < cls.size(); i++) {
        String className = (String)cls.get(i);
        if(Character.isLowerCase(
             className.charAt(0)))
          System.out.println(
            "class capitalization error, file: "
            + file + ", class: " 
            + className);
      }
    }
  }
  public void checkIdentNames() {
    Iterator files = identMap.keySet().iterator();
    ArrayList reportSet = new ArrayList();
    while(files.hasNext()) {
      String file = (String)files.next();
      ArrayList ids = identMap.getArrayList(file);
      for(int i = 0; i < ids.size(); i++) {
        String id = (String)ids.get(i);
        if(!classes.contains(id)) {
          // Игнорирует идентификаторы длиной 3 или
          // более символов, если они все в верхнем регистре
          // (эероятно это значения static final):
          if(id.length() >= 3 &&
             id.equals(
               id.toUpperCase()))
            continue;
          // Проверяется, записан ли первый символ в верхнем регистре:
          if(Character.isUpperCase(id.charAt(0))){
            if(reportSet.indexOf(file + id)
                == -1){ // Еще не включено в отчет
              reportSet.add(file + id);
              System.out.println(
                "Ident capitalization error in:"
                + file + ", ident: " + id);
            }
          }
        }
      }
    }
  }
  static final String usage =
    "Usage: \n" + 
    "ClassScanner classnames -a\n" +
    "\tAdds all the class names in this \n" +
    "\tdirectory to the repository file \n" +
    "\tcalled 'classnames'\n" +
    "ClassScanner classnames\n" +
    "\tChecks all the java files in this \n" +
    "\tdirectory for capitalization errors, \n" +
    "\tusing the repository file 'classnames'";
  private static void usage() {
    System.err.println(usage);
    System.exit(1);
  }
  public static void main(String[] args) 
  throws IOException {
    if(args.length < 1 || args.length > 2)
      usage();
    ClassScanner c = new ClassScanner();
    File old = new File(args[0]);
    if(old.exists()) {
      try {
        // Пробуем открыть существующий
        // файл свойств:
        InputStream oldlist =
          new BufferedInputStream(
            new FileInputStream(old));
        c.classes.load(oldlist);
        oldlist.close();
      } catch(IOException e) {
        System.err.println("Could not open "
          + old + " for reading");
        System.exit(1);
      }
    }
    if(args.length == 1) {
      c.checkClassNames();
      c.checkIdentNames();
    }
    // Записываем имя класса в хранилище:
    if(args.length == 2) {
      if(!args[1].equals("-a"))
        usage();
      try {
        BufferedOutputStream out =
          new BufferedOutputStream(
            new FileOutputStream(args[0]));
        c.classes.store(out,
          "Classes found by ClassScanner.java");
        out.close();
      } catch(IOException e) {
        System.err.println(
          "Could not write " + args[0]);
        System.exit(1);
      }
    }
  }
}

class JavaFilter implements FilenameFilter {
  public boolean accept(File dir, String name) {
    // Strip path information:
    String f = new File(name).getName();
    return f.trim().endsWith(".java");
  }
} ///:~

Класс MultiStringMap является инструментом, позволяющим вам ставить в соответствие группу строк и каждое ключевое включение. Он использует HashMap (в этот раз через наследование). В качестве ключевых значений используются единичные строки, которые ставятся в соответствие значению ArrayList. Метод add( ) просто проверяет, есть ли уже такое ключевое значение в HashMap, а если его нет, помещает его туда. Метод getArrayList( ) производит ArrayList определенных ключей, а printValues( ), который особенно полезен для отладки, печатает все значения ArrayList, получая ArrayList.

Для облегчения жизни все имена классов стандартной библиотеки Java помещаются в объект Properties (из стандартной библиотеки Java). Помните, что объект Properties является типом HashMap, который хранит только объекты String и для ключевого значения, и для хранимого элемента. Однако он может быть сохранен на диске и восстановлен с диска в одном вызове метода, так что он идеален в качестве хранилища имен. На самом деле нам нужен только список имен, но HashMap не может принимать null ни для ключевых значений, ни для хранящихся значений. Так что один и тот же объект будет использоваться и для ключа, и для значения.

Для классов и идентификаторов, которые будут обнаружены в определенном директории, используются две MultiStringMap: classMap и identMap. Также, когда запускается программа, она загружает хранилище стандартных имен классов в объект Properties, называемый classes, а когда обнаруживается новое имя класса в локальном директории, то оно добавляется и в classes, и в classMap. Таким образом, classMap может использоваться для обхода всех классов в локальном директории, а classes может использоваться для проверки, является ли текущий значащий элемент именем класса (что указывается определением объекта или началом метода, так как захватывается следующий значащий элемент — до точки с запятой — и помещается в identMap).

Конструктор по умолчанию для ClassScanner создает список имен, используя JavaFilter, показанный в конце файла, который реализует интерфейс FilenameFilter. Затем вызывается scanListing( ) для каждого имени файла.

Внутри scanListing( ) открывается файл исходного кода и передается в StreamTokenizer. В документации есть функции slashStarComments( ) и slashSlashComments( ), предназначенные для отсеивания коментариев, которым передается true, но это выглядит некорректно, так как это плохо работает. Поэтому эти строки закомментированы, а комментарии извлекаются другим методом. Чтобы извлечь комментарий, “/” должен трактоваться как обычный символ, и нужно не позволять StreamTokenizer собирать его как часть комментария, поэтому метод ordinaryChar( ) говорит StreamTokenizer, чтобы он не делал это. Это также верно в отношении точки (“.”), так как мы хотим иметь метод, который бы извлекал индивидуальные идентификаторы. Однако символ подчеркивания, который трактуется StreamTokenizer как индивидуальный символ, должен оставляться как часть идентификатора, так как он появляется в таких значениях типа static final, как TT_EOF, и т. д., очень популярных в этой программе. Метод wordChars( ) принимает диапазон символов, которые вы хотите добавить к остающимся внутри значащего элемента, анализирующегося одним словом. Наконец, когда анализируете однострочный комментарий или обнаруживаете строку, для которой необходимо определить конец строки, то при вызове eolIsSignificant(true) конец строки будет обнаружен раньше, чем он будет получен StreamTokenizer.

Оставшаяся часть scanListing( ) читает и реагирует на значащие элементы, пока не встретится конец файла, которых будет обнаружен, когда nextToken( ) вернет значение final static StreamTokenizer.TT_EOF.

Если значащим элементом является “/”, он потенциально может быть комментарием, так что вызывается eatComments( ), чтобы разобраться с этим. Но нас будут интересовать другие ситуации, когда мы имеем дело со словом, для которого есть несколько специальных случаев.

Если это слово class или interface, то следующий значащий элемент представляет имя класса или интерфейса, и оно помещается в classes и classMap. Если это слово import или package, то нам не нужна оставшаяся часть строки. Все остальное должно быть идентификатором (которые нас интересуют) или ключевым словом (которые нас не интересуют, но все они написаны в нижнем регистре, так что они не портят рассматриваемые нами вещи). Они добавляются в identMap.

Метод discardLine( ) является простым инструментом, ищущим конец строки. Обратите внимание, что при каждом получении значащего элемента вы должны проверять конец строки.

Метод eatComments( ) вызывается всякий раз, когда обнаружен слеш в главном цикле анализа. Однако это не обязательно означает, что обнаружен комментарий, так что должен быть извлечен следующий значащий элемент, чтобы проверить, не является ли он слешем (в этом случае строка пропускается) или звездочкой. Но если это ни то, ни другое, это означает, что тот значащий элемент, который вы только что извлекли, необходимо вернуть в главный цикл анализа! К счастью, метод pushBack( ) позволяет вам “втолкнуть назад” текущий элемент во входной поток, поэтому, когда главный цикл анализа вызовет nextToken( ), то он получит то, что вы только что втолкнули обратно.

По соглашению, метод classNames( ) производит массив из всех имен, содержащихся в classes. Этот метод не используется в программе, но он очень полезен для отладки.

Следующие два метода относятся к тем, в которых действительно идет проверка. В checkClassNames( ), имя класса извлекается из classMap (который, запомните, содержит только имена их этой директории, организованные по именам файлов, так что имя файла может быть напечатано наряду с беспорядочными именами классов). Это выполняется путем получения каждого ассоциированного ArrayList, и прохода по нему в поисках элементов с меленькой первой буквой. Если такой элемент найден, то печатается соответствующее сообщение об ошибке.

В checkIdentNames( ), используется аналогичный подход; каждое имя идентификатора извлекается из identMap. Если имени нет в списке classes, оно трактуется как идентификатор или ключевое слово. Проверяется особый случай: если длина имени идентификатора больше или равна трем, и все символы являются символами верхнего регистра, этот идентификатор игнорируется, потому что, вероятно, это значение static final, такое как TT_EOF. Конечно, это не идеальный алгоритм, но он означает, что вы будете предупреждены обо всех идентификаторах, записанных в верхнем регистре, и находящихся не на месте.

Вместо сообщения о каждом идентификаторе, который начинается с большой буквы, этот метод хранит историю всего, о чем уже сообщил в ArrayList вызов reportSet( ). Это трактует ArrayList , как “набор”, который говорит вам, встречались ли эти экземпляры в наборе. Экземпляры производятся соединением имени файла и идентификатора. Если элемента нет в наборе, он добавляется, после чего делается сообщение.

Оставшаяся часть текста программы занимается методом main( ), занимается обработкой аргументов командной строки и определяет, хотите ли вы создать хранилище имен из стандартной библиотеки Java, или хотите проверить написанный вами код. В обоих случаях он создает объект ClassScanner.

Независимо от того, строите ли вы хранилище, или используете его, вы должны попробовать открыть существующее хранилище. При создании объекта File и проверки существования, вы можете решить, стоит ли открывать файл и загружать (load( )) в Properties список классов classes внутри ClassScanner. (Классы из хранилища добавляются, а не переписываются, к классам, найденным конструктором ClassScanner.) Если вы передадите один аргумент командной строки, это будет означать, что вы хотите выполнить проверку имен классов и имен идентификаторов, но если вы передадите два аргумента (второй начинается с “-a”), тем самым вы построите хранилище имен классов. В этом случае открывается файл вывода и используется метод Properties.save( ) для записи списка в файл, наряду со строками, которые обеспечивают заголовочную информацию файла.

Резюме

Библиотека потоков ввода/вывода java удовлетворяет основным требованиям: вы можете выполнить чтение и запись с консолью, файлом, блоком памяти или даже через Internet (как вы увидите в Главе 15). С помощью интерфейсов вы можете создать новые типы объектов ввода и вывода. Вы также можете использовать простую расширяемость объектов потоков, имея в виду, что метод toString( ) вызывается автоматически, когда вы передаете объект в метод, который ожидает String (ограничение Java на “автоматическое преобразование типов”).

Есть несколько вопросов, оставшихся без ответа в документации и дизайне библиотеке потоков ввода/вывода. Например, было бы неплохо, если бы вы могли сказать, что хотите появление исключения при попытке перезаписи существующего файла, когда вы открываете его для вывода — некоторые системы программирования позволяют вам открыть файл только для вывода, только если он еще не существует. В Java это означает, что вы способны использовать объект File для определения существования файла, потому что, если вы откроете его, как FileOutputStream или FileWriter, он всегда будет перезаписан.

Библиотека потоков ввода/вывода вызывает смешанные чувства; она делает много работы и она компактна. Но если вы не готовы понимать шаблон декоратора, то дизайн становится интуитивно не понятен, поэтому есть простор для дополнительных исследований и обучения. Это то же не все: нет поддержки определенного рода форматированного вывода, который поддерживают практически все пакеты ввода/вывода других языков.

Однако, как только вы поймете шаблоны декорации и начнете использование библиотеки в тех решениях, которые требуют гибкости, вы сможете использовать выгоды дизайна, с точки зрения которого дополнительные строки кода не будут вас беспокоить столь сильно.

Если вы не нашли того, что искали в этой главе (которая было только введением и не преследовала цель всестороннего рассмотрения), то за более глубоким обзором можете обратиться к книге Java I/O, Elliotte Rusty Harold (O’Reilly, 1999).

Упражнения

Решения для выбранных упражнений могут быть найдены в электронной документации The Thinking in Java Annotated Solution Guide, доступной за малую плату на www.BruceEckel.com.

  1. Откройте текстовый файл так, чтобы вы смогли прочесть его построчно. Читайте каждую строку, как String, и поместите этот объект String в LinkedList. Распечатайте все строки из LinkedList в обратном порядке.
  2. Измените Упражнение 1 так, чтобы имя читаемого фала принималось из командной строки.
  3. Измените Упражнение 2, чтобы была возможность открывать текстовый файл, в который вы могли бы писать. Запишите строки из ArrayList вместе с номерами строк (не пробуйте использовать класс “LineNumber”), в файл.
  4. Измените Упражнение 2, чтобы происходил перевод всех строк из ArrayList в верхний регистр, а результат пошлите в System.out.
  5. Измените Упражнение 2, чтобы оно получало дополнительные аргументы из командной строки: слова, которые необходимо найти в файле. Напечатайте строки, в которых есть эти слова.
  6. Измените DirList.java так, чтобы FilenameFilter на самом деле открывал каждый файл и принимал файлы, основываясь на том, существует ли любой из аргументов командной строки в этом файле.
  7. Создайте класс, называемый SortedDirList с конструктором, который принимает информацию о пути к файлу и строит хранящийся список директории из файлов по этому пути. Создайте два перегруженных метода list( ), которые будут производить либо полный список, или подмножество из списка, основываясь на аргументе. Добавьте метод size( ), который принимает имя файла и возвращает размер этого файла.
  8. Измените WordCount.java так, чтобы она производила алфавитную сортировку, используя инструмент из Главы 9.
  9. Измените WordCount.java так, чтобы она использовала классы, содержащие String и подсчитывающие число хранящихся различных слова, а множество (Set) этих объектов содержало список этих слов.
  10. Измените IOStreamDemo.java так, чтобы она использовала LineNumberInputStream для хранения истории числа строк. Обратите внимание, что гораздо легче хранить историю программно.
  11. Начиная с раздела 4 IOStreamDemo.java, напишите программу, которая сравнивает производительность записи файла при использовании буферизации и без нее при вводе/выводе.
  12. Измените раздел IOStreamDemo.java, чтобы подавить появление пробелов в строках, производимых первым вызовом in5br.readLine( ). Сделайте это, используя цикл while и readChar( ).
  13. Восстановите программу CADState.java, как описано в тексте.
  14. В Blips.java, скопируйте файл и переименуйте его в BlipCheck.java, затем переименуйте класс Blip2 в BlipCheck (сделав его public и удалив публичный код из класса Blips). Удалите маркер //! в файле и выполните программу, включая раздражающие строки. Далее, закомментируйте конструктор по умолчанию для BlipCheck. Запустите программу и объясните почему она работает. Обратите внимание, что после компиляции вы должны выполнить программу с помощью строки “java Blips”, потому что метод main( ) все еще находится в классе Blips.
  15. В Blip3.java, закомментируйте две строки после фразы “Вы обязаны сделать это:” и запустите программу. Объясните результат и почему он отличается от того, когда эти две строки присутствуют в программе.
  16. (Промежуточное) В Главе 8 найдите пример GreenhouseControls.java, который состоит из трех файлов. В GreenhouseControls.java внутренний класс Restart( ) имеет жестко привязанный набор событий. Измените программу так, чтобы она читала события и относительное время из текстового файла. (Рекомендация: Используйте шаблон разработки метода производства для построения событий — Смотрите Thinking in Patterns with Java, доступную на www.BruceEckel.com.)

[57] Design Patterns, Erich Gamma et al., Addison-Wesley 1995.

[58] XML является другим способом решения проблемы перемещения данных между различными платформами, которая не зависит от того, есть ли Java на всех платформах. Однако инструментарий Java поддерживает XML.

[59] Глава 13 покажет даже более последовательное решение этого: GUI программу со скроллируемой текстовой областью.

[ Предыдущая глава ] [ Оглавление ] [ Содержание ] [ Индекс ] [ Следующая глава ]