JavaBranch

пятница, 14 мая 2010 г.

Object всему голова! (серия III)

Основным методом создания объектов в Java безусловно является конструктор, но есть и другие, например десериализация и клонирование. При клонировании, также как и при десериализации, объект создается без использования конструктора. Собственно в этой серии речь пойдет о клонировании объектов и о методе Object.clone.

protected Object clone() throws CloneNotSupportedException

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

Кстати, хочу отметить, что и Cloneable и Serializable, являются реализацией основополагающего паттерна Marker Interface.

Обратимся к документации метода:

/**
 * Создает и возвращает копию данного объекта. Точное значение термина 
 * "копия", может зависеть от класса объекта. Общий смысл заключаются в 
 * том, что для любого объекта x, выражение:
 * 
 *    x.clone() != x
 * 
 * было истинным и выражение:
 * 
 *    x.clone().getClass() == x.getClass()
 * 
 * тоже было истинным, но эти требования не являются безусловными. Как 
 * правило, условие заключается в том, чтобы выражение:
 * 
 *    x.clone().equals(x)
 * 
 * было истинным. Но и это требование не является безусловным.  
 * 
 * В соответствии с соглашением, возвращаемый объект должен быть получен 
 * по средствам вызова super.clone. Если класс и все его суперклассы 
 * (за исключением Object) выполнили это условие, то можно утверждать, 
 * что x.clone().getClass() == x.getClass() будет истинным.
 * 
 * В соответствии с соглашением, объект, возвращенный этим методом, 
 * должен быть независимым от этого объекта (который клонируется).
 * Для достижения этой независимости, может понадобиться изменить одно 
 * или несколько полей объекта, полученного с помощью super.clone, до 
 * его возвращения. Как правило, это применяется когда объект имеет 
 * "вложенную структуру" и агрегирует в себе изменяемые объекты, при 
 * клонировании меняют ссылки на копии объектов. Если класс содержит в 
 * себе только примитивные типы и ссылки на неизменяемые объекты, то
 * обычно никаких изменений в объекте, возвращенным super.clone, не 
 * требуется. 
 * 
 * Метод clone в классе Object выполняет определенную операцию 
 * клонирования. Если класс не реализует интерфейс Cloneable, 
 * то будет брошено исключение CloneNotSupportedException.
 * Отметим, что все массивы реализуют интерфейс Cloneable. Если класс
 * реализует Cloneable, то будет создан новый экземпляр того же класса 
 * и все его поля будут проинициализированы теми же значениями. Таким 
 * образом, этот метод выполняет "точное копирование" объекта, не 
 * "глубокое копирование".
 * 
 * Класс Object не реализует интерфейс Cloneable и поэтому, вызов метода 
 * clone у объекта класса Object приведет к генерации исключения.
 */

Так как интерфейс Cloneable тесно связан с методом clone, хочу сразу привести документацию на данный интерфейс:

/**
 * Класс, реализующий интерфейс Cloneable, сообщает методу Object.clone() 
 * о том, что объекты данного класса могут быть клонированы. 
 * 
 * Вызов метода clone у объекта класса Object приведет к генерации 
 * исключения CloneNotSupportedException.
 * 
 * В соответствии с соглашением, классы реализующие данный интерфейс 
 * должны переопределить метод Object.clone (который является protected)
 * с модификатором public. Подробнее о переопределения Object.clone
 * смотрите в документации к методу.
 * 
 * Обратите внимание, что этот интерфейс не содержит метода clone. Это 
 * означает, что сам факт реализации данного интерфейса еще не говорит о 
 * том, что экземпляры класса могут быть клонированы. 
 */

В документации все довольно просто и понятно изложено, на основе ее содержания можно сделать краткое заключение (оно же, то самое "разрешение", о котором говорилось в начале):

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

При переопределении метода clone, необходимо понимать что происходит в Object.clone. Давайте посмотрим... Если имеется соответствующее "разрешение" на клонирование объекта, то в методе будут выполнены следующие действия:
  1. определяется размер исходного объекта,
  2. выделяется такой же объем памяти,
  3. копируются (побитовое копирование) данные исходного объекта в выделенную область.
Только метод clone способен определить необходимый объем памяти и выполнить побитовое копирование исходного объекта. Таким образом, при переопределении метода clone, первым делом необходимо вызвать реализацию суперкласса. Если все суперклассы придерживаются этого соглашения, то это неизбежно приведет к вызову метода базового класса Object. Таким образом, будет получен объект точно такого же типа.

