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?

0

2 Answers 2

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 }
Sign up to request clarification or add additional context in comments.

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?
This is the way it was designed. If you've checked the link I posted, that's the recommended solution.
@StéphaneAppercel: The approach is also consonant with my findings here, although I'd welcome any additional insight.
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);
        }
    }
}

Comments

Your Answer

Draft saved
Draft discarded

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.