12

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?

Tobias Liefke
  • 8,637
  • 2
  • 41
  • 58
  • What about `clone`? – Evgeni Enchev Feb 28 '19 at 14:37
  • I don't totally get you, can you please give us an example of what is the inputs and what you expect to get? – Youcef LAIDANI Feb 28 '19 at 14:38
  • @Evgeni: Yeah, what about clone - I don't see how this will apply to any of my requirements. – Tobias Liefke Feb 28 '19 at 14:45
  • @YCF_L I extended my example with an expected result. – Tobias Liefke Feb 28 '19 at 14:45
  • Are you set on maintaining two data structures versus some clever implementation of the List that actually performs the int to string conversion when data is accessed? I'm sure there are pros and cons to both. – Evan Feb 28 '19 at 14:46
  • @Evan Let's say that `stringList` is not in my "possession". – Tobias Liefke Feb 28 '19 at 14:48
  • If you din't mind what is your purpose behind this action? – Youcef LAIDANI Feb 28 '19 at 15:01
  • The purpose is, that I don't like to reinvent the wheel. I adopted my question to reflect, that I'm not able to change the source list implementation or the interface of the expected result. I'm only able to change the result itself, and therefore I would go with the wrapper solution - if there is no other. – Tobias Liefke Feb 28 '19 at 15:15
  • 1
    Your code example suggests that `thirdPartyBean.getIntListProperty()` returns a reference to its contained list, which still allows modifications and `secondBean.setStringListProperty(stringList)` will make the second bean to use a reference to the specified list and both beans are then magically connected, doing the right thing despite the `List` interface has no change notification mechanism. I don’t know of many real life cases where this would work. Most beans return unmodifiable list views or defensive copies to stop the client code from even trying. – Holger Mar 05 '19 at 16:27
  • As I said - think of `List.subList` (or `Collections.checkedList`) which do similar things. And think of the bean as a plain "data object" - no need to encapsulate the list with some additional "add" and "remove" functions. It just returns the list as one of its attributes and lets the client change the content. I wouldn't say that this is an unusual use case. – Tobias Liefke Mar 05 '19 at 18:00
  • If a custom wrapper is not desired use a Stream instead and materialize values on demand. There is no other way you can achieve 2-way binding on a non observable List – firephil Mar 06 '19 at 21:00
  • I don't really see, what you mean with "materialize values on demand" - I can't change the original list with a stream, so how should that help me? Maybe you should add a small example as your own answer? – Tobias Liefke Mar 07 '19 at 20:52
  • Stream=thirdPartyBean.getIntListProperty().stream().map(String::valueOf) this will always reflect the changes of the original List because it is lazy when you collect() to a List i.e materialize the stream, it will have up to date values with the original. To push back changes is not possible with this approach use a wrapper. BTW it is bad practise and thread unsafe the thing you are trying to achieve. Languages like Rust are disallowing explicitly modifications of a reference from multiple points. – firephil Mar 08 '19 at 19:05
  • Your bounty, along with the end of your question and many of your comments, seems to be asking for a ready made solution. That sounds suspiciously like asking for an offsite resource (e.g. a library) which is off-topic for Stack Overflow. As you appear to know how to do what you want—that is, create a wrapper around the original `List` that does the conversions for you—what are you actually asking for? – Slaw Mar 09 '19 at 10:57
  • @firephil: You know that you can use a stream only once? One of the reasons why you should avoid to store a stream in a variable. And would you say that using `List.subList` is bad practice? Or using `ArrayList` - which is not thread safe too? Your last point sounds like even using Java is bad practice? – Tobias Liefke Mar 09 '19 at 23:13
  • @Slaw: You are right, I shouldn't ask for anything other than the wrapper - I should simply ask for the solution and look if the wrapper is the last thing standing at the top. But I didn't want another third party library - I just was hopping that there is something in one of the new Java APIs that I had missed. At the end I think I will give OldCurmudgeon the bounty, even if she did the thing that I said I was not looking for - just because from my point of view this is by now the best solution. – Tobias Liefke Mar 09 '19 at 23:24
  • For what it's worth, I tried finding a third party library (there's nothing in the Java SE library, as far as I know). There doesn't seem to be anything in _Guava_, but _Apache Commons Collections_ has something named [`TransformedList`](https://commons.apache.org/proper/commons-collections/javadocs/api-4.3/org/apache/commons/collections4/list/TransformedList.html). However, I don't understand its benefit since the generic signatures seem to force the two types to be related. – Slaw Mar 10 '19 at 01:04

