3
\$\begingroup\$

I am writing some code that takes and returns interfaces following the patterns in Effective Java Third Edition. I understand the principle - we want to take in interfaces because in many cases we don't really care about the discrete implementation but rather just the features of this interface. We return interfaces for the same reason. It gives the caller the ability to use the preferred implementation class. Take this extreme contrived example:

import java.util.*;

public class Main
{
    public static List<Integer> op(List<Integer> is) {
        ArrayList<Integer> results = new ArrayList<>();
        
        for (Integer i : is) {
            results.add(i + 1);
        }
        
        return results;
    }
    
    public static void main(String[] args) {
        ArrayList<Integer> nums = new ArrayList(Arrays.asList(1,2,3,4));

        // This line
        ArrayList<Integer> results = (ArrayList<Integer>) op(nums);
        
        System.out.println(results);
    }
}

So if the caller of op wanted to use a LinkedList they could through simple casting. But to me this feels clunky. In my codebase it is littered with various casts that seem to hinder readability.

Is there a smarter way to do this? Or is this explicit casting the only way to do this properly?

\$\endgroup\$
3
  • \$\begingroup\$ Why would the caller want a linked list? Why would the caller need to know what type of list is used? Why not just declare results as List<Integer> ? \$\endgroup\$ Aug 9, 2020 at 18:13
  • 1
    \$\begingroup\$ @TedBrownlow One idea could be that the caller may want a different access type. For example, appending to a LinkedList is O(1) vs ArrayList O(N) and they may want to append more after running op on the list. A similar argument could be made for removal. \$\endgroup\$
    – CL40
    Aug 9, 2020 at 18:29
  • \$\begingroup\$ try with public static <T, K extends List<T>> K op(List<T> is) and return (K)results I can tell that such construction worked fine in a different context for me. \$\endgroup\$ Aug 9, 2020 at 20:26

3 Answers 3

2
\$\begingroup\$

Let me clarify something fist

It gives the caller the ability to use the preferred implementation class.

Even if we return interface from method, that doesn't mean that that caller can choose implementation.'

So if the caller of op wanted to use a LinkedList they could through simple casting.

If you mean something like (LinkedList<Integer>) op(nums), then this wont work and will throw java.lang.ClassCastException: java.util.ArrayList cannot be cast to java.util.LinkedList.

When method returns interface, in general, you shouldn't assume specific implementation, and thus should avoid such castings. Having interface here, for example, will give more freedom to the author of this method for future changes: imagine someone will write SuperPerfectList which works better than any other list and author decides to use it. In this case even if method changes, we won't need to refactor every place in our code where we call this method, because signature of this method hasn't changed.

If we talk about the situation where we want another implementation of this interface, then, in case of list, we can do new LinkedList(result) and get what we want.

\$\endgroup\$
1
  • \$\begingroup\$ Or if we feel that copying the results is too inefficient, you could change the method signature to take an empty results list as a parameter, which obviously could be any List implementation you wish. \$\endgroup\$
    – tgdavies
    Aug 11, 2020 at 7:09
0
\$\begingroup\$

Another way could be to pass a factory method. It is pretty close to the comment of tgdavies, passing an empty list. But with a factory method, multiple new instances can be requested if necessary. For example:

public static <T extends List<Integer>> T op(final List<Integer> list, final Supplier<T> factory) {
    final T results = factory.get();
    ...
    return results;
}

Calling it would look like:

final ArrayList<Integer> list = op(asList(1, 2, 3), ArrayList::new);
\$\endgroup\$
0
\$\begingroup\$

I feel like you are misunderstanding a few things about working with interfaces. In your op() method you are creating an ArrayList. The type of the List is then "locked in" and cannot be changed. Even if you return only the List interface, the underlying object will still be an ArrayList. As Flame's answer already mentioned, you cannot cast this object to LinkedList because they are not compatible.

So there are two options:

  1. Be specific about which kind of List implementation you are using. This means changing the return type of op() to ArrayList. The caller can then use all the functionality specified by ArrayList.

  2. Keep your declaration as it is. The caller then has to work with only the List interface and cannot work with any extended functionality that the ArrayList provides. That is because it cannot make any guarantees which implementation is used.

I'd choose option 2 since very rarely does one need functionality which isn't already provided by the List interface. And again, in general, you cannot change the implementation from ArrayList to LinkedList by casting. You'd need to create a new List with the desired implementation and copy all the contents into it. An easy way to do this is: new LinkedList<>(someArrayList);

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.