В иерархии JavaFX TransformationList есть нечто, называемое SortedList. Список полностью доступен для наблюдения, так что добавления / удаления будут уведомлять всех других слушателей, наблюдающих за списком.
Основной подход к этому состоит в том, что вы наблюдаете за другим ObservableList для изменений и стратегически используете Collections.binarySearch () , поскольку другие предложили найти индекс добавления или удаления во время Olog (n).
Существует одна проблема, о которой я не упоминал, и которая заключается в возможности отслеживать добавленные элементы, имеющие одинаковую подпись compareTo , т.е. T1.compareTo (T2) == 0. В этом случае отсортированный список (я опубликую свой собственный исходный код ниже) должен иметь тип элемента оболочки, который я назову Элемент . Это похоже на то, что создатели в JavaFX сделали с SortedList . Причина этого полностью связана с операциями удаления, невозможно найти исходный элемент, если есть дубликаты сравнения. Обычно в реализации NavigableSet , такой как TreeSet, эти дубликаты никогда не попадут в Set. Список другой.
У меня есть библиотека наблюдаемых списков, которые можно эффективно объединить в цепочку (очень похоже на потоки Java), которые полностью распространяют результаты в нисходящем направлении, как предыдущий источник в цепочке обновлений.
Иерархия классов
Интерфейс
/**
* Binds the elements of this list to the function application of each element of a
* source observable list.
* <p>
* While a {@code IListContentBinding} is bound, any attempt to modify its contents
* will result in an {@code UnsupportedOperationException}. To unbind the list, call
* {@link #unbind() unbind}.
*
* @param <S> The element type of the input source list that will generate change
* events.
* @param <T> The element type of this output list.
*/
public interface IListContentBinding<S, T> extends ObservableList<T>, ObservableListValue<T>, IContentBinding {... details not shown ....}
Абстрактный базовый класс для всех типов привязки (Сортировка, Разное, Карта, Плоская карта и т. Д.)
/**
* Binds the elements of this list to the function application of each element of a
* source observable list.
* <p>
* While a {@code ListContentBinding} is bound, any attempt to modify its contents
* will result in an {@code UnsupportedOperationException}. To unbind the list, call
* {@link #unbind() unbind}.
*
* @param <S> The element type of the source list that will generate change events.
* @param <T> The element type of this binding.
*/
public abstract class ListContentBinding<S, T> extends ObservableListWrapper<T>
implements IListContentBinding<S, T> {.... details not shown ....}
Класс связывания сортировки
/**
* A {@code ListContentBinding} implementation that generates sorted elements from a
* source list. The comparator can be set to another {@code Comparator} function at
* any time through the {@link #comparatorProperty() comparator} property.
* <p>
* Unlike the Collections {@link Collections#sort(List) list sort} or Arrays
* {@link Arrays#sort(Object[]) array sort}, once this binding has been added to the
* order of duplicate elements cannot be guaranteed to match the original order of
* the source list. That is the insertion and removal mechanism do not guarantee that
* the original order of duplicates (those items where T1.compareTo(T2) == 0) is
* preserved. However, any removal from the source list is <i>guaranteed</i> to
* remove the exact object from this sorted list. This is because an int <i>ID</i> field
* is added to the wrapped item through the {@link Element} class to ensure that
* matching duplicates can be further compared.
* <p>
* Added/Removed objects from the source list are placed inside this sorted list
* through the {@link Arrays#binarySearch(Object[], Object, Comparator) array binary
* search} algorithm. For any duplicate item in the sorted list, a further check on
* the ID of the {@code Element} corresponding to that item is compared to the
* original, and that item. Each item added to this sorted list increases the
* counter, the maximum number of items that should be placed in this list should be
* no greater than {@code Integer.MAX_VALUE - Integer.MIN_VALUE}, or 4,294,967,295
* total elements. Sizes greater than this value for an instance of this class
* may produce unknown behavior.
* <p>
* Removal and additions to this list binding are proportional to <i>O(logn)</i>
* runtime, where <i>n</i> is the current total number of elements in this sorted
* list.
*
* @param <T> The element type of the source and this list binding.
*/
class ListContentSortBinding<T> extends ListContentBinding<T, T> implements IListContentSortBinding<T> {
/**
* Each location in the source list has a random value associated it with to deal
* with duplicate elements that would return T1.compareTo(T2) == 0.
*/
private Element[] elements = newElementArray(10);
/**
* The same elements from {@link #elements} but placed in their correct sorted
* position according to the {@link #elementComparator element comparator}.
*/
protected Element[] sortedElements = newElementArray(10);
/**
* Create a new instance.
*
* @param source The source observable list. Sorted elements will be generated
* from the source and set as the content of this list binding.
* @param comparator The sorter. An observable that will update the comparator of
* this binding when invalidated. The sorter can be set to another
* {@code Comparator} function at anytime through the
* {@link #comparatorProperty() comparator} property.
* @param options The options of this binding. Considers {@code DependencyOption}
* instances.
* <p>
* All bindings consider {@code BeforeChangeOption} and
* {@code AfterChangeOption}.
*/
@SafeVarargs
ListContentSortBinding(ObservableList<T> source, ObservableObjectValue<Comparator<? super T>> comparator,
BindingOption<T, T>... options) {
this(source, comparator.get(), options);
comparatorProperty().bind(comparator);
}
/**
* Create a new instance.
*
* @param source The source observable list. Sorted elements will be generated
* from the source and set as the content of this list binding.
* @param comparator The sorter. The sorter can be set to another
* {@code Comparator} function at anytime through the
* {@link #comparatorProperty() comparator} property.
* @param options The options of this binding. Considers {@code DependencyOption}
* instances.
* <p>
* All bindings consider {@code BeforeChangeOption} and
* {@code AfterChangeOption}.
*/
@SafeVarargs
ListContentSortBinding(ObservableList<T> source, Comparator<? super T> comparator,
BindingOption<T, T>... options) {
super(new ArrayList<>(), options);
List<Observable> observables = new ArrayList<>(
Arrays.asList(BindingOptionBuilder.extractDependencies(options)));
setComparator(comparator);
observables.add(comparatorProperty());
bind(source, observables.toArray(new Observable[observables.size()]));
}
@Override
protected void sourceChanged(Change<? extends T> change) {
List<? extends T> source = change.getList();
while (change.next()) {
int from = change.getFrom();
if (change.wasPermutated() || change.wasUpdated()) {
List<? extends T> srcMod = source.subList(from, change.getTo());
removed(source, from, srcMod.size());
added(source, from, srcMod);
} else {
List<? extends T> removed = change.getRemoved();
List<? extends T> added = change.getAddedSubList();
if (change.wasReplaced()) {
int min = Math.min(added.size(), removed.size());
replaced(source, from, added.subList(0, min));
added = added.subList(min, added.size());
removed = removed.subList(min, removed.size());
}
if (removed.size() > 0) {
removed(source, from, removed.size());
}
if (added.size() > 0) {
if (source.size() >= elements.length) {
ensureSize(source.size());
}
added(source, from, added);
}
ensureSize(source.size());
}
}
}
/**
* Replace the items in this sorted list binding resulting from a replacement
* operation in the source list. For each of the items added starting at the
* <i>from</i> index in the source list, and items was removed at the same source
* position.
*
* @param source The source list.
* @param from The index of where the replacement started in the source
* (inclusive). The removed and added elements occurred starting at
* the same source position.
* @param added The added source elements from the change.
*/
@SuppressWarnings({})
private void replaced(List<? extends T> source, int from, List<? extends T> added) {
int oldSize = size();
for (int i = 0; i < added.size(); i++) {
int index = from + i;
Element e = elements[index];
// Find the old element and remove it
int pos = findPosition(e, index, oldSize);
System.arraycopy(sortedElements, pos + 1, sortedElements, pos, oldSize - pos - 1);
remove(pos);
T t = added.get(i);
// Create a new element and add it
e = new Element(t);
elements[index] = e;
pos = findPosition(e, index, oldSize - 1);
if (pos < 0) {
pos = ~pos;
}
System.arraycopy(sortedElements, pos, sortedElements, pos + 1, oldSize - pos - 1);
sortedElements[pos] = e;
add(pos, t);
}
}
/**
* Add the elements from the source observable list to this binding.
*
* @param source The source list.
* @param from The index of where the addition started in the source (inclusive).
* @param added The added source elements from the change.
*/
@SuppressWarnings({})
private void added(List<? extends T> source, int from, List<? extends T> added) {
if (size() == 0) {
int size = added.size();
Element[] temp = newElementArray(size);
for (int i = 0; i < added.size(); i++) {
T t = added.get(i);
Element e = new Element(t);
elements[i] = e;
temp[i] = e;
}
if (elementComparator == null) {
addAll(added);
return;
}
Arrays.sort(temp, elementComparator);
System.arraycopy(temp, 0, sortedElements, 0, temp.length);
addAll(Arrays.stream(temp).map(e -> (T) e.t).collect(Collectors.toList()));
return;
}
int size = size();
System.arraycopy(elements, from, elements, from + added.size(), size - from);
for (int i = 0; i < added.size(); i++) {
int index = from + i;
T t = added.get(i);
Element e = new Element(t);
int pos = findPosition(e, index, size);
if (pos < 0) {
pos = ~pos;
}
elements[index] = e;
if (pos < size) {
System.arraycopy(sortedElements, pos, sortedElements, pos + 1, size - pos);
}
sortedElements[pos] = e;
add(pos, t);
size++;
}
}
/**
* Remove the elements from this binding that were removed from the source list.
* Update the {@link #elements} mapping.
*
* @param source The source list.
* @param from The index of where the removal started in the source (inclusive).
* @param removedSize The total number of removed elements from the source list
* for the change.
*/
@SuppressWarnings({})
private void removed(List<? extends T> source, int from, int removedSize) {
if (source.size() == 0) {
elements = newElementArray(10);
sortedElements = newElementArray(10);
elementCounter = Integer.MIN_VALUE;
clear();
return;
}
int oldSize = size();
int size = oldSize;
for (int i = 0; i < removedSize; i++) {
int index = from + i;
Element e = elements[index];
int pos = findPosition(e, index, size);
System.arraycopy(sortedElements, pos + 1, sortedElements, pos, size - pos - 1);
remove(pos);
sortedElements[--size] = null;
}
System.arraycopy(elements, from + removedSize, elements, from, oldSize - from - removedSize);
for (int i = size; i < oldSize; i++) {
elements[i] = null;
}
}
/**
* Locate the position of the element in this sorted binding by performing a
* binary search. A binary search locates the index of the add in Olog(n) time.
*
* @param e The element to insert.
* @param sourceIndex The index of the source list of the modification.
* @param size The size of the array to search, exclusive.
*
* @return The position in this binding that the element should be inserted.
*/
private int findPosition(Element e, int sourceIndex, int size) {
if (size() == 0) {
return 0;
}
int pos;
if (elementComparator != null) {
pos = Arrays.binarySearch(sortedElements, 0, size, e, elementComparator);
} else {
pos = sourceIndex;
}
return pos;
}
/**
* Ensure that the element array is large enough to handle new elements from the
* source list. Also shrinks the size of the array if it has become too large
* with respect to the source list.
*
* @param size The minimum size of the array.
*/
private void ensureSize(int size) {
if (size >= elements.length) {
int newSize = size * 3 / 2 + 1;
Element[] replacement = newElementArray(newSize);
System.arraycopy(elements, 0, replacement, 0, elements.length);
elements = replacement;
replacement = newElementArray(newSize);
System.arraycopy(sortedElements, 0, replacement, 0, sortedElements.length);
sortedElements = replacement;
} else if (size < elements.length / 4) {
int newSize = size * 3 / 2 + 1;
Element[] replacement = newElementArray(newSize);
System.arraycopy(elements, 0, replacement, 0, replacement.length);
elements = replacement;
replacement = newElementArray(newSize);
System.arraycopy(sortedElements, 0, replacement, 0, replacement.length);
sortedElements = replacement;
}
}
/**
* Combines the {@link #comparatorProperty() item comparator} with a secondary
* comparison if the items are equal through the <i>compareTo</i> operation. This
* is used to quickly find the original item when 2 or more items have the same
* comparison.
*/
private Comparator<Element> elementComparator;
/**
* @see #comparatorProperty()
*/
private ObjectProperty<Comparator<? super T>> comparator =
new SimpleObjectProperty<Comparator<? super T>>(this, "comparator") {
@Override
protected void invalidated() {
Comparator<? super T> comp = get();
if (comp != null) {
elementComparator = Comparator.nullsLast((e1, e2) -> {
int c = comp.compare(e1.t, e2.t);
return c == 0 ? Integer.compare(e1.id, e2.id) : c;
});
} else {
elementComparator = null;
}
}
};
@Override
public final ObjectProperty<Comparator<? super T>> comparatorProperty() {
return comparator;
}
@Override
public final Comparator<? super T> getComparator() {
return comparatorProperty().get();
}
@Override
public final void setComparator(Comparator<? super T> comparator) {
comparatorProperty().set(comparator);
}
@Override
protected void onInvalidating(ObservableList<T> source) {
clear();
ensureSize(source.size());
added(source, 0, source);
}
/**
* Counter starts at the Integer min value, and increments each time a new
* element is requested. If this list becomes empty, the counter is restarted at
* the min value.
*/
private int elementCounter = Integer.MIN_VALUE;
/**
* Generate a new array of {@code Element}.
*
* @param size The size of the array.
*
* @return A new array of null Elements.
*/
@SuppressWarnings("unchecked")
private Element[] newElementArray(int size) {
return new ListContentSortBinding.Element[size];
}
Класс оберточного элемента
/**
* Wrapper class to further aid in comparison of two object types <T>. Since
* sorting in a list allows duplicates we must assure that when a removal occurs
* from the source list feeding this binding that the removed element matches. To
* do this we add an arbitrary <i>int</i> field inside this element class that
* wraps around the original object type <T>.
*/
final class Element {
/** Object */
private final T t;
/** ID helper for T type duplicates */
private int id;
Element(T t) {
this.t = Objects.requireNonNull(t);
this.id = elementCounter++;
}
@Override
public String toString() {
return t.toString() + " (" + id + ")";
}
}
}
ИСПЫТАНИЕ НА УСТАНОВКУ JUNIT
@Test
public void testSortBinding() {
ObservableList<IntWrapper> source = FXCollections.observableArrayList();
int size = 100000;
for (int i = 0; i < size / 2; i++) {
int index = (int) (Math.random() * size / 10);
source.add(new IntWrapper(index));
}
ListContentSortBinding<IntWrapper> binding =
(ListContentSortBinding<IntWrapper>) CollectionBindings.createListBinding(source).sortElements();
Assert.assertEquals("Sizes not equal for sorted binding | Expected: " +
source.size() + ", Actual: " + binding.size(),
source.size(), binding.size());
List<IntWrapper> sourceSorted = new ArrayList<>(source);
Collections.sort(sourceSorted);
for (int i = 0; i < source.size(); i++) {
IntWrapper expected = sourceSorted.get(i);
IntWrapper actual = binding.get(i);
Assert.assertEquals("Elements not equal in expected sorted lists | Expected: " +
expected.value + ", Actual: " + actual.value,
expected.value, actual.value);
}
System.out.println("Sorted Elements Equal: Complete.");
// Randomly add chunks of elements at random locations in the source
int addSize = size / 10000;
for (int i = 0; i < size / 4; i++) {
List<IntWrapper> added = new ArrayList<>();
int toAdd = (int) (Math.random() * addSize);
for (int j = 0; j < toAdd; j++) {
int index = (int) (Math.random() * size / 10);
added.add(new IntWrapper(index));
}
int atIndex = (int) (Math.random() * source.size());
source.addAll(atIndex, added);
}
sourceSorted = new ArrayList<>(source);
Collections.sort(sourceSorted);
for (int i = 0; i < source.size(); i++) {
IntWrapper expected = sourceSorted.get(i);
IntWrapper actual = binding.get(i);
Assert.assertEquals("Elements not equal in expected sorted lists | Expected: " +
expected.value + ", Actual: " + actual.value,
expected.value, actual.value);
}
System.out.println("Sorted Elements Equal - Add Multiple Elements: Complete.");
// Remove one element at a time from the source list and compare
// to the elements that were removed from the sorted binding
// as a result. They should all be identical index for index.
List<IntWrapper> sourceRemoved = new ArrayList<>();
List<IntWrapper> bindingRemoved = new ArrayList<>();
ListChangeListener<IntWrapper> bindingListener = change -> {
while (change.next()) {
if (change.wasRemoved()) {
bindingRemoved.addAll(change.getRemoved());
}
}
};
// Watch the binding for changes after the upstream source changes
binding.addListener(bindingListener);
for (int i = 0; i < size / 4; i++) {
int index = (int) (Math.random() * source.size());
IntWrapper removed = source.remove(index);
sourceRemoved.add(removed);
}
for (int i = 0; i < bindingRemoved.size(); i++) {
IntWrapper expected = bindingRemoved.get(i);
IntWrapper actual = sourceRemoved.get(i);
Assert.assertEquals("Elements not equal in expected sorted lists | Expected: " +
expected + ", Actual: " + actual,
expected.value, actual.value);
Assert.assertEquals("Element refs not equal in expected sorted lists | Expected: " +
expected.value + ", Actual: " + actual.value,
expected.r, actual.r, 0);
}
System.out.println("Sorted Remove Single Element: Complete.");
// Replace random elements from the source list
bindingRemoved.clear();
sourceRemoved.clear();
int removeSize = size / 10000;
for (int i = 0; i < size / 1000; i++) {
int replaceIndex = (int) (Math.random() * source.size());
int index = (int) (Math.random() * size / 10);
IntWrapper replace = new IntWrapper(index);
source.set(replaceIndex, replace);
}
sourceSorted = new ArrayList<>(source);
Collections.sort(sourceSorted);
for (int i = 0; i < source.size(); i++) {
IntWrapper expected = sourceSorted.get(i);
IntWrapper actual = binding.get(i);
Assert.assertEquals("Elements not equal in expected sorted lists | Expected: " +
expected.value + ", Actual: " + actual.value,
expected.value, actual.value);
}
System.out.println("Sorted Elements Replace: Complete.");
// Remove random chunks from the source list
bindingRemoved.clear();
sourceRemoved.clear();
Set<IntWrapper> sourceRemovedSet =
Collections.newSetFromMap(new IdentityHashMap<>()); // set for speed
while (source.size() > 0) {
int index = (int) (Math.random() * source.size());
int toRemove = (int) (Math.random() * removeSize);
toRemove = Math.min(toRemove, source.size() - index);
List<IntWrapper> removed = source.subList(index, index + toRemove);
sourceRemovedSet.addAll(new ArrayList<>(removed));
removed.clear(); // triggers list change update to binding
}
Assert.assertEquals(bindingRemoved.size(), sourceRemovedSet.size());
// The binding removed will not necessarily be placed in the same order
// since the change listener on the binding will make sure that the final
// order of the change from the binding is in the same order as the binding
// element sequence. We therefore must do a contains() to test.
for (int i = 0; i < bindingRemoved.size(); i++) {
IntWrapper expected = bindingRemoved.get(i);
Assert.assertTrue("Binding Removed Did Not Contain Source Removed",
sourceRemovedSet.contains(expected));
}
System.out.println("Sorted Removed Multiple Elements: Complete.");
}
ИСПЫТАНИЕ НА ЭЛЕМЕНТЕ СТАДИИ
@Test
public void sortBindingBenchmark() {
ObservableList<IntWrapper> source = FXCollections.observableArrayList();
ObservableList<IntWrapper> binding =
(ListContentSortBinding<IntWrapper>) CollectionBindings.createListBinding(source).sortElements();
int size = 200000;
Set<IntWrapper> toAdd = new TreeSet<>();
while (toAdd.size() < size) {
int index = (int) (Math.random() * size * 20);
toAdd.add(new IntWrapper(index));
}
// Randomize the order
toAdd = new HashSet<>(toAdd);
System.out.println("Sorted Binding Benchmark Setup: Complete.");
long time = System.currentTimeMillis();
for (IntWrapper w : toAdd) {
source.add(w);
}
long bindingTime = System.currentTimeMillis() - time;
System.out.println("Sorted Binding Time: Complete.");
source.clear(); // clear the list and re-add
ObservableList<IntWrapper> sortedList = new SortedList<>(source);
time = System.currentTimeMillis();
for (IntWrapper w : toAdd) {
source.add(w);
}
long sortedListTime = System.currentTimeMillis() - time;
System.out.println("JavaFX Sorted List Time: Complete.");
// Make the test "fair" by adding a listener to an observable
// set that populates the sorted set
ObservableSet<IntWrapper> obsSet = FXCollections.observableSet(new HashSet<>());
Set<IntWrapper> sortedSet = new TreeSet<>();
obsSet.addListener((SetChangeListener<IntWrapper>) change -> {
sortedSet.add(change.getElementAdded());
});
time = System.currentTimeMillis();
for (IntWrapper w : toAdd) {
obsSet.add(w);
}
long setTime = System.currentTimeMillis() - time;
System.out.println("Sorted Binding Benchmark Time: Complete");
Assert.assertEquals(sortedSet.size(), binding.size());
System.out.println("Binding: " + bindingTime + " ms, " +
"JavaFX Sorted List: " + sortedListTime + " ms, " +
"TreeSet: " + setTime + " ms");
}
Класс Wrapper только для тестов
/**
* Wrapper class for testing sort bindings. Verifies that duplicates were sorted
* and removed correctly based on the object instance.
*/
private static class IntWrapper implements Comparable<IntWrapper> {
static int counter = Integer.MIN_VALUE;
final int value;
final int id;
IntWrapper(int value) {
this.value = value;
this.id = counter++;
}