Copy a list to another list without losing the runtime type of original list

Viewed 56

I want to copy a List<T> to another list without losing the runtime type. All of the techniques that come to my mind is not achieving this. (Also, I don't want to simply return the reference of the input list as I don't want edits in input list to be reflected into the copied list)

What I tried (see all the copy* methods) is listed below. As expected, none of them is giving (x.getClass() == y.getClass()) to be true

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;

public class TestX {
    public static void main(String[] args) {
        List<Integer> x = new LinkedList<>();
        x.add(1);
        x.add(2);
        x.add(3);

        List<Integer> y = copy2(x);
        System.out.println("is runtime type equal? " + (x.getClass() == y.getClass()));
        // I want (x.getClass() == y.getClass()) to be true
        
        y.add(4);
        System.out.println(x); // should be [1, 2, 3]
        System.out.println(y); // should be [1, 2, 3, 4]
    }

    private static <T> List<T> copy0(List<T> input) {
        return new ArrayList<>(input);
    }

    @SuppressWarnings("unchecked")
    private static <T> List<T> copy1(List<T> input) {
        Object[] x = input.toArray();
        return (List<T>) List.of(x);
    }

    private static <T> List<T> copy2(List<T> input) {
        return input.stream().collect(Collectors.toList());
    }

    private static <T> List<T> copy3(List<T> input) {
        // Since Java 10
        return List.copyOf(input);
    }

}
3 Answers

The best approach is to make the caller send in a function that will create a new list of the same type. The alternative would be to use reflection, which would begin a stream of dangerous assumptions.

The following two are examples of copy implementations adapted from your code:

private static <T> List<T> copy0(List<T> input, Supplier<List<T>> newListCreator) {
    List<T> newList = newListCreator.get();
    newList.addAll(input);

    return newList;
}

private static <T> List<T> copy2(List<T> input, Supplier<List<T>> newListCreator) {
    return input.stream().collect(Collectors.toCollection(newListCreator));
}

And you can call either in this way:

List<Integer> y = copy2(x, LinkedList::new);

A LinkedList is not an ArrayList. If you want both types to be equal, then you might use reflection to invoke the default constructor of the actual class called in. Something like,

private static <T> List<T> copy0(List<T> input) {
    Class<?> cls = input.getClass();
    try {
        List<T> al = (List<T>) cls.getConstructor(null).newInstance(null);
        Iterator<T> iter = input.iterator();
        while (iter.hasNext()) {
            al.add(iter.next());
        }
        return al;
    } catch (Exception e) {
    }
    return null;
}

Which outputs

is runtime type equal? true

when I test it with your code. Note this makes dangerous assumptions about constructors and is not the approach I would take in a real project.

Try like this, the code shows below ignore try... catch...

private static <T> List<T> copy2(List<T> input) { 
    List<T> instance = input.getClass().newInstance();
    return input.stream().collect(Collectors.toCollection(() -> instance));
}
Related