Pull to refresh

Идея реализации пакета I/O в Java

Reading time 6 min
Views 6.3K

Совершенство достигается не тогда, когда уже нечего прибавить,
а когда уже ничего нельзя отнять.
Антуан де Сент-Экзюпери, Ветер, песок и звезды, 1939

Часто приходится проектировать и разрабатывать пакеты ввода/вывода для приложений на Java. С одной стороны есть java.io, которого бывает более чем достаточно. Однако, на практике редко удается обойтись набором стандартных классов и интерфейсов.

В статье, приводится практический пример идеи для реализации пакетов ввода/вывода на платформе Java.


Постановка задачи


Для наглядности рассмотрим пример. Пусть требуется разработать пакет ввода/вывода для матричной библиотеки. При этом, необходимо учитывать, что:
  • Матриц (типов/классов) может быть бесчисленное множество, например — плотные, разреженные;
  • Входных и выходных форматов может также быть много, например — MatrixMarket, XML;

Формат MatrixMarket


Для формата MatrixMarket будут следующие представления матрицы:
0  2
3  0

Для плотной матрицы:

%%MatrixMarket matrix array real general
2 2
0
2
3
0

Для разреженной матрицы:

%%MatrixMarket matrix coordinate real general
2 2 2
0 1 2
1 0 3

Реализация


Таким образом, реализация пакета I/O должна быть настолько гибкой, чтобы при расширении системы (добавления нового типа матрицы, например блочной или добавления нового формата, например CSV), не требовалось полностью переписывать пакет — а было достаточным лишь реализовать дополнительный класс — класс новой матрицы или нового формата.

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

Возвращаясь к основам паттернов проектирования, можно кратко описать шаблон мост, как разделение абстракции и реализации. В нашем случае — отделение типа матрицы (плотная, блочная) от формата (XML, MatrixMarket). Достигается это за счет введения двух интерфейсов — интерфейса абстракции и интерфейса реализации. Интерфейс абстракции должен высокоуровнево описывать поведение пакета, например методы — readMatrix(), writeMatrix(). В то время, как интерфейс реализации должен описывать низкоуровневые моменты, такие как — readMatrixElement(), writeMatrixElement() и т.д. Тогда в самом простом случае, диаграмма классов для пакета ввода/вывода выглядит следующим образом.



Высоуровневый метод writeMatrix() представляет собой последовательность низкоуровневых вызовов:
  • writeMatrixHeader() — запись информации о типе матрицы;
  • writeMatrixMeta() — запись информации о размерности матрицы;
  • writeMatrixElement() — запись информации об элементе матрицы.

Получается что, шаблон мост решает описанную ранее проблему, благодаря разделению реализации и абстракции. Но в большинстве случаев, объекты, с которыми работают пакеты ввода/вывода уже реализуют механизмы сериализации (Srializable, Externalizable). В нашем случае, интерфейс Matriх уже расширяет интерфейс Externalizable. Почему именно Externalizable а не Serizliable можно прочитать в этом или этом (работа автора) исследованиях. В кратце — Externalizble работает в разы быстрее за счет сокращенного количества вызовов в JVM/Reflection.

И так методы readExternal/writeExternal для плотной матрицы выглядят так:

public void writeExternal(ObjectOutput out) throws IOException {
	
  out.writeInt(rows);
  out.writeInt(columns);

  for (int i = 0; i < rows; i++) {
    for (int j = 0; j < columns; j++) {
      out.writeDouble(self[i][j]);
    }
  }
}

public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
	
  rows = in.readInt();
  columns = in.readInt();
	
  self = new double[rows][columns];
  for (int i = 0; i < rows; i++) {
    for (int j = 0; j < columns; j++) {
      self[i][j] = in.readDouble();
    }
  }
}

Внимательный читатель скажет: “Это же в точности напоминает шаблон Мост!” и будет совершенно прав. Интерфейсы ObjectOutput и ObjectInput реализуют идею шаблона в качестве интерфейсов реализации. Тогда возникает вопрос — “Зачем плодить еще классы вида MatrixReader/MarixWriter и писать в них дубликаты методов readExterna()l/writeExternal()?”. Правильно — незачем. Тем более методология DRY (Don’t Repeat Yourself — Не повторяй себя) нам об этом напоминает.

В таком случае, попытаемся пересмотреть предложенную реализацию пакета, с учетом того, что java.io уже содержит интерфейсы реализации — ObjectInput/ObjectOutput. Т.е. нам требуется лишь реализовать классы форматов — MMOutoutStream/MMInputStream (MM = MatrixMarket), чтобы использовать их вместо стандартных классов для сериализации — ObjectInputStream/ObjectOutputStream. Тогда использование будет очень прозрачным:

