简体   繁体   中英

How can I return the same Collection type given as a parameter, but with a different element type?

I want to do this:

public <C extends Collection<?>> C<File> strings_to_files(C<String> strings)
{
    C<File> files = new C<File>();

    for(String string : strings)
    {
        files.add(new File(string)
    }

    return files;
}

I just want it to take any Collection of strings, and return that same Collection type with files. Is there any way to do this? Or maybe I just have the syntax wrong...

There's no good way to do that directly. The easiest thing, I think, would be to pass the target collection as a second argument:

public void string_to_files(Collection<String> strings, Collection<File> files) {
    for(String string : strings) {
        files.add(new File(string));
    }
}

The client code can then decide what type of collection it wants back. This doesn't do what you asked, but it's the cleanest way to avoid having to cast and suppress warnings.

Alternatively, just declare the method to return a Collection<File> (or a specific Collection implementation type) and instantiate a specific Collection type of your choice.

Let's pretend that your code compiles, and you pass in List . You'll have a compile error, since List is an interface, not a concrete class.

public static List<File> strings_to_files(List<String> strings)
{
    List<File> files = new List<File>(); // compile error, since this is an interface

    for(String string : strings)
    {
        files.add(new File(string));
    }

    return files;
}

You could do some magic with reflection:

public Collection<File> strings_to_files(Collection<String> strings)
{
    Collection<File> files = null;
    try {
        Constructor ctor = findDefaultConstructor(strings.getClass());
        files = (Collection<File>) ctor.newInstance();
    } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
            | SecurityException e) {
        throw new RuntimeException(e);
    }

    for(String string : strings)
    {
        files.add(new File(string));
    }

    return files;
}

private Constructor findDefaultConstructor(Class collectionClass) {
    for (Constructor ctor : collectionClass.getDeclaredConstructors()) {
        System.out.println(ctor);
        if (ctor.getParameterCount() == 0) {
            ctor.setAccessible(true);
            return ctor;
        }
    }
    return null;
}

This magic will work for classes, like ArrayList or HashSet . However, it won't work for lists created by Arrays.asList(...) . So, there's no good fix for this within this method. However, you can pass the responsibility to the caller of this method.

Since all the standard collections except Map implement the Collection interface, which means, they all comply with the Collection interface and can be treated a Collection , so you don't really need to write <C extends Collection<?>> . Your function must use one of the concrete classes which implements Collection to store the files because you will use new to initialize it, meaning it must be concrete. However, if you want to hide this detail, just use Collection<File> as the return type.

public Collection<File> strings_to_files(Collection<String> strings)
{
    Collection<File> files = new ArrayList<>();

    for(String string : strings)
    {
        files.add(new File(string));
    }

    return files;
}

In Java 8, the function body can be simplified as :

return strings.stream().map(File::new).collect(Collectors.toCollection(ArrayList::new));

Or:

return strings.stream().map(File::new).collect(Collectors.toList());

You can do the same as @ted-hopp suggested and enforce the invariant with a runtime check:

public void strings_to_files(Collection<String> strings, Collection<File> files) {
    if (!strings.getClass().equals(files.getClass())) {
        throw new IllegalArgumentException("strings and files must belong to the same collection type");
    }
    for(String string : strings) {
        files.add(new File(string));
    }
}

I think these are two seperate questions:
1. How to create a new collection of the same type you get as an argument.
2. How to express in the function declaration that the caller is garantueed to get the same type of collection as he passed in.


The easier question is 2.:
Not possible. Since generic types cannot have a generic type parameter themselves, this does not work. You can put it in the documentation, but the function declaration would have to look like this:

/**
 * @return The same type of collection as was passed as argument.
 */
public Collection<File> strings_to_files(Collection<String> strings) {}

The caller will have to cast the result back to the type that was passed:

ArrayList<File> files = (ArrayList<File>)strings_to_files(new ArrayList<String>());

Question 1.:
One of Tamas Rev 's answers provided a possibility.

However, if you do not want to use reflection, you can create you own collections which provide the methods to recreate collections of the same type or a copy on every collection object itself. This is actually pretty simple:

public interface Collection<E> extends java.util.Collection<E> {

    /**
     * @return Shallow copy with the same collection type as this objects real collection type.
     */
    public Collection<E> copy();

    /**
     * @return New empty instance of the same type as this objects real collections type.
     */
    public <F> Collection<F> newInstance();
}

public interface List<E> extends java.util.List<E>, Collection<E> {
    @Override
    public List<E> copy();
    @Override
    public <F> List<F> newInstance();
}

public class LinkedList<E> extends java.util.LinkedList<E> implements List<E> {
    @Override
    public LinkedList<E> copy() {
        return new LinkedList<E>(this);
    }
    @Override
    public <F> LinkedList<F> newInstance() {
        return new LinkedList<F>();
    }
}

You need to do this with every Collection class you need. Now you can use those collections in your function:

public Collection<File> strings_to_files(Collection<String> strings)
{
    Collection<File> files = strings.<File>newInstance();

    for(String string : strings)
    {
        files.add(new File(string));
    }

    return files;
}

You can still pass your collections to other libraries. Another advantage is that you can put other useful things into your collections like an actual class object of the generic parameter, so you can always enforce and retrieve the generic parameter type.
The only problem is, default collections need to be converted to your collections if you want to use them.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM