3

In order to return an immutable observable list to the clients of my API, I've used the FXCollections.unmodifiableObservableList(list) wrapper as follows:

private final ObservableList<Person> persons = FXCollections.observableArrayList();

public ObservableList<Person> getPersons() {
    return FXCollections.unmodifiableObservableList(persons);
}

However, when a client adds a ListChangeListener to the returned list, it does not get notified when changes occur. This is apparently because the wrapper created by the FXCollections class sets a weak listener on the wrapped list, and this weak listener gets garbage collected.

Did I miss something about this wrapper?

What's the proper way to return an immutable observable list?

Stéphane Appercel
  • 1,427
  • 2
  • 13
  • 21

2 Answers2

4

You are on the right track: the wrapper list adds a weak listener to the wrapped list in order to avoid memory leaks, and if you don't hold a reference to the wrapper list it will be garbage collected.

See this test (taken from here):

private ObservableList<String> s = FXCollections.observableArrayList();

@Override
public void init() throws Exception {
    FXCollections.unmodifiableObservableList(s).addListener((ListChangeListener.Change<? extends String> c) -> 
        System.out.println(c));
    s.setAll("A1");
    s.setAll("A2");
    System.gc();
    s.setAll("A3"); // <- Doesn't trigger the listener
}

It prints:

{ [A1] added at 0 }
{ [A1] replaced by [A2] at 0 }

But if you add a reference to the list:

private ObservableList<String> s = FXCollections.observableArrayList();

@Override
public void init() throws Exception {
    // create a reference 
    final ObservableList<String> wrapperList = FXCollections.unmodifiableObservableList(s); 

    wrapperList.addListener((ListChangeListener.Change<? extends String> c) -> 
        System.out.println(c));
    s.setAll("A1");
    s.setAll("A2");
    System.gc();
    s.setAll("A3"); // <- Triggers the listener
}

Now it prints:

{ [A1] added at 0 }
{ [A1] replaced by [A2] at 0 }
{ [A2] replaced by [A3] at 0 }
José Pereda
  • 44,311
  • 7
  • 104
  • 132
  • So basically I need to have an additional field property that will hold a reference to the immutable observable list in addition to the original observable list itself, and suppress the warning when I return the immutable field. This is not developer friendly. Any other option? – Stéphane Appercel Jun 03 '17 at 10:52
  • This is the way it was designed. If you've checked the link I [posted](https://bugs.openjdk.java.net/browse/JDK-8088454), that's the recommended solution. – José Pereda Jun 03 '17 at 11:02
  • @StéphaneAppercel: The approach is also consonant with my findings [here](https://stackoverflow.com/a/44343112/230513), although I'd welcome any additional insight. – trashgod Jun 03 '17 at 17:11
1

For reference, I profiled the (somewhat contrived) example below though several periodic and forced garbage collection cycles without seeng anything untoward—only a small, secular, upward trend as instances of Integer accumulate. The WeakListChangeListener added to the backing list is held by a private implementation. The weak listener is removed from the backing list when the listener becomes null and can no longer forward changes to the unmodifiable list's listener(s). If a client's listener stops seeing changes, you may need to look at how the client manages the list returned by getPersons().

Addendum: As note in the WeakListChangeListener API and here by @José Pereda, "You have to keep a reference to the ListChangeListener that was passed in as long as it is in use, otherwise it will be garbage collected too soon."

changed { [0] added at 0 }
…
changed { [2162] added at 2162 }

profile

import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;

/**
 * @see https://stackoverflow.com/a/44343112/230513
 */
public class ObservableListTest {

    public static void main(String[] args) throws Exception {
        ObservableList<Integer> list1 = FXCollections.observableArrayList();
        ObservableList<Integer> list2 = FXCollections.unmodifiableObservableList(list1);
        list2.addListener((ListChangeListener.Change<? extends Integer> c) -> {
            System.out.println("changed " + c);
        });
        int i = 0;
        while (true) {
            list1.add(Integer.valueOf(i++));
            Thread.sleep(1000);
        }
    }
}
trashgod
  • 203,806
  • 29
  • 246
  • 1,045