// запись
ObjectOutput mmos = new MMOutputStream(“file.mm”);
mmos.writeObject(a);
mmos.close();

// чтение	
ObjectInput mmis = new MMInputStream(“file.mm”);
Matrix b = (Matrix) mmis.readObject();

Приведенный выше код, легко трансформируется в код сериализации. Для этого, достаточно лишь заменить классы MM* на Object*. (MMOutputStream -> ObjectOutputStream).

Осталась одна нерешенная проблема. Проблема разделения логических блоков файла. В нашем случае файл делится на:
  • Header — заголовок, содержащий тип матрицы;
  • Meta — мета информацию, содержащую размерность матрицы;
  • Data — данные.

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

Для решения этой проблемы автор предлагает использовать специальные маркеры (байты), обозначающие границы каждого из блоков — заголовка (HEADER_MARKER), Мета-информации (META_MARKER) и элемента (ELEMENT_MARKER).

Тогда методы writeExternal()/readExternal() будут выглядеть следующим образом:
@Override
public void writeExternal(ObjectOutput out) throws IOException {

  out.writeInt(rows);
  out.writeInt(columns);
  out.writeByte(META_MARKER); // записываем маркер META

  for (int i = 0; i < rows; i++) {
    for (int j = 0; j < columns; j++) {
      out.writeDouble(self[i][j]);
      out.writeByte(ELEMENT_MARKER); // записываем маркер ELEMENT
    }
  }
}

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
	
  rows = in.readInt();
  columns = in.readInt();
  in.readByte(); // пропускаем меркер META

  self = new double[rows][columns];
    for (int i = 0; i < rows; i++) {
      for (int j = 0; j < columns; j++) {
        self[i][j] = in.readDouble();
        in.readByte(); // пропускаем меркер ELEMENT
      }
    }
  }

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

С точки зрения потока, в нашем случае MMInputStream/MMOutputStream, запись будет выглядеть следующим образом:
  • Записывать в буфер, любые данные, полученные через методы writeDouble()/writeInt();
  • При получении одного из маркеров — выполнить ассоциированную с ним операции на основании буфера значений.

Ниже приведена основная часть реализации класса MMOutputSteam:
public class MMOutputStream extends OutputStream implements ObjectOutput {

  @Override
  public void writeByte(int v) throws IOException {
    switch (v) {
    case HEADER_MARKER:
      writeHeader();
      break;
    case META_MARKER:
      writeMeta();
      break;
    case ELEMENT_MARKER:
      writeElement();
      break;
    }
  }
	
  @Override
  public void writeInt(int v) throws IOException {
    put(String.valueOf(v));		
  }

  @Override
  public void writeDouble(double v) throws IOException {
    put(String.format(Locale.US, "%.12f", v));
  }

 @Override
 public void writeObject(Object obj) throws IOException {
    if (matrix instanceof SparseMatrix) {
     put(SPARSE_HEADER);
    } else if (matrix instanceof DenseMatrix) {
     put(DENSE_HEADER);
    } 
		
    writeHeader();
    matrix.writeExternal(this);
    flush();
  }

 private void writeHeader() throws IOException {
    out.write("%%MatrixMarket ");
    out.write(buffer[0] + " ");
    out.write(buffer[1] + " ");
    out.write("real general");
    out.newLine();
 }
	
  private void writeMeta() throws IOException {
    dumpBuffer();
    out.newLine();
  }
	
  private void writeElement() throws IOException {
    dumpBuffer();
    out.newLine();
   }
	
  private void put(String value) {
    buffer[length++] = value;
  }
	
  private void dumpBuffer() throws IOException {
    for (int i = 0; i < length; i++) {
      out.write(buffer[i] + " ");
    }
  }
}


Резюме


Предложенные вариант реализации пакета ввода/вывода в Java является достаточно удачным применением шблона Мост к существущим иерархиям в Java API. Автор надеется, что описанная в статье идея станет еще одним удобным инструментом в распоряжении читателей и натолкнет их на дополнительные рассуждения по этому теме.

*


Описанный в статье пример, является частью открытой библиотеки для решения задач линейной алгебры — la4j. Рассмотренную реализацию идеи можно посмотреть в пакете la4j.io. В текущей версии поддерживается только формат MatrixMarket.

PS автор топика = автор la4j.
Tags:
Hubs:
+9
Comments 49
Comments Comments 49

Articles