从另一个 ObservableList 中将映射的不同值派生到一个 ObservableList 中?

Deriving Mapped Distinct Values into an ObservableList off another ObservableList?

我有一个有趣的问题,我是 JavaFX 的新手,我需要创建一个有点小众的 ObservableList 实现。

本质上,我需要一个 ObservableList 来维护另一个 ObservableList 的映射派生值列表。我需要创建一个 ObservableDistinctList<P,V> 接受另一个 ObservableList<P> 和一个 Function<P,V> lambda 作为其构造函数参数。 ObservableDistinctList<P,V>ObservableList<P>.

中的每个元素维护了应用 Function<P,V> 的不同值列表

例如,假设我 ObservableList<Flight> flights 具有以下实例。

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

如果我根据每个 Flight 对象的承运人值创建一个新的 ObservableDistinctList,我将在客户端执行此操作。

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

这些将是 distinctCarriers 列表中的唯一值。

WN
DL
UA

如果将航班添加到 flights,它会在添加之前首先检查是否确实存在新的不同值。因此,新的 WN 航班不会导致添加到 distinctCarriers 列表中,但 AA 航班会。相反,如果航班从 flights 中删除,它需要检查其他实例是否会在删除之前保留该值。从 flights 中删除 WN 航班不会导致从 distinctCarriers 列表中删除 WN,但删除 DL 航班将导致其删除。

这是我的实现。我是否正确实施了 ListChangeListener?我对 List 可变性感到非常不舒服,所以我想 post 在我考虑在我的项目中使用它之前。另外,我是否需要担心使用 ArrayList 来支持它的线程安全?

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

下面是一个简单示例(加上驱动程序 - 提示:这就是您应该在问题中提供的内容:-) 自定义 ObservableList,它在源列表中保留 属性 元素的不同值。它使自己与 adding/removing 项上的源保持同步。同步通过以下方式实现:

  • 正在监听源列表变化
  • 收到删除时:如果删除是最后一个具有不同 属性 的,则从自己中删除 属性 并通知其自己的侦听器有关删除的信息。不然没啥可做的。
  • 收到添加时:如果添加的是第一个具有不同 属性 的,则将 属性 添加到自己(最后)并通知其自己的听众有关添加的信息。不然没啥可做的。

通知是通过向实用方法nextRemove/nextAdd发送消息来处理的。

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

}