Приведу пример реализации клонирования:

class Animal implements Cloneable {

  @Override
  public Object clone() throws CloneNotSupportedException {
    return super.clone(); 
  }

}

Стоит обратить внимание на то, что исключение CloneNotSupportedException является обрабатываемым исключением, а это значит его обязательно придется обработать. Но в своей практике я встречал вот такую реализацию метода clone:

  @Override
  public Object clone() {
    try {
      return super.clone();
    } catch (CloneNotSupportedException e) {
      assert false : "Этого не может быть!";
      return null;
    }
  }

Данная реализация избавляет от необходимости следить за исключениями CloneNotSupportedException. Создатель класса подразумевает что исключительная ситуация никогда не наступит. Однако, у такой реализации есть один недостаток: если я решу создать наследника от класса с подобной реализацией метода clone и захочу запретить клонирование экземпляров моего класса, то мне не удастся этого сделать. Я не смогу выкинуть исключение CloneNotSupportedException в теле переопределенного мной метода, т.е. донести до пользователя класса, что клонирование не разрешено. Но есть способ обойти это ограничение, например, я могу выкинуть не обрабатываемое исключение UnsupportedOperation­Exception, которое специально предназначено для того, что бы запретить использовать унаследованный метод.

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


Объект машина2 является точной копией объекта машина1. Но такое копирование может не удовлетворять необходимым условиям бизнес-логики. Из представленной схемы видно, что после клонирования обе машины имеют один и тот же пробег, одну и туже марку, одного и того же владельца, один и тот же мотор. Очевидно, что последний факт не имеет ничего общего со здравым смыслом - две машины не могут иметь один и тот же мотор. В этом случае логику копирования придется "допиливать" вручную.

class Car implements Cloneable {
 
  private int mileage;
 
  private String bradn;
 
  private Owner owner;
 
  private Engine engine;
 
  @Override
  public Object clone() throws CloneNotSupportedException{
    Car copy = (Car) super.clone();
    copy.engine = (Engine) this.engine.clone();
    return copy;
  }
 
}

class Owner {
 
}

class Engine implements Cloneable {
 
  @Override
  public Object clone() throws CloneNotSupportedException{
    return super.clone();
  }
 
}

Такая реализация Car.clone уже поддается здравой логике - у каждой машины свой мотор :).


В данном примере все просто и понятно, но на практике, реализация глубокого копирования оказывается не такой уж и простой задачей. Большая часть типов в Java Core не разрешают клонирование, а это значит, что при реализации клонирования Вы можете наткнуться на то, что агрегируемый объект не может быть клонирован. Бывают такие ситуации, когда приходится возиться с полями примитивного типа. Например, если класс содержит уникальный идентификатор, то клону придется присвоить новый, отличный от своей копии, идентификатор. Также можно наткнуться на поле с модификатором final, изменить его значения после клонирования объекта будет невозможно. Подводных камней, при реализации глубокого копирования объекта, очень много - так же как и подходов для их обхода. Но я не буду заострять на них внимание. Мне было важно донести до Вас, что при реализации копирования, разработчик должен сам позаботится о логике копирования и предусмотреть все варианты использования его класса. Но в любом случае, хорошо если Вы задокументируете логику копирования. Это поможет другим разработчикам лучше понять, как именно нужно обращаться с экземплярами Вашего класса.

Подведем итог. Клонирование - очень трудно контролируемый механизм и прежде чем его задействовать, нужно хорошо продумать реализацию. Возможно, можно обойтись и без него, например, задействовав конструктор копий (Car(Car c){...}). Но если Вы все же решили его использовать, будьте очень осторожны!

И в завершении, хочу добавить: клонировать объект можно с помощью сериализации, данный подход имеет свои недостатки, но тем не менее, имеет право на существование. Библиотека Apache Commons Lang содержит реализацию этого подхода: org.apache.commons.lang.SerializationUtils.clone(Serializable object)

На этом все. Продолжение следует, в следующей серии об equals и hashCode...

воскресенье, 2 мая 2010 г.

Eclipse Logging Framework

Если Вы пишите плагин для Eclipse RCP и Вам понадобился логгер, то лучше всего для этих целей использовать возможности самой платформы. Класс Plugin, по средствам метода getLog(), предоставляет необходимый API для логирования (org.eclipse.core.runtime.ILog).

   Plugin plugin = MyPlugin.getDefault();
   ILog logger = plugin.getLog();
   IStatus event = new Status(IStatus.INFO, 
         plugin.getBundle().getSymbolicName(), 
         "Какое нибудь сообщение");
   logger.log(event);
