Thomas N. Thomas N. - 1 month ago 9
Java Question

Deriving Mapped Distinct Values into an ObservableList off another ObservableList?

I have an interesting problem, and I am relatively new to JavaFX and I need to create a somewhat niche

ObservableList
implementation.

Essentially, I need an
ObservableList
that maintains a list of mapped derived values off another
ObservableList
. I need to create an
ObservableDistinctList<P,V>
that accepts another
ObservableList<P>
and a
Function<P,V>
lambda as its constructor arguments. The
ObservableDistinctList<P,V>
maintains a list of distinct values off the applied
Function<P,V>
for each element in
ObservableList<P>
.

For example, say I have
ObservableList<Flight> flights
with the following instances.

Flt # Carrier Orig Dest Dep Date
174 WN ABQ DAL 5/6/2015
4673 WN DAL HOU 5/6/2015
485 DL DAL PHX 5/7/2015
6758 UA JFK HOU 5/7/2015


If I created a new ObservableDistinctList off the carrier values of each Flight object, this is how I would do it on the client side.

ObservableDistinctList<Flight,String> distinctCarriers = new
ObservableDistinctList(flights, f -> f.getCarrier());


These would be the only values in that
distinctCarriers
list.

WN
DL
UA


If a flight got added to
flights
, it would first check if a new distinct value is actually present before adding it. So a new
WN
flight would not cause an addition to the
distinctCarriers
list, but an
AA
flight will. Conversely, if a flight gets removed from
flights
, it needs to check if other instances would persist the value before removing it. Removing a WN flight from
flights
would not cause a removal of
WN
from the
distinctCarriers
list, but removing the
DL
flight will cause its removal.

Here is my implementation. Did I implement the
ListChangeListener
correctly? I get really uncomfortable with List mutability so I wanted to post this before I consider using it in my project. Also, do I need to worry about threadsafety using an
ArrayList
to back this?

public final class ObservableDistinctList<P,V> extends ObservableListBase<V> {

private final ObservableList<P> parentList;
private final Function<P,V> valueExtractor;
private final List<V> values;

public ObservableDistinctList(ObservableList<P> parentList, Function<P,V> valueExtractor) {
this.parentList = parentList;
this.valueExtractor = valueExtractor;
this.values = parentList.stream().map(p -> valueExtractor.apply(p)).distinct().collect(Collectors.toList());

this.parentList.addListener((ListChangeListener.Change<? extends P> c) -> {
while (c.next()) {
if (c.wasRemoved()) {
final Stream<V> candidatesForRemoval = c.getRemoved().stream().map(p -> valueExtractor.apply(p));
final List<V> persistingValues = parentList.stream().map(p -> valueExtractor.apply(p)).distinct().collect(Collectors.toList());

final Stream<V> valuesToRemove = candidatesForRemoval.filter(v -> ! persistingValues.contains(v));

valuesToRemove.forEach(v -> values.remove(v));
}

if (c.wasAdded()) {
final Stream<V> candidatesForAdd = c.getAddedSubList().stream().map(p -> valueExtractor.apply(p));
final List<V> existingValues = parentList.stream().map(p -> valueExtractor.apply(p)).distinct().collect(Collectors.toList());

final Stream<V> valuesToAdd = candidatesForAdd.filter(v -> ! values.contains(v));

valuesToAdd.forEach(v -> values.add(v));
}
}
});
}
@Override
public V get(int index) {
return values.get(index);
}

@Override
public int size() {
return values.size();
}
}

Answer

