Автоматическая генерация метода toString для Java-класса

Программирование Учебник по Java Решение задач на языке Java Автоматическая генерация метода toString для Java-класса

Помечено: ,

В этой теме 0 ответов, 1 участник, последнее обновление  Васильев Владимир Сергеевич 1 месяц, 1 неделя назад.

  • Автор
    Сообщения
  • #5570
    @admin

    Автор оригинальной статьи: Сайед Фаред Ахмад.
    Воспользуйтесь мощью Reflection и значительно сократите время кодирования.

    Обзор

    Опытные разработчики Java знают о важности добротно написанных методов toString(). Действительно, намного проще создать и отладить объекты, которые могут просматриваться в хорошо читаемой форме, что особенно актуально при работе с распределенными приложениями. К сожалению, разработка метода toString() для многих классов может забрать много времени, особенно для классов с множественными атрибутами. Поскольку поведение метода toString() довольно постоянно, задача создания метода хорошо автоматизируется. Утилита, приведенная в этой статье помогает Вам сделать это при сокращении времени на разработку.

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

    Автоматизируйте метод toString

    Я сейчас продемонстрирую утилиту, с помощью которой Вы можете делать это. Данный инструмент автоматически генерирует регулярный и устойчивый метод toString() для указанного класса, почти устраняя потраченное на разрабатку время. Это также унифицирует формат метода toString(). Если Вы изменяете этот формат, Вы должны перекомпилировать методы toString(); однако это намного проще, чем изменять вручную сотни или тысячи классов.

    Поддержка сгенерированного кода также проста. Если Вы добавляете большее количество атрибутов в классы, Вам необходимо проделать изменения также в методе toString(). Так как генерация методов toString автоматизирована, Вам необходимо запустить утилиту снова на классе, чтобы проделать Ваши замены. Данный подход более прост и менее подверженный ошибкам по сравнению с изменением кода вручную.

    Код

    Данная статья не предлагает объяснение reflection API; но последующий код предполагает, что Вы имеете, по крайней мере, понимание концепции отражения (reflection). Вы можете заглянуть в раздел Ресурсов, в документацию reflection API. Утилита выглядит следующим образом:

    package fareed.publications.utilities;
    import java.lang.reflect.*;
    
    public class ToStringGenerator {
     public static void main(String[] args) {
      if (args.length == 0) {
       System.out.println("Provide the class name 
    	as the command line argument");
       System.exit(0);
      }
    
      try {
    
      Class targetClass = Class.forName(args[0]);
    
      if (!targetClass.isPrimitive() 
    	&& targetClass != String.class) {
       Field fields[] = targetClass.getDeclaredFields();
    
       Class cSuper = targetClass.getSuperclass();
     // Retrieving the super class
    
        output("StringBuffer buffer
     	 = new StringBuffer(500);");
     // Buffer Construction
    
        if (cSuper != null && cSuper != Object.class) {
        output("buffer.append(super.toString());");
     // Super class's toString()
        }
    
       for (int j = 0; j < fields.length; j++) {
        output("buffer.append(\"" + 
    	fields[j].getName() + " = \");");
     // Append Field name
    
        if (fields[j].getType().isPrimitive() || 
    	fields[j].getType() == String.class) 
     // Check for a primitive or string
         output("buffer.append(this." + 
     	fields[j].getName() + ");");
     // Append the primitive field value
         else
        {
         /* It is NOT a primitive field so this requires
         a check for the NULL value for the aggregated object */
         output("if ( this." + 
    	fields[j].getName() + "!= null )" );
          output("buffer.append(this." + 
     	fields[j].getName() + ".toString());");
         output("else buffer.append(\"value is null\"); ");
        } // end of else
        } // end of for loop
         output("return buffer.toString();");
      }
        } catch (ClassNotFoundException e) {
       System.out.println("Class not found in the class path");
       System.exit(0);
       }
       }
    
      private static void output(String data) {
     System.out.println(data);
     }
    
    }

    Код канала вывода

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

    Ограничения к подходу

    Имеются два важных ограничения к этому подходу. Первый — это то, что он не поддерживает объекты, содержащие циклы(циклические ссылки). Например, если объект A содержит ссылку на объекту B, который в свою очередь содержит ссылку на объект A, то данный метод работать не будет. Однако, такой случай довольно редкий для проектов.

    Второе ограничение — это то, что добавление или вычитание полей(members) требует перекомпиляции метода toString(). Поскольку это должно быть сделано с или без инструмента, это не проблема для этого подхода.
    Выводы

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

    Последующие советы

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

    Усовершенствование #1, предложенное Sangeeta Varma

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

    Усовершенствование #2, предложенное Chris Sanscraint

    Первоначально я предложил утилиту для времени разработки а не для оболочки времени выполнения(run-time environment). Разрешение утилите работать во время выполнения может быть очень полезно, но может забрать еще несколько циклов процессора (CPU). Однако, формирование дампа/ отладка объекта (основное использование метода toString()) обычно делается во время разработки, и не применяется во время эксплуатации программы. В некоторых случаях, это выключение(имеется в виду забить комментариями вызов метода) метода toString() во время эксплуатации программы не может быть применено так, как некоторые проекты могут использовать метод toString() для целей деловой логики. Я предлагаю принимать Вам решение в соответствии со спецификой проекта.

    Перед разработкой этой утилиты, я уже держал в уме эту гибкость относительно выполнения. Сначала, я разработал отдельный делегирующий класс, который использовался любым клиентским классом, чтобы сгенерировать метод toString(). Класс генерировал этот метод, используя вызов метода подобного возвращению из ToStringGenerator.generatetoString() (this), где this указывает на текущий экземпляр клиентского класса, и инструкция кода написана в реализации метода toString(). Но такой подход потерпел неудачу, потому что Reflection API не позволяет получить значения для private переменных класса во время выполнения. Так что класс был только полезен для public переменных класса, которые мне не нужны.

    Но потом Chris Sanscraint указал, что тот же самый код Reflection API получает значения private переменных класса во время выполнения, когда код написан в пределах метода того же самого вызывающего класса. Так что я модифицировал утилиту, которую нужно использовать во время выполнения, и кроме того, метод toString() не нужно модифицировать или редактировать для удаления или добавления любых атрибутов в данном классе.

    Усовершенствование #3, предложенное Eric Ye

    Первоначально я использовал данную утилиту для доступа к переменным класса в сгенерированном коде, но Mr. Ye указал, что код может также использоваться в статическом методе(static method) или даже для вывода статических членов. Так что модифицированный код может теперь обрабатывать, и класс, и экземпляры переменных класса. Ye также указал на ошибку, которая была исправлена в данной версии программы. Эта ошибка заставляла класс генерировать бесполезный код для классов не имеющих атрибутов.

    Модификации Кода

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

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

    Внутренний класс, однако, имеет способность обратиться к private переменным родительского класса так, что reflection код, выполняющийся в пределах его методов, мог также получить доступ к значениям private переменных. Так я решил изменить утилиту на использование внутренного класса, который мог быть вставлен в любой родительский класс клиента. Я также включил файл ToStringGeneratorExample.java, который использует файл ToStringGenerator.java как внутренний класс, чтобы реализовать метод toString().

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

Для ответа в этой теме необходимо авторизоваться.