Это сообщение попадет в "${workspace}/.metadata/.log".

Также можно отслеживать и перенаправлять все сообщения. В данном примере все сообщения перенаправляются в log4j.

public class MyPlugin extends AbstractUIPlugin {

   public static final String PLUGIN_ID = "myplugin";

   private static MyPlugin plugin;

   private ILogListener logListener;

   public MyPlugin() {
   }

   public void start(BundleContext context) throws Exception {
      super.start(context);
      plugin = this;

      logListener = new ILogListener() {

         private Logger logger = Logger.getLogger(PLUGIN_ID);

            @Override
            public void logging(IStatus status, String pluginName) {
               if (status == null) { 
                          return;
                   }

                      int severity = status.getSeverity();
                      Level level = Level.DEBUG;  
                      if (severity == Status.ERROR) {
                          level = Level.ERROR;
                      } else if (severity == Status.WARNING) {
                          level = Level.WARN;
                      } else if (severity == Status.INFO) {
                          level = Level.INFO;
                      } else if (severity == Status.CANCEL) {
                             level = Level.FATAL;
                      }

                      pluginName = formatText(pluginName);
                      String statusPlugin = formatText(status.getPlugin());
                      String statusMessage = formatText(status.getMessage());
                      StringBuffer message = new StringBuffer();
                      if (pluginName != null) {
                  message.append(pluginName);
                         message.append(" - ");
                      }
                      if (statusPlugin != null 
                     && (pluginName == null 
                           || !statusPlugin.equals(pluginName))) {

                         message.append(statusPlugin);
                         message.append(" - ");
                      } 
                      message.append(status.getCode());
                      if (statusMessage != null) {
                        message.append(" - ");
                         message.append(statusMessage);
                      }

                      logger.log(level, message.toString(), status.getException());
            }

               private String formatText(String text) {
                      if (text != null) {
                         text = text.trim();
                         if (text.length() == 0) {
                            return null;
                         }
               } 

                      return text;
            }

      };
 
      plugin.getLog().addLogListener(logListener);
   }
 
   public void stop(BundleContext context) throws Exception {
      plugin = null;
        plugin.getLog().removeLogListener(logListener);

        super.stop(context);
    }

    ...

}

Всего написанного должно хватить, что бы начать использовать штатный логгер Eclipse RCP.

суббота, 1 мая 2010 г.

Object всему голова! (серия II)

Эта серия посвящена еще одному методу класса Object. Сразу хочу сказать, что в большинстве случаев от использования этого метода нужно отказываться. А вот почему это нужно делать и зачем он тогда вообще нужен? - об этом и пойдет речь далее...

protected void finalize() throws Throwable;

Для начала взглянем на документацию к методу:

/**
 * Вызывается сборщиком мусора (GC) для объекта в тот момент, когда GC
 * определил, что на объект больше нет ссылок. Переопределение метода 
 * finalize в подклассах предназначено для освобождения системных ресурсов 
 * и выполнения других необходимых очисток.
 * 
 * Основной целью метода finalize является его вызов в момент когда Java
 * виртуальной машиной установлено, что объект более недостижим из живых
 * потоков, за исключением тех случаев, когда метод finalize вызывается
 * повторно. В методе finalize могут выполняться любые действия, в том 
 * числе и те, в следствии которых объект снова станет доступным. Но как
 * правило, до того как объект будет безвозвратно удален, выполняют 
 * необходимую очистку. Например, метод finalize объекта, который 
 * представляет из себя I/O соединение, может содержать вызов закрытия 
 * этого соединения до того как объект будет удален.
 * 
 * Реализация метод finalize в классе Object не содержит никаких действий, 
 * она пустая. Но в подклассах можно переопределить эту реализацию.
 * 
 * Спецификацией Java не гарантируется, что метод finalize будет когда 
 * либо вызван для кого либо объекта. Но гарантируется, что поток, 
 * вызывающий метод finalize, в момент вызова не будет блокировать какие 
 * либо данные объекта. Если в момент выполнения метода будет брошено 
 * исключение, то оно будет проигнорировано и метод завершится.
 * 
 * После того как для объекта был выполнен метод finalize, никаких 
 * действий не будет выполнено, пока JVM снова не проверит объект на
 * недоступность из живых потоков и только потом, по результатам проверки, 
 * объект может быть удален из памяти.
 * 
 * Метод finalize не может быть вызван JVM более одного раза для каждого 
 * объекта.
 * 
 * Любое исключение, выброшенное в методе finalize, завершит выполнение 
 * очистки, но будет проигнорировано.
 */
