6

I have a couple of Java classes that extend various implementations of the generic List interface. They simply log anything that is added to the List.

The LoggingArrayList is shown below. As the name suggests, it extends ArrayList. The LoggingLinkedList class is identical except that it extends LinkedList.

My main objective is to avoid having to duplicate all of the common code just so that I can use a different base class. I'm trying to adhere to the DRY principle (Don't Repeat Yourself) as much as possible.

First of all, please don't suggest a better way of logging. That's not at all my real application. It's just an easy way to demo the problem I'm having.

I have two closely related questions. The first one is the question in the title. How can I reference a "super" method in a Java class that implements an interface but does not extend another class?

The LoggingArrayList class as shown below works fine but when I change the class declaration from ...extends ArrayList to ...implements List then the three references to super.method() are no longer callable, hence my first question.

A good answer to my second question will almost make the first question moot. The second question is this: Is there a way to declare an abstract base class or perhaps an interface that extends List with default implementations of the various add() methods so that I can simply extend that abstract base class or implement that interface and only specify what kind of List will be the basis for the concrete class?

For example, I'd like to do something like this:

interface LoggingList<T extends Object, L extends List<T>> extends L
{
    // overloaded methods go here as shown below 
    //  with overloaded methods declared as default for the interface
}

...then I could simply implement LoggingList one time for each concrete implementation of List without duplicating all of the common code. The concrete classes might then look something like this, with no additional code needed inside their curly braces:

public class LoggingArrayList<T extends Object> implements LoggingList<T, ArrayList<T>> {}
public class LoggingLinkedList<T extends Object> implements LoggingList<T, LinkedList<T>> {}

