Сопоставить элементы коллекции и сохранить ссылку на исходную коллекцию

Я ищу способ создать коллекцию, список, набор или карту, которая содержит преобразованные элементы оригинальной коллекции и отражает все изменения в этой коллекции.

Например, если у меня есть 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>, у вас есть два решения.

  1. Не создавайте начальный List<String>, используйте метод или оболочку, которая всегда будет возвращать преобразованные значения из List<Integer>, поэтому у вас никогда не будет статического List<String>.

  2. Создайте обертку вокруг 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;
    }

}