Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Map collection elements and keep reference to source collection

I'm looking for a way to create a collection, list, set, or map which contains the transformed elements of an original collection and reflects every modification in that collection.

For example if I have a List<Integer> from a third party API and another API is expecting a List<String>. I know I can transform the list like this:

List<Integer> intList = thirdPartyBean.getIntListProperty();
List<String> stringList = intList.stream().map(Integer::toString)
    .collect(Collectors.toList());
secondBean.setStringListProperty(stringList);

The problem is, if anything is changed in one of the lists the other one will still reflect the previous state. Let's assume that intList contains [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]

So I'm searching for something like List.sublist(from, to) where the result is "backed" by the original list.

I'm thinking of implementing my own list wrapper which is used like this:

List<String> stringList = new MappedList<>(intList, Integer::toString, Integer::valueOf);

The second lambda is for inverting the conversion, to support calls like stringList.add(String).

But before I implement it myself I would like to know if I try to reinvent the wheel - maybe there is already a common solution for this problem?

like image 288
Tobias Liefke Avatar asked Feb 28 '19 14:02

Tobias Liefke


People also ask

How can we access elements of a Collection?

The elements in a collection can be accessed by index (starts at 0). The length Property returns the number of elements in the collection.

What are the methods in Collection interface?

The Collection interface contains methods that perform basic operations, such as int size() , boolean isEmpty() , boolean contains(Object element) , boolean add(E element) , boolean remove(Object element) , and Iterator<E> iterator() .


1 Answers

I would wrap the list in another List with transformers attached.

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

Note that the fromTransformer needs null checking for the input value, if source may contain null.

Now you are not transforming the original list into another one and losing contact with the original, you are adding a transformation to the original list.

like image 104
OldCurmudgeon Avatar answered Oct 24 '22 01:10

OldCurmudgeon