8 Answers8

8

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.

Tobias Liefke
  • 8,637
  • 2
  • 41
  • 58
OldCurmudgeon
  • 64,482
  • 16
  • 119
  • 213
8

I don't know what version of the JDK you are using, but if you are okay with using the JavaFX library you can use ObservableList. You do not need to modify an existing list as ObservableList is a wrapper for java.util.List. Look at extractor in FXCollection for complex Objects. This article has an example of it.

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);
    }
}
Pika Supports Ukraine
  • 3,612
  • 10
  • 26
  • 42
Shawn
  • 403
  • 8
  • 38
  • This is only possible if i'm able to change all usages of the `stringList` with a reference to the `ObservableList`. I adopted my question to reflect that I'm not able to do this, because the original list is part of a third party API. – Tobias Liefke Feb 28 '19 at 15:31
  • are you able to wrap it in an ObservableList? if you are you can use a callback method with custom Extractor which allows for any sort of data to be observed. – Shawn Feb 28 '19 at 15:44
  • I'm not able to wrap the original `intList`. Try to think of `intList = some3rdPartyBean.getIntListProperty()`. By the way you are the third person with the "observable" answer. – Tobias Liefke Feb 28 '19 at 16:08
  • As your answer is the one with the highest rate, I thought I give it a try - even if it is not the one I was looking for. I used my example values: `intList.addAll(1, 2, 3)` (and initialized the stringList with `intList.stream().map(String::valueOf).forEach(stringList::add)`). The result for `intList.add(4); stringList.remove(0);` is still `[1, 2, 3, 4]` and `[2, 3]` - you must be missing something (I guess that the first `c.getRemoved()` is wrong). And please think of situations like `intList.add(2, 5)` as well. – Tobias Liefke Mar 07 '19 at 21:02
3

This is exactly the kind of problems that the Observer Pattern solves.

You can create two wrappers, around List<String> and List<Integer> and let first wrapper observe the state of the other one.

Ameen
  • 137
  • 3
  • This is only possible if i'm able to change all usages of the `List` with my own "observable" implementation. I adopted my question to reflect that I'm not able to do this, because the original list is part of a third party API. – Tobias Liefke Feb 28 '19 at 15:22
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;
        }
    }