The problem is that the interface definition as I have proposed it is invalid (won't compile) and also, the references to super.method(s) in the code shown below are unavailable unless I make LoggingList an abstract subclass instead of an interface and then I end up right back where I am now.

Thanks in advance for any ideas on how to accomplish my DRY goal.

Here's my whole LoggingArrayList class.

public abstract class LoggingArrayList<T extends Object>
    extends ArrayList<T>
{
    protected void log(T e)
    {
        System.out.println(e == null ? "null" : e.toString());
    }

    @Override
    public boolean add(T e) {
        log(e);
        // How do I reference a super.method()
        // in a class that implements an interface
        // but does not extend another class?
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends T> clctn) {
        boolean anyChanges = false;
        for(T e : clctn)
        {
            // ensure that we call our overridden version of add()
            //  so it gets logged.
            anyChanges = anyChanges || add(e);
        }
        return anyChanges;
    }

    @Override
    public boolean addAll(int i, Collection<? extends T> clctn) {
        for(T e : clctn)
        {
            // ensure that we call our overridden version of add() 
            //  so it gets logged.
            add(i, e);
            i++; // keep the inserted elements in their original order
        }
        return !clctn.isEmpty();
    }

    @Override
    public T set(int i, T e) {
        log(e);
        // How do I reference a super.method()
        // in a class that implements an interface
        // but does not extend another class?
        return super.set(i, e);
    }

    @Override
    public void add(int i, T e) {
        log(e);
        // How do I reference a super.method()
        // in a class that implements an interface
        // but does not extend another class?
        super.add(i, e);
    }
}
Jan
  • 13,738
  • 3
  • 30
  • 55
David
  • 251
  • 2
  • 11
  • Extending collections is always a bad practice. For some more complex structures there are also pitfalls that can be avoided by delegating instead of extending. Take a look at [this article on extending classes](http://www.javaworld.com/article/2073649/core-java/why-extends-is-evil.html). – Dariusz Dec 04 '15 at 09:31
  • The article does a good job of explaining your point. In fact, I already considered the potential for that very problem in the case of the two addAll methods. You'll note that I'm already ensuring that my own implementation of add() is called rather than assuming the base class will always call it. I guess the delegate approach at least has the advantage that it doesn't have to replace any of the base class implementation or make any assumptions about it. I can just add my logging without caring about what the actual List is doing internally. – David Dec 04 '15 at 10:36

3 Answers3

3

Once way to do this would be: Delegate.

Instead of having multiple implementations for different kinds of Lists, you could just have one

public class LoggingList<T extends Object> implements List<T>

  protected List<T> superList;

  public LoggingList(List<T> anotherList) {
     superList= anotherList;
  }

  protected void log(T e) {
      System.out.println(e == null ? "null" : e.toString());
  }

  @Override
  public boolean add(T e) {
      log(e);
      return superList.add(e);
  }

And then instead of calling super. you'd call superList.

You would still have to adapt your constructors away from new LoggingLinkedList() to new LoggingList(new LinkedList()); - but that should be no biggie...

Jan
  • 13,738
  • 3
  • 30
  • 55
  • This also called the decorator or the wrapper design pattern: https://en.wikipedia.org/wiki/Decorator_pattern – Puce Dec 04 '15 at 09:32
  • Eclipse lets you generate delegate methods automatically, *alt-s, m*. You just select all methods of your `superList` in the window that appears and it's done. – Dariusz Dec 04 '15 at 09:33
  • Thanks for the quick reply. I considered that approach, but then I have to overload all of the List methods just to call the corresponding superList methods. I'd prefer to use inheritance and not mess with the unaffected methods if possible. – David Dec 04 '15 at 09:36
  • It's not that overloading all of the List methods is difficult. As you point out, any decent IDE will do most of the work for you. It's just that this approach ends up with a lot of redundant code that obscures the real point of extending the class in the first place. The resulting code is harder to read and I still end up with just a different form of having to repeat myself. – David Dec 04 '15 at 09:45
  • But you'd be only repeating yourself *once* - for all kinds of Lists. Instead of *several times* for each different list you need to wrap. There's a reason I tagged your post "design-patterns" as well. And as far as I understood your question - the point was to add logging to some if not all methods of a list - and make that widely applicable to many different List implementations? – Jan Dec 04 '15 at 09:52
  • I agree that repeating myself once is an improvement over doing it for every class, although there aren't a lot of List implementations that I use anyway, and a well placed comment or two could make it clear that the intent is to only log the "add" methods. It's a common enough design pattern that it wouldn't be that hard to follow or to implement. I was just hoping there was a way to use a type parameter (L in my example) to define what the class extends (ArrayList in my example). I may end up going with this approach if my original idea isn't feasible. – David Dec 04 '15 at 10:12
  • BTW, my real application would be doing some validation of the elements being added and only logging anomalies and also aggregating some statistics about the contents as elements are added so the List doesn't need to be repeatedly traversed to get that info later. – David Dec 04 '15 at 10:12
  • Sorry - Java inheritance sometimes sucks. But AOP would do you no good as well as it's not your classes, so annotations on methods will not work as well. – Jan Dec 04 '15 at 10:14
1

You can use JDK proxy for this purpuse. Just google "how to use jdk proxy class".

Here described a your use case.

Sergey Morozov
  • 4,528
  • 3
  • 25
  • 39
  • Thanks Surgey. This is very interesting, although it's nothing like the answers I was expecting. I haven't seen this class before. I'll definitely look into it and see if it's helpful for my application. Still, I'd like to see if there's a way to do anything more conventional like my original approach, just with the correct syntax if the concept is feasible. – David Dec 04 '15 at 10:20
  • 1
    @Jan - Dynamic proxies **proxy** objects to **implement** interfaces. It can easily wrap a `List` **and** implement a `List`. – OldCurmudgeon Dec 04 '15 at 10:41
1

Just for future reference, here's a complete working solution that I put together using the proxy approach as Sergey Morozov suggested. The core of it is less than 50 lines of code. Over half of the code, included at the end, is just a unit test for the actual functionality.

I'm not sure yet if I'll ultimately use this approach for my purpose, but it was a very useful exercise. Thanks for suggesting it Sergey.

public class LoggingListProxyFactory
{
    protected static void log(String methodName, Object element)
    {
        System.out.println(methodName
                + ": ["
                + (element == null ? "null" : element.toString())
                + "]"
        );
    }

    public static <T extends Object> List<T> getProxy(final List<T> list) {
        return (List<T>) Proxy.newProxyInstance(
                list.getClass().getClassLoader(),
                new Class[]{ List.class },
                new LoggingInvocationHandler(list)
        );
    }

    private static class LoggingInvocationHandler<T>
            implements InvocationHandler
    {
        final List<T> underlyingList;

        public LoggingInvocationHandler(List<T> list) {
            this.underlyingList = list;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable
        {
            // These are the List interface methods that we want to log.
            // In every case, the new elements happen to be the last parameter.
            //
            // boolean add(Object e)
            // void add(int index, Object element)
            // boolean addAll(Collection c)
            // boolean addAll(int index, Collection c)
            // Object set(int index, Object element)
            String methodName = method.getName();
            if( ( "add".equals(methodName)
                | "addAll".equals(methodName)
                | "set".equals(methodName)
                )
                // a few additional probably unnecessary checks
                && args != null
                && args.length == method.getParameterCount()
                && method.getParameterCount() > 0
                )
            {
                log(methodName, args[args.length-1]);
            }
            return method.invoke(underlyingList, args);
        }
    }

    public void testGetProxy() {
        List<String>[] testLists = new List[] {
            new ArrayList<>(),
            new LinkedList<>()
        };
        for(List<String> aList : testLists)
        {
            List<String> proxy = LoggingListProxyFactory.getProxy(aList);

//          aList.add(42); // type is enforced at compile time
            aList.add(aList.getClass().getSimpleName());
            aList.add("unlogged");
            aList.add(null);

//          proxy.add(42); // type is enforced at compile time
            proxy.add(proxy.getClass().getSimpleName());
            // exercise each the methods that are being logged
            proxy.add("foo");
            proxy.add(0, "bar");
            proxy.add(null);
            proxy.addAll(aList);
            proxy.addAll(7, aList);
            proxy.set(5, "five");

            System.out.println();
            System.out.println(aList.getClass().getSimpleName()
                    + ".size() = " + aList.size());
            aList.stream().forEach(System.out::println);

            System.out.println();
            System.out.println("proxy.size() = " + proxy.size());
            proxy.stream().forEach(System.out::println);
        }
    }
}
David
  • 251
  • 2
  • 11