1. Overview

In this tutorial, we'll look at how we can apply the decorator pattern using Lombok's @Delegate annotation.

2. Why Use @Delegate?

Let's assume that we have a long interface and we want to create a decorator for it. Although we want to override just a few methods, we need to provide an implementation for all of them - even if it is just for delegation.

If we have a base forwarding decorator, it can ease the task. For example, Guava library provides some forwarding decorators for interfaces like List and Map.

If we don't have a base forwarding decorator, we have two options. Obviously, we can create such a base decorator from scratch. Or we can use Lombok's @Delegate annotation since it automatically generates the forwarding methods for us.

3. How To Use @Delegate

The @Delegate annotation creates delegation methods for the variable type it annotates. Moreover, we can exclude some methods from this automatic process.

Now we'll work with the List interface. In the end, we want to create a decorator that counts removal operations.

public class RemovalCountingList<E> implements List<E> {

    @Delegate(excludes = ExcludedListMethods.class)
    private final List<E> delegate;

    private final AtomicInteger removalCount = new AtomicInteger();

    public RemovalCountingList(List<E> delegate) {
        this.delegate = delegate;
    }

    @Override
    public E remove(int index) {
        System.out.println("Removal count: " + removalCount.incrementAndGet());
        return delegate.remove(index);
    }

    @Override
    public boolean remove(Object o) {
        boolean isRemoved = delegate.remove(o);
        if (isRemoved) {
            System.out.println("Removal count: " + removalCount.incrementAndGet());
        }

        return isRemoved;
    }

    /**
     * Excluded methods that Lombok will not implement, we will implement/override these methods.
     */
    private abstract class ExcludedListMethods {

        public abstract E remove(int index);

        public abstract boolean remove(Object o);
    }
}

The RemovalCountingList class implements List and has an instance field - delegate - with the same type. Also, note the usage of @Delegate. With this annotation, Lombok generates all List methods and puts a forwarding call to the wrapped instance, delegate. Moreover, the excludes attribute tells Lombok that it shouldn't generate the methods present in the ExcludedListMethods class. Then we're implementing those methods manually.

@Delegate(excludes = ExcludedListMethods.class)
private final List<E> delegate;

To recap, the @Delegate annotation tells Lombok to generate all methods of List except the ones in ExcludedListMethods.

As the next step, we'll look at the actual class that Lombok generates:

public class RemovalCountingList<E> implements List<E> {

    private final List<E> delegate;

    private final AtomicInteger removalCount = new AtomicInteger();

    public RemovalCountingList(List<E> delegate) {
        this.delegate = delegate;
    }

    @Override
    public E remove(int index) {
        System.out.println("Removal count: " + removalCount.incrementAndGet());
        return delegate.remove(index);
    }

    @Override
    public boolean remove(Object o) {
        boolean isRemoved = delegate.remove(o);
        if (isRemoved) {
            System.out.println("Removal count: " + removalCount.incrementAndGet());
        }

        return isRemoved;
    }

    public boolean addAll(int index, Collection<? extends E> c) {
        return this.delegate.addAll(index, c);
    }

    public boolean removeAll(Collection<?> c) {
        return this.delegate.removeAll(c);
    }

    public boolean containsAll(Collection<?> c) {
        return this.delegate.containsAll(c);
    }

    public List<E> subList(int fromIndex, int toIndex) {
        return this.delegate.subList(fromIndex, toIndex);
    }

    public Iterator<E> iterator() {
        return this.delegate.iterator();
    }

    public void add(int index, E element) {
        this.delegate.add(index, element);
    }

    public boolean removeIf(Predicate<? super E> filter) {
        return this.delegate.removeIf(filter);
    }

    public void clear() {
        this.delegate.clear();
    }

    public int indexOf(Object o) {
        return this.delegate.indexOf(o);
    }

    public int size() {
        return this.delegate.size();
    }

    public E get(int index) {
        return this.delegate.get(index);
    }

    public Stream<E> parallelStream() {
        return this.delegate.parallelStream();
    }

    public int lastIndexOf(Object o) {
        return this.delegate.lastIndexOf(o);
    }

    public void forEach(Consumer<? super E> action) {
        this.delegate.forEach(action);
    }

    public boolean add(E e) {
        return this.delegate.add(e);
    }

    public boolean contains(Object o) {
        return this.delegate.contains(o);
    }

    public boolean addAll(Collection<? extends E> c) {
        return this.delegate.addAll(c);
    }

    public <T> T[] toArray(T[] a) {
        return this.delegate.toArray(a);
    }

    public boolean isEmpty() {
        return this.delegate.isEmpty();
    }

    public boolean retainAll(Collection<?> c) {
        return this.delegate.retainAll(c);
    }

    public ListIterator<E> listIterator(int index) {
        return this.delegate.listIterator(index);
    }

    public void sort(Comparator<? super E> c) {
        this.delegate.sort(c);
    }

    public Object[] toArray() {
        return this.delegate.toArray();
    }

    public ListIterator<E> listIterator() {
        return this.delegate.listIterator();
    }

    public void replaceAll(UnaryOperator<E> operator) {
        this.delegate.replaceAll(operator);
    }

    public E set(int index, E element) {
        return this.delegate.set(index, element);
    }

    public Stream<E> stream() {
        return this.delegate.stream();
    }

    public Spliterator<E> spliterator() {
        return this.delegate.spliterator();
    }

    /**
     * Excluded methods that Lombok will not implement, we will implement/override these methods.
     */
    private abstract class ExcludedListMethods {

        public abstract E remove(int index);

        public abstract boolean remove(Object o);
    }
}

Here, all methods have a forwarding call to the wrapped instance.

Lastly, we'll create a RemovalCountingList instance and perform List operations.

public class ClientMain {

    public static void main(String[] args) {
        RemovalCountingList<String> cities = new RemovalCountingList<>(new ArrayList<>());

        cities.add("London");
        cities.add("Paris");
        cities.add("Istanbul");
        cities.add("Tokyo");

        String removedCity = cities.remove(0);
        System.out.println("Removed city: " + removedCity);

        boolean isRemoved = cities.remove("Istanbul");
        System.out.println("Is removed?: " + isRemoved);
    }
}

4. Using Generics

Previously, Lombok inferred the delegation type from the annotated field. However, the @Delegate annotation also has a types attribute to explicitly define the delegation types.

When using the types attribute, if the target class is a parameterized type, it can create some complications.

Let's investigate the case with an example:

@Delegate(types = List.class, excludes = ExcludedListMethods.class)
private final List<E> delegate;

Here, we're adding the types attribute besides excludes. Then we're providing List.class without the type parameter while RemovalCountingList still uses a type parameter. As a result, the method signatures don't match. We may try adding the type parameter in types, but List<E>.class is not a valid expression.

To solve this issue, we must create a private inner class using the type parameter of the container class. In this case, the code will compile successfully:

@Delegate(types = IncludedListMethods.class, excludes = ExcludedListMethods.class)
private final List<E> delegate;

/**
 * Included methods that Lombok will implement.
 */
private abstract class IncludedListMethods implements List<E> {

}

Note that the inner class IncludedListMethods implements List<E>.

5. Summary

In this tutorial, we've investigated the usage of the @Delegate annotation with Lombok. We've seen that Lombok can generate forwarding decorators in an easy way.

As always the source code for all examples in this tutorial is available on Github.