映射集合元素并保持对源集合的引用

时间:2019-02-28 14:33:18

标签: java collections

我正在寻找一种创建集合,列表,集合或地图的方法,其中包含原始集合的转换元素并反映该集合中的所有修改。

例如,如果我有来自第三方API的List<Integer>,而另一个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);

第二个lambda用于反转转换,以支持诸如stringList.add(String)之类的调用。

但是在我自己实施之前,我想知道是否尝试重新发明轮子-也许已经有解决此问题的通用解决方案?

8 个答案:

答案 0 :(得分:8)

我会将列表包装在另外一个装有变压器的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检查输入值。

现在您没有将原始列表转换为另一个列表并失去与原始列表的联系,而是将转换添加到原始列表中。

答案 1 :(得分:8)

我不知道您使用的是哪个版本的JDK,但如果可以使用JavaFX库,则可以使用ObservableList。您不需要修改现有列表,因为ObservableListjava.util.List的包装器。 Look at extractor in FXCollection for complex Objects。这个article有一个例子。

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

答案 2 :(得分:3)

这正是Observer Pattern解决的问题。

您可以围绕List<String>List<Integer>创建两个包装器,让第一个包装器观察另一个包装器的状态。

答案 3 :(得分:1)

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

答案 4 :(得分:0)

您需要在第一个列表的顶部创建一个包装器,并考虑您的示例List<Integer>。 现在,如果您希望List<String>反映对List<Integer>完成的所有运行时更改,则有两种解决方案。

  1. 不要创建初始的List<String>,请使用始终会返回List<Integer>中转换后的值的方法或包装器,因此永远不会有静态的List<String>

  2. List<Integer>周围创建一个包装,该包装应引用List<String>,并覆盖add(),addAll(),remove()和removeAll()方法。在覆盖的方法中,更改List<String>的状态。

答案 5 :(得分:0)

另一种选择是使用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]

答案 6 :(得分:0)

由于您的示例,我假设您无权访问该方法,只能修改列表,而不能访问数据本身。 您可以使用原始类型。

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允许您将列表传递给以“列表”作为参数的每个方法。但是您会丢失类型安全性,这对您而言可能是问题,也可能不是问题。 只要方法仅访问它们添加的元素,您就不会有问题。

答案 7 :(得分:0)

为此尝试实现一个线程。下面的示例模拟了您呈现的上下文,但始终会有一些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;
    }

}