Сопоставить элементы коллекции и сохранить ссылку на исходную коллекцию
Я ищу способ создать коллекцию, список, набор или карту, которая содержит преобразованные элементы оригинальной коллекции и отражает все изменения в этой коллекции.
Например, если у меня есть List<Integer>
от стороннего API, а другой API ожидает List<String>
. Я знаю, что могу преобразовать список следующим образом:
List<Integer> intList = thirdPartyBean.getIntListProperty();
List<String> stringList = intList.stream().map(Integer::toString)
.collect(Collectors.toList());
secondBean.setStringListProperty(stringList);
Проблема в том, что если что-то изменится в одном из списков, другой будет по-прежнему отражать предыдущее состояние. Предположим, что intList
содержит [1, 2, 3]
:
intList.add(4);
stringList.remove(0);
System.out.println(intList.toString()); // will print: [1, 2, 3, 4]
System.out.println(stringList.toString()); // will print: [2, 3]
// Expected result of both toString(): [2, 3, 4]
Поэтому я ищу что-то вроде List.sublist(from, to)
где результат "поддерживается" исходным списком.
Я думаю о реализации моей собственной обертки списка, которая используется следующим образом:
List<String> stringList = new MappedList<>(intList, Integer::toString, Integer::valueOf);
Вторая лямбда - для инвертирования преобразования, для поддержки вызовов вроде stringList.add(String)
.
Но прежде чем я сам это реализую, я хотел бы знать, пытаюсь ли я заново изобрести колесо - может быть, уже есть общее решение этой проблемы?
Ответы
Ответ 1
Я бы обернул список в другой List
с прикрепленными трансформаторами.
public class MappedList<S, T> extends AbstractList<T> {
private final List<S> source;
private final Function<S, T> fromTransformer;
private final Function<T, S> toTransformer;
public MappedList(List<S> source, Function<S, T> fromTransformer, Function<T, S> toTransformer) {
this.source = source;
this.fromTransformer = fromTransformer;
this.toTransformer = toTransformer;
}
public T get(int index) {
return fromTransformer.apply(source.get(index));
}
public T set(int index, T element) {
return fromTransformer.apply(source.set(index, toTransformer.apply(element)));
}
public int size() {
return source.size();
}
public void add(int index, T element) {
source.add(index, toTransformer.apply(element));
}
public T remove(int index) {
return fromTransformer.apply(source.remove(index));
}
}
private void test() {
List<Integer> intList = new ArrayList<>(Arrays.asList(1, 2, 3));
List<String> stringList = new MappedList<>(intList, String::valueOf, Integer::valueOf);
intList.add(4);
stringList.remove(0);
System.out.println(intList); // Prints [2, 3, 4]
System.out.println(stringList); // Prints [2, 3, 4]
}
Обратите внимание, что fromTransformer
нуждается в проверке null
для входного значения, если source
может содержать null
.
Теперь вы не трансформируете исходный список в другой и теряете контакт с оригиналом, вы добавляете трансформацию в исходный список.
Ответ 2
Я не знаю, какую версию JDK вы используете, но если вы согласны с использованием библиотеки JavaFX, вы можете использовать ObservableList
. Вам не нужно изменять существующий список, так как ObservableList
является оболочкой для java.util.List
. Посмотрите на экстрактор в FXCollection для сложных объектов. Эта статья имеет пример этого.
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.ListChangeListener.Change;
public class ObservableBiList{
//prevent stackoverflow
private static final AtomicBoolean wasChanged = new AtomicBoolean( false);
public static <T, R> void change( Change< ? extends T> c, ObservableList< R> list, Function< T, R> convert) {
if( wasChanged.get()){
wasChanged.set( false);
return;
}
wasChanged.set( true);
while( c.next()){
if( c.wasAdded() && !c.wasReplaced()){
for( T str : c.getRemoved())
list.add( convert.apply( str));
}else if( c.wasReplaced()){
for( int i=c.getFrom();i<c.getTo();i++)
list.set( i,convert.apply( c.getList().get( i)));
}else if( c.wasRemoved()){
for( T str : c.getRemoved())
list.remove( convert.apply( str));
}
}
System.out.printf( "Added: %s, Replaced: %s, Removed: %s, Updated: %s, Permutated: %s%n",
c.wasAdded(), c.wasReplaced(), c.wasRemoved(), c.wasUpdated(), c.wasPermutated());
}
public static void main( String[] args){
ObservableList< Integer> intList = FXCollections.observableArrayList();
intList.addAll( 1, 2, 3, 4, 5, 6, 7);
ObservableList< String> stringList = FXCollections.observableArrayList();
stringList.addAll( "1", "2", "3", "4", "5", "6", "7");
intList.addListener( ( Change< ? extends Integer> c) -> change( c, stringList, num->Integer.toString( num)));
stringList.addListener( ( Change< ? extends String> c) -> change( c, intList, str->Integer.valueOf( str)));
intList.set( 1, 22);
stringList.set( 3, "33");
System.out.println( intList);
System.out.println( stringList);
}
}
Ответ 3
Это именно тот тип проблем, который решает шаблон наблюдателя.
Вы можете создать две оболочки вокруг List<String>
и List<Integer>
и позволить первой оболочке наблюдать за состоянием другой.
Ответ 4
public static void main(String... args) {
List<Integer> intList = ObservableList.createBase(new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)));
List<String> stringList = ObservableList.createBase(intList, String::valueOf);
stringList.remove(0);
intList.add(6);
System.out.println(String.join(" ", stringList));
System.out.println(intList.stream().map(String::valueOf).collect(Collectors.joining(" ")));
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private static final class ObservableList<T, E> extends AbstractList<E> {
// original list; only this one could be used to add value
private final List<T> base;
// current snapshot; could be used to remove value;
private final List<E> snapshot;
private final Map<Function<T, ?>, List> cache;
public static <T, E> List<E> createBase(List<T> base) {
Objects.requireNonNull(base);
if (base instanceof ObservableList)
throw new IllegalArgumentException();
return new ObservableList<>(base, null, new HashMap<>());
}
public static <T, R> List<R> createBase(List<T> obsrv, Function<T, R> func) {
Objects.requireNonNull(obsrv);
Objects.requireNonNull(func);
if (!(obsrv instanceof ObservableList))
throw new IllegalArgumentException();
return new ObservableList<>(((ObservableList<T, R>)obsrv).base, func, ((ObservableList<T, R>)obsrv).cache);
}
@SuppressWarnings("AssignmentOrReturnOfFieldWithMutableType")
private ObservableList(List<T> base, Function<T, E> func, Map<Function<T, ?>, List> cache) {
this.base = base;
snapshot = func != null ? base.stream().map(func).collect(Collectors.toList()) : (List<E>)base;
this.cache = cache;
cache.put(func, snapshot);
}
@Override
public E get(int index) {
return snapshot.get(index);
}
@Override
public int size() {
return base.size();
}
@Override
public void add(int index, E element) {
if (base != snapshot)
super.add(index, element);
base.add(index, (T)element);
cache.forEach((func, list) -> {
if (func != null)
list.add(index, func.apply((T)element));
});
}
@Override
public E remove(int index) {
E old = snapshot.remove(index);
for (List<?> back : cache.values())
if (back != snapshot)
back.remove(index);
return old;
}
}
System.out.println(String.join(" ", stringList));
System.out.println(intList.stream().map(String::valueOf).collect(Collectors.joining(" ")));
}
private static final class ObservableList<E> extends AbstractList<E> {
private final List<List<?>> cache;
private final List<E> base;
public static <E> List<E> create(List<E> delegate) {
if (delegate instanceof ObservableList)
return new ObservableList<>(((ObservableList<E>)delegate).base, ((ObservableList<E>)delegate).cache);
return new ObservableList<>(delegate, new ArrayList<>());
}
public static <T, R> List<R> create(List<T> delegate, Function<T, R> func) {
List<R> base = delegate.stream().map(func).collect(Collectors.toList());
List<List<?>> cache = delegate instanceof ObservableList ? ((ObservableList<T>)delegate).cache : new ArrayList<>();
return new ObservableList<>(base, cache);
}
@SuppressWarnings("AssignmentOrReturnOfFieldWithMutableType")
private ObservableList(List<E> base, List<List<?>> cache) {
this.base = base;
this.cache = cache;
cache.add(base);
}
@Override
public E get(int index) {
return base.get(index);
}
@Override
public int size() {
return base.size();
}
@Override
public void add(int index, E element) {
for (List<?> back : cache)
back.add(index, element);
}
@Override
public E remove(int index) {
E old = base.remove(index);
for (List<?> back : cache)
if (back != base)
back.remove(index);
return old;
}
}
Ответ 5
Вам нужно создать оболочку поверх первого списка, учитывая ваш пример, List<Integer>
. Теперь, если вы хотите, чтобы List<String>
отражал все изменения времени выполнения, внесенные в List<Integer>
, у вас есть два решения.
-
Не создавайте начальный List<String>
, используйте метод или оболочку, которая всегда будет возвращать преобразованные значения из List<Integer>
, поэтому у вас никогда не будет статического List<String>
.
-
Создайте обертку вокруг List<Integer>
, которая должна иметь ссылку на List<String>
, и переопределите методы add(), addAll(), remove() и removeAll(). В переопределенных методах измените состояние вашего List<String>
.
Ответ 6
Другой вариант - использовать класс JavaFX ObservableList
который может обернуть существующий список наблюдаемым слоем, на котором вы можете определить операции, которые вы хотите распространять.
Вот пример, который распространяется из списка строк в список целых чисел:
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
ObservableList<String> strings = FXCollections.observableList(strList);
strings.addListener((ListChangeListener<String>) change -> {
if(change.next()) {
if (change.wasAdded()) {
change.getAddedSubList().stream().map(Integer::valueOf).forEach(intList::add);
} else if (change.wasRemoved()) {
change.getRemoved().stream().map(Integer::valueOf).forEach(intList::remove);
}
}
});
strList = strings;
strList.add("1");
strList.add("2");
strList.add("2");
System.out.println(intList);
strList.remove("1");
System.out.println(intList);
Если вы выполните этот код, вы увидите этот вывод на консоли:
[1, 2, 2]
[2, 2]
Ответ 7
Из вашего примера я предполагаю, что метод, к которому у вас нет доступа, только изменяет список и не имеет доступа к самим данным. Вы можете использовать Raw Types.
List list = new ArrayList<Object>();
Если вы хотите получить доступ к данным, вы должны преобразовать все в нужный тип.
list.stream().map(String::valueOf).<do_something>.collect(toList())
Не самое чистое решение, но может работать на вас. Я думаю, что самым чистым решением было бы внедрить оболочку, как вы уже заявили.
Пример использования System.out:
public static void testInteger(List<Integer> list) {
list.add(3);
list.remove(0);
}
public static void testString(List<String> list) {
list.add("4");
list.remove(0);
}
public static void main(String...args) {
List list = new ArrayList<Object>(Arrays.asList("1", "2"));
testInteger(list);
System.out.println(list.toString()); // will print: [2, 3]
testString(list);
System.out.println(list.toString()); // will print: [3, 4]
}
Вы всегда используете одну и ту же ссылку, так что вам не нужно беспокоиться о несоответствиях, и она более производительна, чем всегда для преобразования объектов. Но что-то вроде этого сломало бы код:
public static void main(String...args) {
List list = new ArrayList<Object>(Arrays.asList("1", "2"));
testInteger(list);
System.out.println(list.toString()); // will print: [2, 3]
testString(list);
System.out.println(list.toString()); // will print: [3, 4]
accessData(list); //Will crash
}
public static void accessData(List<Integer> list) {
Integer i = list.get(0); //Will work just fine
i = list.get(1); //Will result in an Class Cast Exception even tho the Method might define it as List<Integer>
}
RawTypes позволяют вам передавать список каждому методу, который принимает "List" в качестве аргумента. Но вы теряете безопасность Types, это может или не может быть проблемой в вашем случае. Пока методы обращаются только к добавленным элементам, у вас не будет проблем.
Ответ 8
Попробуйте реализовать поток для этого. Приведенный ниже пример имитирует контекст, который вы представили, но всегда будет иметь загруженное ядро на 100%.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
public class Main {
static List<Integer> intListProperty;
static List<String> stringList;
public static void main(String... args) throws InterruptedException {
Main m = new Main();
m.execute();
}
private void updateAlways(Main main) {
class OneShotTask implements Runnable {
Main main;
OneShotTask(Main main) {
this.main = main;
}
public void run() {
while (main.intListProperty == main.getIntListProperty()) {}
main.intListProperty = getIntListProperty();
main.stringList = main.intListProperty.stream().map(s -> String.valueOf(s)).collect(Collectors.toList());
main.updateAlways(main);
}
}
Thread t = new Thread(new OneShotTask(main));
t.start();
}
public void execute() throws InterruptedException {
System.out.println("Starting monitoring");
stringList = new ArrayList<>();
intListProperty = new ArrayList<>();
intListProperty.add(1);
intListProperty.add(2);
intListProperty.add(3);
updateAlways(this);
while(true) {
Thread.sleep(1000);
System.out.println("\nintListProperty: " + intListProperty.toString()); // will print: [1, 2, 3, 4]
System.out.println("stringList: " + stringList.toString()); // will print: [2, 3]
}
}
// simulated
//thirdPartyBean.getIntListProperty();
private List<Integer> getIntListProperty() {
long timeInMilis = System.currentTimeMillis();
if(timeInMilis % 5000 == 0 && new Random().nextBoolean()) {
Object[] objects = intListProperty.toArray();
// change memory position
intListProperty = new ArrayList<>();
intListProperty = new ArrayList(Arrays.asList(objects));
intListProperty.add(new Random().nextInt());
}
return intListProperty;
}
}