Применение WeakHashmap для списков слушателей

http://java.sun.com/developer/JDCTechTips/2006/tt0211.html#1

В статье от 11мая 1999 года Reference Objects были описаны основные идеи применения ссылочных объектов, но не приводилось детального описания. Данная статья позволит вам получить больше сведений, касающихся данной темы. В основном ссылочные объекты применяются для косвенных ссылок на память необходимую объектам. Ссылочные объекты хранятся в очереди (класс ReferenceQueue), в которой отслеживается доступность ссылочных объектов. Исходя из типа ссылочного объекта, сборщик мусора может освобождать память даже тогда, когда обычные ссылки не могут быть освобождены.

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

Object obj = new Object()

Вы можете представлять прямые ссылки, как сильные ссылки, не требующие создания дополнительного кода для создания или доступа к объектам. Оставшиеся три типа ссылок представляют собой подклассы класса Reference, расположенного в пакете java.lang.ref. Слабые ссылки поддерживаются классом SoftReference, слабые ссылки – классом WeakReference и ссылки фантомы – классом PhantomReference.

Мягкие ссылки действуют как кэш данных. При недостаточном количестве системной памяти сборщик мусора может принять решение об удалении объекта если единственной ссылкой на него является мягкая ссылка. Другими словами, если на объект нет жестких ссылок, то он потенциально является кандидатом для удаления. Сборщик мусора удаляет все мягкие ссылки до того, как он выбросит исключение OutOfMemoryException.

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

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

Для демонстрации использования объекта WeakHashMap, мы используем следующую программу WeakTest, создающую объект WeakHashMap, хранящий один элемент. Затем в программе создается второй поток, ожидающий очищения хеш-таблицы и взывающий сборщик мусора каждые полсекунды. Основной поток ожидает завершения выполнения второго потока.

import java.util.*;

public class WeakTest {
 
private static Map<String, String> map;

 
public static void main(String args[]) {
   
map = new WeakHashMap<String, String>();
    map.put
(new String("Scott"), "McNealey");
    Runnable runner =
new Runnable() {
     
public void run() {
       
while (map.containsKey("Scott")) {
         
try {
           
Thread.sleep(500);
         
} catch (InterruptedException ignored) {
          }
         
System.out.println("Checking for empty");
          System.gc
();
       
}
      }
    }
;

    Thread t =
new Thread(runner);
    t.start
();
    System.out.println
("Main joining");
   
try {
     
t.join();
   
} catch (InterruptedException ignored) {
    }
  }
}

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

После выполнения данной программы вы должны получить следующие результаты:

Main joining
Checking
for empty

В данной программе существует два аспекта, о которых необходимо упомянуть. Первое, обычно не требуется явно вызывать метод System.gc(). Так как программа WeakTest является достаточно простой и не использует большого объема памяти, то требуется явный вызов сборщика мусора. Второе, обратите внимание на следующий вызов new String("Scott"). Вы можете задаться вопросом, зачем вызывать оператор new String(), когда конечным результатом будет та же строка "Scott". Ответ заключается в том, что если вы явно не вызовете оператор new String(), то ссылка ключа хеш-таблицы будет хранится в наборе системных строковых констант. Тем самым данная ссылка не будет удалена и слабая ссылка то же не сможет быть удалена. Для решения данной проблемы, при вызове new String("Scott") создается ссылка на ссылку в наборе системных строковых констант. Содержимое строки при этом не дублируется. Строка по-прежнему хранится в наборе констант. Данный вызов просто создает отдельный указатель на строковую константу в наборе.

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

Для демонстрации, вышеприведенных идей, давайте создадим класс WeakListModel, который реализует интерфейс ListModel. Интерфейс ListModel использует WeakHashMap для сортировки объектов ListDataListener.

Исключая, объявления импорта, определение класса включает имя класса и объявление переменных:

