Deriving Mapped Distinct Values into an Observable

2019-05-26 05:16发布

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();
    }
}

1条回答
forever°为你锁心
2楼-- · 2019-05-26 05:45

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);
    }

}
查看更多
登录 后发表回答