Oleg Cherednik
  • 17,377
  • 4
  • 21
  • 35
  • As I said: I neither can change the implementation of `intList` - nor can I change all of its references to my own implementation. I only can change the implementation of `stringList`. – Tobias Liefke Feb 28 '19 at 15:59
  • You do not have to. I've just created a wrapper and you have to work with it using this decorator. – Oleg Cherednik Feb 28 '19 at 16:07
  • 1
    Just because _I_ work with the decorator, doesn't mean that the third party API is working with the decorator. By the way you are the fourth person with the "observable" answer. It's somehow exhausting to discuss the same arguments in four different threads. (Second btw: https://meta.stackoverflow.com/questions/300837/what-comment-should-i-add-to-code-only-answers) – Tobias Liefke Feb 28 '19 at 16:12
0

You need to create a wrapper on top of the first list, considering your example, the List<Integer>. Now if you want List<String> to reflect all the runtime changes done to List<Integer>, you have two solutions.

  1. Don't create initial List<String>, use a method or a wrapper which will always return the transformed values from List<Integer>, so you'll never have a static List<String>.

  2. Create a wrapper around List<Integer>, which should have a reference to List<String>, and override the add(), addAll(), remove() and removeAll() methods. In the overridden methods, change the state of your List<String>.

Abhijay
  • 278
  • 1
  • 8
  • I don't see how point 1 applies to my question - see my comment in the answer: "Let's say that `stringList` is not in my possession". And point 2 is exactly what I propose with `new MappedList<>(...)` in my question. What I mean is, that you don't answer my bottomline question: "Maybe there is already a common solution for this problem?" – Tobias Liefke Feb 28 '19 at 14:51
  • If you want to always maintain 2 different lists, the I think the 2nd point, your proposal with new MappedList<>(...) is the only way. – Abhijay Feb 28 '19 at 14:56
  • That was not the question. The question is, if maybe there is already such a `MappedList<>` implementation that I don't know. – Tobias Liefke Feb 28 '19 at 14:59
0

Another option would be to use the JavaFX ObservableList class which can wrap an existing list with an observable layer on which you can define the operations you want to propagate.

Here is an example which propagates from the string list to the integer list:

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

If you execute this code you will see this output on the console:

[1, 2, 2]
[2, 2]
gil.fernandes
  • 12,978
  • 5
  • 63
  • 76
  • This is only possible if i'm able to change all usages of the `strList` with a reference to the `strings`. I adopted my question to reflect that I'm not able to do this, because the original list is part of a third party API. – Tobias Liefke Feb 28 '19 at 15:34
  • @TobiasLiefke Yes, you are right about that. But since `ObservableList` is an implementation of `List`, cannot you simply point `strList` to `strings` with `strList = strings;`? – gil.fernandes Feb 28 '19 at 15:42
  • But just because I change _my_ reference wouldn't change all the other references in the third party code. `intList = new ArrayList<>(Arrays.asList(1, 2, 3));` was just an example, try to think more of `intList = some3rdPartyBean.getIntListProperty()` – Tobias Liefke Feb 28 '19 at 16:02
0

Because of your example I am assuming that the method, you have no access to, only modifies the list and does not access the data itself. You could use Raw Types.

List list = new ArrayList<Object>();

If you want to access the data you have to convert everything to the desired type.

list.stream().map(String::valueOf).<do_something>.collect(toList())

Not the cleanest solution but might work for you. I think the cleanest solution would be to implement a wrapper as you already stated.

Example using the 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] 
}

You always use the same reference, that way you dont need to worry about inconsistencies and its more performant then to always transform the objects. But something like this would break the code:

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 allow you to pass the list to every Method that take a 'List' as argument. But you lose Typesafety, that may or may not be a problem in your case. As long as the methods only access the elements they added you will have no problem.

Chris
  • 109
  • 2
  • 9
  • 1
    .stream().map(String::valueOf).collect(toList()) you have to materialize the stream to a List after the mapping... – firephil Mar 06 '19 at 20:30
  • @Chris, I don't see how raw types change the way I can access the list. Could you add an example, how my code with the `System.out.println` would look like? – Tobias Liefke Mar 07 '19 at 20:14
  • added an example that relates to your described usecase – Chris Mar 08 '19 at 08:45
  • By declaring `List` you define a contract that this list will _only_ contain objects of type String. If you cast this list to a raw type and add elements of other types, you will violate that contract and have ClassCastExceptions as you already found out. _As long as the methods only access the elements they added you will have no problem_ - what is the point in having a shared list, when I only may access my elements of the list? I think you already see how useless this would be? – Tobias Liefke Mar 10 '19 at 10:31
  • Then your only solution will be a wrapper... You have to convert the values somewhere. Again if you only access data the way described in your example my solution would work. Its like Schröddingers Kitten, if you access data the cardhouse collapses, but as long as you dont you are fine. – Chris Mar 11 '19 at 08:29
0

Try to implement a Thread for this. The example below simulates the context you have presented, but will always have some 100% busy core.

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

}

  • Hi Oliver, welcome to stackoverflow. I really would like to give you the bounty but I have to admit that I have problems to understand the intention of many parts of your code. Thus I just gave it a go and executed your code. The result is: `intListProperty: [1, 2, 3], stringList: []`. I guess that you are missing some major parts of your code. Nevertheless I don't think that an "update thread" will help me here, because of concurrency problems I would have to deal with. – Tobias Liefke Mar 07 '19 at 20:35