Below is a simple example (plus driver - hint: that's what you should have provided in the question :-) custom ObservableList that keeps distinct values of a property of the elements in a source list. It keeps itself sync'ed to the source on adding/removing items. The sync is implemented by:

  • listening to list changes of source
  • when receiving a removed: if the removed had been the last with the distinct property, remove that property from oneself and notify its own listener about the removal. Otherwise, there's nothing to do.
  • when receiving an added: if the added is the first with the distinct property, add the property to oneself (at the end) and notify its own listeners about the addition. Otherwise, there's nothing to do.

The notification is handled by messaging the utility methods nextRemove/nextAdd as appropriate.

/**
 * Example of how to implement a custom ObservableList.
 * 
 * Here: an immutable and unmodifiable (in itself) list containing distinct
 * values of properties of elements in a backing list, the values are extracted
 * via a function 
 */
public class DistinctMapperDemo extends Application {

    public static class DistinctMappingList<V, E> extends ObservableListBase<E> {

        private List<E> mapped;
        private Function<V, E> mapper;

        public DistinctMappingList(ObservableList<V> source, Function<V, E> mapper) {
            this.mapper = mapper;
            mapped = applyMapper(source); 
            ListChangeListener l = c -> sourceChanged(c);
            source.addListener(l);
        }

        private void sourceChanged(Change<? extends V> c) {
            beginChange();
            List<E> backing = applyMapper(c.getList());
            while(c.next()) {
                if (c.wasAdded()) {
                    wasAdded(c, backing);
                } else if (c.wasRemoved()) {
                    wasRemoved(c, backing);
                } else {
                    // throw just for the example
                    throw new IllegalStateException("unexpected change " + c);
                }
            }
            endChange();
        }

        private void wasRemoved(Change<? extends V> c, List<E> backing) {
            List<E> removedCategories = applyMapper(c.getRemoved());
            for (E e : removedCategories) {
                if (!backing.contains(e)) {
                    int index = indexOf(e);
                    mapped.remove(index);
                    nextRemove(index, e);
                }
            }
        }

        private void wasAdded(Change<? extends V> c, List<E> backing) {
            List<E> addedCategories = applyMapper(c.getAddedSubList());
            for (E e : addedCategories) {
                if (!contains(e)) {
                    int last = size();
                    mapped.add(e);
                    nextAdd(last, last +1);
                }
            }
        }

        private List<E> applyMapper(List<? extends V> list) {
            List<E> backing = list.stream().map(p -> mapper.apply(p)).distinct()
                    .collect(Collectors.toList());
            return backing;
        }

        @Override
        public E get(int index) {
            return mapped.get(index);
        }

        @Override
        public int size() {
            return mapped.size();
        }

    }

    int categoryCount;
    private Parent getContent() {
        ObservableList<DemoData> data = FXCollections.observableArrayList(
                new DemoData("first", "some"),
                new DemoData("second", "some"),
                new DemoData("first", "other"),
                new DemoData("dup", "other"),
                new DemoData("dodo", "next"),
                new DemoData("getting", "last")

                );
        TableView<DemoData> table = new TableView<>(data);
        TableColumn<DemoData, String> name = new TableColumn<>("Name");
        name.setCellValueFactory(new PropertyValueFactory<>("name"));
        TableColumn<DemoData, String> cat = new TableColumn<>("Category");
        cat.setCellValueFactory(new PropertyValueFactory<>("category"));
        table.getColumns().addAll(name, cat);

        Function<DemoData, String> mapper = c -> c.categoryProperty().get();
        ObservableList<String> mapped = new DistinctMappingList<>(data, mapper);
        ListView<String> cats = new ListView<>(mapped);

        Button remove = new Button("RemoveSelected DemoData");
        remove.setOnAction(e -> {
            int selected = table.getSelectionModel().getSelectedIndex(); 
            if (selected <0) return;
            data.remove(selected);
        });

        Button createNewCategory = new Button("Create DemoData with new Category");
        createNewCategory.setOnAction(e -> {
            String newCategory = data.size() == 0 ? "some" + categoryCount : 
                data.get(0).categoryProperty().get() + categoryCount;
            data.add(new DemoData("name" + categoryCount, newCategory));
            categoryCount++;
        });
        VBox buttons = new VBox(remove, createNewCategory);
        HBox box = new HBox(table, cats, buttons);
        return box;
    }

    public static class DemoData {
        StringProperty name = new SimpleStringProperty(this, "name");
        StringProperty category = new SimpleStringProperty(this, "category");

        public DemoData(String name, String category) {
            this.name.set(name);
            this.category.set(category);
        }

        public StringProperty nameProperty() {
            return name;
        }

        public StringProperty categoryProperty() {
            return category;
        }
    }
    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.setScene(new Scene(getContent()));
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

}
Comments