JavaFX Observable Collections

By | 2017年4月8日

1. 什么是Observable Collections

Observable collections是JavaFX对Java集合的扩展,除了实现Java Collection接口(List, Set和Map),还实现了Observable接口。

  • ObservableList
  • ObservableSet
  • ObservableMap

除了可以当普通的List, Set和Map使用,它们还具有Observable的两大特性:

  • 监听invalidation事件
  • 监听元素更改事件

JavaFX没有暴露Ovservable List, Set和Map的实现方法,需要调用FXCollections的静态方法来创建ObservableList, ObservableSet和ObservableMap。

2. ObservableList

2.1 创建ObservableList

调用FXCollections的静态方法。

// Create un empty, unmodifiable ObservableList
<E> ObservableList<E> emptyObservableList()

// Create an empty ObservableList
<E> ObservableList<E> observableArrayList()

// Create with given items
<E> ObservableList<E> observableArrayList(Collection<? extends E> col)
<E> ObservableList<E> observableArrayList(E... items)
<E> ObservableList<E> observableList(List<E> list)

// Create with an extractor
<E> ObservableList<E> observableArrayList(Callback<E, Observable[] extractor)
<E> ObservableList<E> observableList(List<E> list, Callback<E, bservable[]> extractor)

2.2 监听ObservableList的Invalidation事件

对list的所有改动(add, remove, set等),都会触发invalidation listener。

ObservableList<String> list =
    FXCollections.observableArrayList("one", "two");
list.addListener(new InvalidationListener() {
  @Override
  public void invalidated(Observable observable) {
    System.out.println("List is invalid.");
  }
});

2.3 监听ObservableList的Change事件

下面的例子中,ObservableList存储的是Student对象,name和gender都是StringProperty,是为了监听ObservableList的Update事件。

public class Student {
  private long id;
  private StringProperty nameProp;
  private StringProperty genderProp;

}

FXCollections.observableArrayList(Callback),构造时传递一个Callback,告诉ObservableList监听elment的哪些属性变化。如下面,是监听Student的姓名和性别,如果Student的姓名或性别变了,触发ListChangeListener。

ObservableList<Student> students = FXCollections.observableArrayList(
    new Callback<Student, Observable[]>() {
      @Override
      public Observable[] call(Student student) {
        return new Observable[] {student.getNameProp(), student.getGenderProp()};
      }
});

添加ListChangeListener。

students.addListener(new ListChangeListener<Student>() {
  @Override
  public void onChanged(ListChangeListener.Change<? extends Student> change) {

    while (change.next()) {
      if (change.wasPermutated()) {
        handlePermutated(change);
      } else if (change.wasUpdated()) {
        handleUpdated(change);
      } else if (change.wasReplaced()) {
        handleReplaced(change);
      } else if (change.wasRemoved()) {
        handleRemoved(change);
      } else if (change.wasAdded()) {
        handleAdded(change);
      }
    }
  }
}

通常我们更关心List改变的具体内容,而不是仅仅知道List改变了。Change对象封装了List改变的具体内容,类似SQL DataSet,通过游标change.next()来遍历改变的detail。多数情况下,change只有一个改动,但在removeAll(…)时,如果删除的元素不连续,则每一段被删除的元素是一个改动,所有被删除的片段,组成一个Change对象,通过游标,获得所有的改动内容。经过我测试,其它操作(add, addAlll, set, sort, remove)时,Change对象只有一个改动,change游标只执行一次。

  • Change::wasPermutated(),sort时触发,元素没改变。
  • Change::wasUpdated(),更改元素name或性别是,触发。
  • Change::wasReplaced(),set(index, element)调用时触发。replace事件是remove事件和add事件的合体,先remove当前元素,再添加新的元素。
  • Change::wasRemoved(),remove, removeAll, set时触发。
  • Change::wasAdded(),add, addAll, set时触发。

2.3.1 Permutate事件detail

当调用List的sort方法时,不管List已排好序还是乱的,都会触发Permutate事件,getFrom()返回0,getTo()返回List长度,也就是说整个List变动了(对排好序的来说,其实没变)。

students.sort(new Comparator<Student>() {
  @Override
  public int compare(Student s1, Student s2) {
    return (int) (s1.getId() - s2.getId());
  }
});

private void handlePermutated(Change<? extends Student> change) {
  int fromIndex = change.getFrom();
  int toIndex = change.getTo();
}

2.3.2 Update事件detail

当有个Student的name或gender改变时,找到是哪个学生的信息改变了。getFrom()和getTo()返回值一样,指向被更改的元素。记住,这里是更改元素的属性。

student1.setName("New Name");

private void handleUpdated(ListChangeListener.Change<? extends Student> change) {
  Student student = change.getList().get(change.getFrom());
}

2.3.3 Replace事件detail

当有个学生被替换时,我们想知道哪个学生被删除了,哪个学生被新加进来了。

Student newStudent = new Student(100, "Trump", "male");
students.set(2, newStudent);

private void handleReplaced(ListChangeListener.Change<? extends Student> change) {
  // There's one been removed and one been added.
  Student removedStudent = change.getRemoved().get(0);
  Student addedStudent = change.getAddedSubList().get(0);
}

2.3.4 Remov事件detail

当有学生被删除了,我们想知道哪些学生被删除了。

students.remove(student1);
students.removeAll(student3, student4);

private void handleRemoved(Change<? extends Student> change) {
  int removedSize = change.getRemovedSize();
  List<? extends Student> removedStudents = change.getRemoved();
}

2.3.5 Add事件detail

当有学生被添加时,我们想知道哪些学生被添加进来了。

students.add(student8);
students.addAll(student10, student11);

private void handleAdded(Change<? extends Student> change) {
  int addedSize = change.getAddedSize();
  List<? extends Student> addedStudents = change.getAddedSubList();
}

3. ObservableSet

ObservableSet元素没有顺序,所以有排序带来的Permutate事件,也没有元素Property更改的Update事件,也没有ObservableList中的Replace事件。

3.1 创建ObservableSet

使用FXCollections中的静态方法创建:

// Create an empty unmodifiable ObservableSet.
<E> ObservableSet<E> emptyObservableSet()

// Create with given elements.
<E> ObservableSet<E> observableSet(E... elements)

// Create with anohter set.
<E> ObservableSet<E> observableSet(Set<E> set)

3.2 监听ObservableSet的Invalidation事件

当Add或remove元素时,Invalidation事件就会触发。

ObservableSet<Student> students = FXCollections.observableSet(student1, student2);

students.addListener(new InvalidationListener() {
  @Override
  public void invalidated(Observable observable) {
    System.out.println("Trigger invalidation event.");
  }
});

students.add(student3);
students.add(student4);
students.remove(student1);

// Output:
Trigger invalidation event.
Trigger invalidation event.
Trigger invalidation event.

3.3 监听ObservableSet的Content change事件

添加SetChangeListener,使用SetChangeListener的内部类Change来获得改动的具体内容。没有游标,每次添加或删除元素都会触发,如果是addAll()或removeAll(),有多少个元素被添加或删除,就触发多少次。

下面例子,ObservableSet中放的是Student对象,通过SetChangeListener可以知道那个学生被加入了,或哪个学生被删除了。

ObservableSet<Student> students = FXCollections.observableSet();

students.addListener(new SetChangeListener<Student>() {
  @Override
  public void onChanged(SetChangeListener.Change<? extends Student> change) {
    // Get the observable set associated with change.
    ObservableSet<? extends Student> set = change.getSet();
    if (change.wasAdded()) {
      Student addedStudent = change.getElementAdded();
      System.out.println("Student " + addedStudent.getId() + " is added.");
    } else if (change.wasRemoved()) {
      Student removedStudent = change.getElementRemoved();
      System.out.println("Student " + removedStudent.getId() + " is removed.");
    }
  }
});

students.add(student1);
students.addAll(Sets.newHashSet(student2, student3));
students.remove(student2);

// Output:
Student 1 is added.
Student 3 is added.
Student 2 is added.
Student 2 is removed.

4. ObservableMap

4.1 创建ObservableMap

使用FXCollections中的静态方法创建:

// Create an empty unmodifiable ObservableMap.
<K,V> ObservableMap<K,V> emptyObservableMap()

// Create an empty ObservableMap.
<K,V> ObservableMap<K, V> observableHashMap()

// Create with given map.
<K,V> ObservableMap<K, V> observableMap(Map<K, V> map)

4.2 监听ObservableMap的Invalidation事件

当有新的<key, value>加入或删除,或一个key的value发生了改变,就会触发Invalidation事件。

ObservableMap<Student, String> student2NameMap = FXCollections.observableHashMap();

student2NameMap.addListener(new InvalidationListener() {
  @Override
  public void invalidated(Observable observable) {
    System.out.println("Trigger map invalidation event.");
  }
});

student2NameMap.put(student1, student1.getName());

Map<Student, String> anotherMap = Maps.newHashMap();
anotherMap.put(student2, student2.getName());
anotherMap.put(student3, student3.getName());
student2NameMap.putAll(anotherMap);

student2NameMap.remove(student1);

// Output:
Trigger map invalidation event.
Trigger map invalidation event.
Trigger map invalidation event.
Trigger map invalidation event.

4.3 监听ObservableMap的content change事件

添加MapChangeListener事件,通过MapChangeListener的内部类Change,可以得到改动的具体内容,Change对象中的常用方法:

  • boolean wasAdded():判断是否是Add事件
  • booolean wasRemoved():判断是否是remove事件
  • K getKey():获得Add或Remove元素的Key
  • V getValueAdded():获得Add的Value
  • V getValueRemoved():获得Remove的Value
  • ObservableMap<K, V> getMap():获得存储元素的原始Map

下面例子中的ObservableMap,key是Student,value是student名字。

ObservableMap<Student, String> student2NameMap = FXCollections.observableHashMap();

student2NameMap.addListener(new MapChangeListener<Student, String>() {
  @Override
  public void onChanged(MapChangeListener.Change<? extends Student, ? extends String> change) {
    Student key = change.getKey();
    if (change.wasRemoved()) {
      String removedStudent = change.getValueRemoved();
      System.out.println("Removed student: <" + key.getId() + "," + removedStudent + ">");
    }

    if (change.wasAdded()) {
      String addedStudent = change.getValueAdded();
      System.out.println("Added student: <" + key.getId() + "," + addedStudent + ">");
    }
  }
});

4.3.1 Add & Remove <key, value>

增加或者删除元素,都会触发content change事件,可以得到具体的增加或站删除的<pair, value>;putAll()增加多个元素时,会触发多次事件。

student2NameMap.put(student1, student1.getName());
// Output: Added student: <1,Trump>

Map<Student, String> anotherMap = Maps.newHashMap();
anotherMap.put(student2, student2.getName());
anotherMap.put(student3, student2.getName());
student2NameMap.putAll(anotherMap);
// Output: 
// Added student: <2,Hillary>
// Added student: <3,Hillary>

student2NameMap.remove(student2);
// Output: Removed student: <2,Hillary>

4.3.2 添加已经存在的Key

添加一个已经存在的key时,会同时触发remove和add事件,先把之前的删了,再加入新的。

student2NameMap.put(student1, student1.getName());

student2NameMap.put(student1, "Fake Name");
// Output:
// Removed student: <1,Trump>
// Added student: <1,Fake Name>

5. 总结

Observable Collection就是实现了Observable接口的List, Set和Map,它们能够监听Invalidation事件和Content change事件。

  • Invalidation事件,大家都一样,使用InvalidationListener。
  • Content change事件,分别用ListChangeListener,SetChangeListener和MapChangeListener。

通过监听元素的变化,可以进而处理业务逻辑或者刷新UI。