public class WeakListModel implements ListModel, Serializable {

    
private Map<ListDataListener, Object>listenerList =
       Collections.synchronizedMap
(
        
new WeakHashMap<ListDataListener, Object>());
    
private final Object present = new Object();
    
private ArrayList<Object> delegate = new ArrayList<Object>();

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

Так как список слушателей хранится в хеше, то вам необходимо использовать пару ключ-значение. Значением ассоциирующемся с каждым ключом в хеше является объект 'present'. Теоретически для данной реализации вы могли бы использовать объект WeakHashSet. Однако, данный класс не определен в наборе коллекций, поэтому мы используем WeakHashMap.

Последняя переменная delegate управляет содержимым ListModel. В основном реализация интерфейса ListModel просто передает запросы переменной delegate для выполнения действий.

Первый набор методов класса WeakListModel относится к изменению размеров и опросу структуры модели. Во всех данных случаях происходит передача вызова объекту delegate:

public int getSize() {
    
return delegate.size();
  
}

  
public Object getElementAt(int index) {
    
return delegate.get(index);
  
}

  
public void trimToSize() {
    
delegate.trimToSize();
  
}

  
public void ensureCapacity(int minCapacity) {
    
delegate.ensureCapacity(minCapacity);
  
}

  
public int size() {
    
return delegate.size();
  
}

  
public boolean isEmpty() {
    
return delegate.isEmpty();
  
}

  
public Enumeration elements() {
    
return Collections.enumeration(delegate);
  
}

  
public boolean contains(Object elem) {
    
return delegate.contains(elem);
  
}

  
public int indexOf(Object elem) {
    
return delegate.indexOf(elem);
  
}

  
public int lastIndexOf(Object elem) {
    
return delegate.lastIndexOf(elem);
  
}

  
public Object elementAt(int index) {
    
return delegate.get(index);
  
}

  
public Object firstElement() {
    
return delegate.get(0);
  
}

  
public Object lastElement() {
    
return delegate.get(delegate.size()-1);
  
}

  
public String toString() {
    
return delegate.toString();
  
}

Следующий набор методов отвечает за добавление и удаление элементов. В добавление к доступу к объекту delegate для операций сохранения, набор слушателей нуждается в оповещении. Данные методы именуются fireXXX() и их реализация будет приведена ниже. Данные методы выполняют основную работу в классе.

public void setElementAt(Object obj, int index) {
    
delegate.set(index, obj);
     fireContentsChanged
(this, index, index);
  
}

  
public void removeElementAt(int index) {
    
delegate.remove(index);
     fireIntervalRemoved
(this, index, index);
  
}

  
public void insertElementAt(Object obj, int index) {
    
delegate.add(index, obj);
     fireIntervalAdded
(this, index, index);
 
}

  
public void addElement(Object obj) {
    
int index = delegate.size();
     delegate.add
(obj);
     fireIntervalAdded
(this, index, index);
  
}

  
public boolean removeElement(Object obj) {
    
int index = indexOf(obj);
    
boolean rv = delegate.remove(obj);
    
if (index >= 0) {
      
fireIntervalRemoved(this, index, index);
    
}
    
return rv;
  
}

  
public void removeAllElements() {
    
int index1 = delegate.size()-1;
     delegate.clear
();
    
if (index1 >= 0) {
      
fireIntervalRemoved(this, 0, index1);
    
}
   }

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

public synchronized void addListDataListener(
   
ListDataListener l) {
    
listenerList.put(l, present);
  
}

  
public synchronized void removeListDataListener(
   
ListDataListener l) {
    
listenerList.remove(l);
  
}

  
public EventListener[] getListeners(Class listenerType) {
    
Set<ListDataListener> set = listenerList.keySet();
    
return set.toArray(new EventListener[0]);
  
}

Наиболее интересным набором методов является набор методов fireXXX(). Во всех трех данных методах fireContentsChanged(), fireIntervalAdded() и fireIntervalRemoved() создается новый объект Set, который использует ключи из объекта WeakHashMap. Данная процедура применяется для того, чтобы в случае изменения набора во время оповещения оповещался бы оригинальный набор.

protected synchronized void fireContentsChanged(
      
Object source, int index0, int index1) {
    
ListDataEvent e = null;

     Set<ListDataListener> set =
      
new HashSet<ListDataListener> (listenerList.keySet());
     Iterator<ListDataListener> iter = set.iterator
();

    
while (iter.hasNext()) {
      
if (e == null) {
        
e = new ListDataEvent(
         
source, ListDataEvent.CONTENTS_CHANGED,
          index0, index1
);
      
}
      
ListDataListener ldl = iter.next();
       ldl.contentsChanged
(e);
    
}
   }

  
protected synchronized void fireIntervalAdded(
      
Object source, int index0, int index1) {
    
ListDataEvent e = null;

     Set<ListDataListener>set =
      
new HashSet<ListDataListener>(listenerList.keySet());
     Iterator<ListDataListener>iter = set.iterator
();

    
while (iter.hasNext()) {
      
if (e == null) {
        
e = new ListDataEvent(
         
source, ListDataEvent.INTERVAL_ADDED,
          index0, index1
);
      
}
      
ListDataListener ldl = iter.next();
       ldl.intervalAdded
(e);
    
}
   }

  
protected synchronized void fireIntervalRemoved(
      
Object source, int index0, int index1) {
    
ListDataEvent e = null;

     Set<ListDataListener>  set =
      
new HashSet<ListDataListener> (listenerList.keySet());

     Iterator<ListDataListener> iter = set.iterator
();

    
while (iter.hasNext()) {
      
if (e == null) {
        
e = new ListDataEvent(
         
source, ListDataEvent.INTERVAL_REMOVED,
          index0, index1
);
      
}
      
ListDataListener ldl = iter.next();
       ldl.intervalRemoved
(e);
    
}
   }

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

import java.io.Serializable;
  
import java.util.*;
  
import java.lang.ref.*;
  
import javax.swing.*;
  
import javax.swing.event.*;