Почему же стоит избегать использования метода finalize?.. Да потому что об этом даже в javadoc написано :-). Метод finalize очень ненадежный и полагаться на него нельзя. Тогда для чего же он нужен?... Пожалуй, с этого и стоит начать.

Применить finalize можно для освобождения ресурсов, которые не связаны с памятью JVM. К таким ресурсам можно отнести ресурсы операционной системы, ресурсы базы данных и т.д. Если какой либо объект занимает внешние ресурсы, то конечно их необходимо освободить,  после того как объект более недостижим. А поскольку, внешние ресурсы не связаны с памятью JVM, сборщик мусора не сможет до них дотянуться и освободить. В этих случаях, для освобождения внешних ресурсов, можно применить finalize. Но если освобождение этих ресурсов критично, то использовать finalize нельзя, так как он может вообще никогда не вызваться или вызваться когда будет уже поздно.

Еще один способ применения finalize - это своего рода защита от дурака. В этом случае, он может использоваться для подстраховки пользователя класса. Например, есть некий класс, который содержит в себе следующие методы:
  • метод open для получения каких нибудь внешних ресурсов;
  • метод close для явного освобождения полученных ресурсов. 
Для страховки в методе finalize можно проверять были ли освобождены занимаемые ресурсы. Если окажется, что пользователь класса забыл их освободить, то можно сделать это за него. Конечно, при таком подходе нельзя со стопроцентной уверенностью сказать, что класс будет защищен от всех ошибок пользователя, но, по крайней мере, он будет более живучим.

При переопределении метода finalize, необходимо вызвать реализацию суперкласса. Если этого не сделать, то finalize суперкласса никогда не будет вызван.

protected void finalize() throws Throwable {
   try { 
      // Освобождаем необходимые ресурсы
   } finally { 
      super.finalize(); 
   }
}
Обратите внимание на реализацию метода finalize, на блок try/finally. Даже если освобождение ресурсов в блоке try прервется исключением, то независимо от этого будет вызван метод finalize наследуемого класса.

Далее я приведу еще один пример, отражающий защищенный вариант финализации.

public class Worker {

   private final Object fg = new Object() {

      @Override
      protected void finalize() throws Throwable {
         // Очищаем необходимые ресурсы, занятые объектом класса Worker
      }

   };

}
Такая реализация позволяет защититься от забывчивых программистов. Если программист переопределит в наследнике от класса Worker метод finalize и забудет вызвать реализацию суперкласса, то ничего страшного не произойдет. Финализация будет выполнена за счет внутреннего объекта. Джошуа Блох в своей книге Effective Java, назвал такой подход Finalizer Guardian.

Стоит предостеречь программистов, пишущих на C++, от использования finalize в качестве деструктора. В C++ весь код, связанный с освобождением памяти объекта, помещается в диструктор, который вызывается автоматически, когда объект выходит из области определения или когда удаляется объект, созданный динамически. А в Java нет такого понятия как деструктор, освобождением памяти связанной с объектом, занимается сборщик мусора.

Теперь пришло время затронуть тему сборки мусора, она тесно связана с методом finalize. Как правило, сборщик мусора активизируется при низком уровне свободной памяти, в этот момент он пытается отчистить память от неиспользуемых объектов. Но запуск GC еще не гарантирует то, что после очистки будет достаточно памяти. Если памяти недостаточно даже после очистки, JVM генерирует исключение OutOfMemoryError. Есть возможность явно попросить GC начать свою работу (Runtime.getRuntime().gc()), но это не гарантирует что GC начнет работу немедленно.

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

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

public class Test {

   public static void main(String [] args) {
      Runtime.getRuntime().runFinalizersOnExit(true);
      for (int i = 0; i < 10; i++) {
          new A();
       }
       System.exit(0);
    }



class A {

      @Override
    protected void finalize() throws Throwable { 
      System.out.println("onFinalize");
    }

   } 
На на консоль будет выведено 10 раз onFinalize, причем даже вызов exit не воспрепятствует этому. Но метод runFinalizersOnExit с версии 1.3 помечен как deprecated. Этот метод по сути небезопасен, в следствии его использования можно получить дедлок.

Необходимо отметить, что Финализацией занимается отдельный поток, имя которого Finalizer. Он последовательно вызывает метод finalize у каждого, стоящего в очереди на финализацию, объекта. А это значит, что если выполнение одного метода finalize застопорилось (или зациклилось), то до остальных дело вообще может не дойти. Причем объекты, готовые к финализации, весят в памяти, сжирая ресурсы. Рассмотрим следующий пример:

public class Test {

