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


    Совершенство достигается не тогда, когда уже нечего прибавить,
    а когда уже ничего нельзя отнять.
    Антуан де Сент-Экзюпери, Ветер, песок и звезды, 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.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 49
    • +3
      Типичный Enterprise-подход. 2 дополнительных класса чтобы читать/записывать матрицу, которые к тому же не решают проблему многих видов матриц и многих форматов хранения. Как вы собираетесь матрицу в координатном формате записывать? Три вызова writeInt(), а затем маркер элемента чтобы три числа в нужном порядке из буфера сбросилось? То есть, детали формата MatrixMarket плавно перетекли в класс разрежённой матрицы? Или ваш MMOutputStream — это просто такой навороченный (и бесполезный) буфер данных?
      • 0
        Проблема решается. Пройдите по ссылке (http://code.google.com/p/la4j/source/browse/trunk/la4j/src/main/java/la4j/io/). Посмотрите полную реализацию. В статье приведена только часть.

        Новые типы матриц тоже добавляются без проблем. Однако если если говорить о новом формате — то возникают некоторые ограничения. Например, как вы заметили. Для того-же XML будет что то вроде:

        Для плотных:

        rows
        columns


        Для разреженных:

        rows
        columns
        nonzero


        Коненчно, чем то приходится жертвовать в замену универсальности.
        • 0
          Ой, все теги съелись:

          1)
          rows
          columns



          2)
          rows
          columns
          nonzero

          • +2
            К чорту. Ну выменя поняли.

            ЗЫ. тег «code» — ввовсе и не код а пожиратель тегов.
            • +1
              Ибо код — тег <source>.
              • 0
                А что тогда значит тег «code». Я всегда думал что code = source, только без подсветки.
          • 0
            В том-то и дело, что я прочитал. Классы матриц передают в поток числа (индексы и значения) в определённом форматом MatrixMarket порядке, а класс потока — это такой странный буфер. То есть, классы матриц знают про MatrixMarket. Какой тогда смысл в этом всём?
            • 0
              Нет-нет. Еще раз. Классы матриц ничего незнаю про формат. Они вообще ничего, кроме того, что их можно сериализовать незнают.

              Есть формат. Он знает что:

              Есть матрицы. Матрицы нужно писать в файл. Файл делится на заголовок, мету, и данные. Маркеры разделяют эти секции. Может я так непонятно объясняю. Дайте ссылку на код, где «матрица знает о формате»
              • 0
                Матрица знает о формате там, где она передаёт в поток вывода тройки <i, j, a_ij>, но не в виде какой-то явно определённой структуры данных, которая задаёт контракт, а просто тремя не связанными вызовами методов. А с другой стороны эти данные именно в этом порядке кладутся в буфер и затем бефер неизменным записывается в файл.
                • 0
                  Ну это не значит — знает. Эти последовательности можно восспринимать как контракт. Для любого другого формата она будет передовать точно такиеже последовательности. Это не зависит от формата.
                  • 0
                    … просто в потоках вывода для немного других форматов файлов (как я привёл пример ниже — CSC) нужно делать преобразователь с буферизированием всей матрицы.
                    • 0
                      Проблема в том, что вы путаете внутреннее представление внешние. Почитайте стандарт MatrixMarket например. Есть ли там хоть одно упоминание о внутреннем представлении?
                      • –1
                        Я не просто его читал, я его реализовывал.
                        • 0
                          Тем более. Тогда должны прекрасно понимать, что записав CSR или CSC матрицы в MM мы получим один и тот же файл. С точностью до байта.
                          • 0
                            Посмотрите внимательно, я говорил о «файле формата CSC», то есть не MatrixMarket.
                            • 0
                              Не знаю такой формат. Знаю такое внутреннее представление. Все равно что записать в формате «hasttable».
                              • 0
                                Формат файла CSC — это файл, в котором записано три массива: VAL, ICOORD, IPTR. Продолжать или уже понятно?
                                • 0
                                  Ну вы описываете внутреннее представление. Ссылку ну то что есть такой внешний формат можно?
                                  • 0
                                    Harwell Boeing people.sc.fsu.edu/~jburkardt/data/hb/hb.html

                                    То же, что я описываю, только сложнее (просто так не прочитать, там прямо в файл сначала записываются спецификаторы ввода/вывода для Fortran, которыми числа в этом файле нужно читать).
                                    • 0
                                      Контр пример :) Вы выйграли. Формат «фигразбери какой» ориентированный на фортран, я уж точно не смогу реализовать по приведенной выше схеме для Java.

                                      Простите. Но пример явно не показательный.
                                      • 0
                                        Если из формата выбросить всю фортран-специфичность, останется обычный CSC (как в памяти), только на диске в виде трёх массивов.
                                        • 0
                                          Ну да все правильно. Только вот зачем так связывать внутреннее и внешниее представление. Какая цель? Например.

                                          У меня есть библиотека котороая хранит матрицы в CSC, и пишет их также. (примерно как вы сказали). Я хочу эти записанные матрицы открыть в Маткаде или еще где то. А Маткад — понятия не имеет что есть какие-то там CSR/CSC (ирония) и понимает лишь высокоуровнево — плотные/разреженые. Как прикажете быть?
                                          • 0
                                            Цель — исключить необходимость преобразования. Если мы читаем координатный MM, то значения там могут лежать в произвольном порядке. Если нам удобно обрабатывать в памяти CSC, то нам нужно сначала отсортировать считанные тройки по номеру столбца (чтобы числа из одного стрлбца были рядом), а затем ещё сформировать дополнительные массивы. Если бы в файле сразу был CSC, это всё было бы не нужно.
                                            • 0
                                              Ну да. Все верно. Только вот разработчики формата так не думали. Так проблема то не в посте оказыавется а в формате MM получается?
                                              • 0
                                                Разработчики формата, видимо, хотели хоть какой-то не очень сложный в реализации способ хранения матриц чтобы матрицы от одного приложения были пригодны для другого. Я не знаю какие именно задачи они решали и что они думали.

                                                Но формат задокументирован в виде одной статьи, у которой в названии к тому же есть «Initial Design», что возможно указывает на недостаток времени для разработки.

                                                Мне кажется, если бы они собирались сделать именно стандарт (который был бы утверждён какой-либо организацией), то кроме плотного и координатного способа записи в MM были бы и CSR, и CSC, и ленточный, и даже блочный.
                                                • 0
                                                  Вполне возможно. Но пока этого не произошло. Лично я буду разделять высокоуровневые описания: плотная, разреженная, блончая (из мира математики). И низкоуровневые: CSR, CSC, двумерный массив, одномерный массив, хеш-таблица (из мира программирования и реализации).
                                            • 0
                                              По поводу открытия в Маткаде формата, о котором он не знает: очевидно, использовать преобразователь.
                              • 0
                                И насчёт точности до байта вы не правы. Строки данных в координатном формате MM можно переставлять как вздумается, они не обязаны быть сортированы.
                                • 0
                                  Тут согласен.
                                  • +1
                                    28 каментариев на двоих. Не хотите поучавствовать в откртом проекте? :)
                                    • –2
                                      А я участвую, просто не в вашем.
                                      • 0
                                        А ссылку можно? Тоже линейная алгебра?
                                        • 0
                                          Будет что показать — будет пост.
                                          • +3
                                            Ох уж я у вас там оторвусь в кментах. Без обид :)
                • 0
                  И детали формата никак не перетекают в реализацию потока. Т.е мыслить нужно так. Есть матрицы — плотные и разреженные. Это сущности «реального мира». Мы всегда для пишем плотные мартрицы как просто «value» и разреженные как «row col value», как формат это преобразует — это уже его задача.
                  • 0
                    Попробуйте написать для существующих матриц поток вывода для формата CSC (compressed sparse columns) не меняя кода в классах матриц. У вас в классе потока вывода внезапно появится буфер размером со всю матрицу, потому что сейчас матрица передаёт элементы не по столбцам.
                    • 0
                      Неправда. Никакой разницы между CSR и СSC нет с точки зрения формата — это одна и также разреженная матрица. Только для CSC нужно будет реалихзовать свою пару readExternal/writeExternal.
                      • 0
                        Вот то, что я хотел услышать. Захотели формат файла CSC — получили новую пару методовreadExternal/writeExternal в классе матриц.
                        • 0
                          Ну это же очевидно. В класссической реализации моста точно, такойже подход. Воспринимайте новый класс матрицы — как уточнение абстракции в мосте.

                          Тем более вам все равно придется писать эти методы для сериализации Т.е. поддержку формата вы получите бесплатно.
                          • 0
                            Я эти методы в классе матрицы написал один раз и хочу чтобы они работали для всех форматов файлов.
                            • 0
                              Что-то вроде написали в AbststactMatrix и хотите лишь реализовывать новые форматы?
                      • 0
                        К слову сейчас la4j поддерживает CSR.
                        • 0
                          Матрица может быть CSR или в координатном виде, а записать хотим в CSC.
                          • 0
                            СSR/CSC — это внутренние представления. Ничего общего с внешними форматами хранения они не имеют.
                            • 0
                              Вот смотрите. Сейчас поток вывода получает тройки (строка, столбец, значение). Чтобы записать это в файл как CSR или CSC нужно будет буферизировать всю матрицу в потоке вывода. Согласны?
                              • 0
                                Да нет такого понятия файл CSR или CSC :) Для форматов есть только разреженые и плотные матрицы. Они пониятия не имеют как там кто их хранит. Я мог хранить хоть в хеш таблице. В формате MM явно написано что записать должна вестить по строкам: строка, столбец, значение.

                                Если мне надо будет записаывать CSC матрицу. То моя задача так написать матод writeExternal() чтобы он отдавал в поток тройки «строка стобец значени». Для любого другого формата эти тройки будут идентичны в рамках терминологии «разреженная — плотная матрица».
                  • +4
                    [оффтопик]
                    Вот интересный на хабре народ (40 коментов к посту). Вот видит человек (А), что другой (автор топика — Б), занимается пожожей темой что и он, Причем занимается на энтузиазме, бесплатно. И топик писать его никто не заставлял. Так нет чтобы написать что то вроде «Чувак, я тоже самое делаю. Давай вот вместе подумаем, может что-нибудь еще более изящное придумаем, а то у тебя тут вот как то непонятно сделано… » и т.д. А нееет. Он пишет. «Каво. Все фуфло! Я уже стораз такое все писал. Как ты ваще такое фуфло пидумал.»
                    [/оффтопик]

                    При всем уважении к gribozavr. Я не против. Он хотябы заинтересовался темой :)
                    • +1
                      Может я немного и потроллил, но я просто указал на недостатки.
                      • 0
                        Нет проблем я же написал, что не против. Тем более как выяснилось недостатков то и почти нет :)

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