   ...

  
}

Для тестирования класса ListModel, создадим программу TestListModel, которая определяет объект ListDataListener и связывает его с созданным объектом WeakListModel. Заметьте, что добавление и удаление имен основателей фирмы Sun в качестве элементов модели оповещает слушателя. Как только сильная ссылка на слушателя удалена, объект WeakListModel может удалить слабые ссылки слушателя. Дальнейшие операции с моделью не будут уведомлять слушателя.

import javax.swing.*;
import javax.swing.event.*;

public class TestListModel {
 
public static void main(String args[]) {
   
ListDataListener ldl = new ListDataListener() {
     
public void intervalAdded(ListDataEvent e) {
       
System.out.println("Added: " + e);
     
}

     
public void intervalRemoved(ListDataEvent e) {
       
System.out.println("Removed: " + e);
     
}

     
public void contentsChanged(ListDataEvent e) {
       
System.out.println("Changed: " + e);
     
}
    }
;
    WeakListModel model =
new WeakListModel();
    model.addListDataListener
(ldl);
    model.addElement
("Scott McNealy");
    model.addElement
("Bill Joy");
    model.addElement
("Andy Bechtolsheim");
    model.addElement
("Vinod Khosla");
    model.removeElement
("Scott McNealy");
    ldl =
null;
    System.gc
();
    model.addElement
("Scott McNealy");
    System.out.println
(model);
 
}
}

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

> java TestListModel

   Added: javax.swing.event.ListDataEvent
[type=1,index0=0,index1
   =
0]
  
Added: javax.swing.event.ListDataEvent[type=1,index0=1,index1
   =
1]
  
Added: javax.swing.event.ListDataEvent[type=1,index0=2,index1
   =
2]
  
Added: javax.swing.event.ListDataEvent[type=1,index0=3,index1
   =
3]
  
Removed: javax.swing.event.ListDataEvent[type=2,index0=0,inde
   x1=
0]
   [
Bill Joy, Andy Bechtolsheim, Vinod Khosla, Scott McNealy]

Обращайтесь к документации java.lang.ref package для получения дополнительной информации о ссылках, оповещениях и доступности.

Теги: java WeakHashMap