    public static void main(String[] args) {
        Random rnd = new Random(1000);
        for (int i = 0; i < 100000; i++) {
            new A(Integer.toHexString(rnd.nextInt()));
        }
        while (true) {
        }
    }

}

class A {

    private String name;

    public A(String str) {
        this.name = str;
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println(
                Thread.currentThread().getName() + " - " + name);
        while (true) {  
        }
    }

}
Если запустить на выполнение это код, то получим примерно следующее:

    console > Finalizer - 396dde12
причем программа будет выполняться вечно и на консоль больше ничего не выведется.

Разберем пример... В методе main создаются 100 000 объектов, в какой то момент Finalizer берется за работу, но зацикливается на первом вызове finalize. Программа остается висеть и ни один finalize больше не выполнится, а все 100 000 объектов останутся в памяти. Если убрать зацикливание в методе finalize, то финализация будет выполнена для множества объектов, об этом будет свидетельствовать результат вывода на консоль.

Если цикл увеличить до 1 000 000 итераций, то программа в скором времени вообще умрет. При этом на консоль не выведется никакого стектрейса, все будет тихо и без какого либо шума. Единственное что может нам помочь разобраться, это код завершения JVM. В данном случае он будет равен 1, что свидетельствует об аварийном завершении программы.

Результат выполнения тестового приложения

Далее приведу еще парочку интересных скриншотов, на которых отражается "предсмертное" состояние JVM.

Дебаг объектов JVM

Куча JVM

Есть еще один момент, который стоит затронуть. В классе RunTime есть метод runFinalization, как Вы дальше увидите, это очень интересный метод. В каждом экземпляре запущенной  JVM присутствует один поток Finalizer, который занимается финализацией объектов. Но на самом деле, таких потоков может быть несколько. Вызывая метод runFinalization, Вы инициируете создание еще одного потока Finalizer-а. Потоки, запущенные с помощью метода runFinalization, будут иметь название Secondary finalizer и будут выполняться пока в очереди на финализацию будут находится объекты. Важно отметить, что runFinalization вернет управление только тогда, когда запущенный им Finalizer завершит свою работу, т.е. пока очередь на финализацию не опустеет. Следующий пример кода продемонстрирует все описанное выше.

public class Test {

   public static void main(String [] args) {
      new Thread(new Runnable() {

         @Override
         public void run() {
            for (int i = 0; i < 100000; i++) { 
               new A();
             }
          }

      }).start();
       new Thread(new Runnable() {
         @Override
         public void run() {
            for (int i = 0; i < 10; i++) { 
               new Thread(new Runnable() { 
              
                  @Override                   
                  public void run() {                      
                     System.out.println("before runFinalization"); 
                     System.runFinalization(); 
                     System.out.println("after runFinalization");
                  }
               }).start();                
               try {                   
                  Thread.sleep(1000);                
               } catch (InterruptedException e) {                   
                  e.printStackTrace();                
               }
            }
         }
   
      }).start();       
      while (true) {          
         Thread.yield();       
      }
   }
class A {
   @Override
   protected void finalize() throws Throwable { 
      System.out.println(Thread.currentThread().getName());
      while (true) {
         Thread.yield();
      }
   }
}
Если выполнить приведенный код, то по выводу на консоль можно будет увидеть, что сначала запустится штатный финализатор (Finalizer), а потом будет запущено еще 10 финализаторов (Secondary finalizer). При этом, на консоли мы некогда не увидим "after runFinalization". Это связано с тем, что метод runFinalization ждет пока созданный им поток завершит свою работу, что в данном примере никогда не произойдет.


Поток Finalizer всегда присутствует в запущенной JVM. Он находится в состоянии wait до того момента, пока не получит от GC команду "фас". В отличии от штатного Finalizer-а, который является потоком-демоном, Finalizer-ы созданные с помощью метода runFinalization являются обычными потоками.

Думаю, что пришло время подвести итоги. Применять метод finalize не безопасно, старайтесь не делать этого, кроме как в качестве подстраховки или для освобождения некритических внешних ресурсов! В тех случаях когда Вы решаете переопределить метод finalize, лучше применить подход Finalizer Guardian. Ну вот и все.

Продолжение следует, в следующей серии о clone...