Rich Interfaces In Java

Leave a comment
Dev

One of the new features added to Java 8 is default methods. In previous versions of Java, adding a new method to an interface would break existing implementations. This made it very hard to evolve your public API without disrupting your clients. Java 8 now allows implementations of methods in an interface. This was needed because Java 8 introduced many new methods on existing interfaces such as sort in List or stream in Collection .

Thin versus rich interfaces

When creating an interface you face a trade-off in either making it rich or thin. A rich interface has many methods which makes it convenient for the caller while a thin interface has only a few methods which makes it easy to implement but not quite as useful for the caller.

Traditionally Java interfaces have been thin rather than rich. Scala, on the other hand, has traits which allow concrete implementations and so tend to be very rich. Now that Java 8 has default methods we can take the same rich interface approach in our Java code as Scala does with traits.

To create a rich interface, define a small number of abstract methods and a large number of concrete methods defined in terms of the abstract methods. Implementors of your interface need only implement the thin abstract part of your interface and thereby gain access to the richer part without having to write any more code.

An example

As a simple example I have implemented the Rational class and Ordered trait from the Programming in Scala book. We don’t get the symbolic method names and implicit conversions but the rich interface concept remains the same.

Here is our rich Ordered interface. It declares one abstract method compare but also implements a number of convenience methods in terms of this abstract method.

/**
 * A 'rich' interface for data that have a single, natural ordering.
 * Clients need only implement the compare method.
 */
public interface Ordered<T> {

    /**
     * Result of comparing `this` with operand `that`.
     *
     * Implement this method to determine how instances of T will be sorted.
     *
     * Returns `x` where:
     *
     *   - `x < 0` when `this < that`
     *
     *   - `x == 0` when `this == that`
     *
     *   - `x > 0` when  `this > that`
     */
    int compare(T that);

    /**
     * Returns true if `this` is less than `that`
     */
    default boolean lessThan(T that) {
        return compare(that) < 0;
    }

    /**
     * Returns true if `this` is greater than `that`.
     */
    default boolean greaterThan(T that) {
        return compare(that) > 0;
    }

    /**
     * Returns true if `this` is less than or equal to `that`.
     */
    default boolean lessThanOrEqual(T that) {
        return compare(that) <= 0;
    }

    /**
     * Returns true if `this` is greater than or equal to `that`.
     */
    default boolean greaterThanOrEqual(T that) {
        return compare(that) >= 0;
    }

    /**
     * Result of comparing `this` with operand `that`.
     */
    default int compareTo(T that) {
        return compare(that);
    }
}

Here is our Rational class which implements Ordered. Apart from overriding equals and hashCode, the only method we need to implement is compare and we get lessThan, greaterThan, lessThanOrEqual, greaterThanOrEqual and compareTo “for free”.

public class Rational implements Ordered<Rational> {

    public final int numerator;
    public final int denominator;

    public Rational(int numerator, int denominator) {
        if (denominator == 0) {
            throw new IllegalArgumentException("Denominator cannot be 0");
        }
        int g = gcd(Math.abs(numerator), Math.abs(denominator));
        this.numerator = numerator / g;
        this.denominator = denominator / g;
    }

    private int gcd(int a, int b) {
        if (b == 0) {
            return a;
        } else {
            return gcd(b, a % b);
        }
    }

    @Override
    public boolean equals(Object that) {
        if (that == this) {
            return true;
        }

        if (!(that instanceof Rational)) {
            return false;
        }

        Rational r = (Rational) that;
        return r.numerator == this.numerator && r.denominator == this.denominator;
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + numerator;
        result = 31 * result + denominator;
        return result;
    }

    @Override
    public String toString() {
        return numerator + "/" + denominator;
    }

    @Override
    public int compare(Rational that) {
        return (this.numerator * that.denominator) - (this.numerator * this.denominator);
    }

    public Rational add(Rational that) {
        return new Rational(this.numerator * that.denominator + that.numerator * this.denominator,
                this.denominator * that.denominator);
    }

    public Rational times(Rational that) {
        return new Rational(this.numerator * that.numerator, this.denominator * that.denominator);
    }
}

Here’s a unit test that exercises the class.

import org.junit.Test;

import static org.junit.Assert.*;

public class RationalTest {

    private final Rational half = new Rational(1, 2);
    private final Rational third = new Rational(1, 3);
    private final Rational twoThirds = new Rational(2, 3);
    private final Rational twoFourths = new Rational(2, 4);

    @Test(expected = IllegalArgumentException.class)
    public void divideByZero() throws Exception {
        new Rational(1, 0);
    }

    @Test
    public void testEquals() throws Exception {
        assertEquals(twoFourths, twoFourths);
        assertEquals(half, twoFourths);
        assertEquals(twoFourths, half);
        assertFalse(half.equals(twoThirds));
    }

    @Test
    public void testHashCode() throws Exception {
        assertEquals(twoFourths.hashCode(), new Rational(2, 4).hashCode());
        assertEquals(half.hashCode(), twoFourths.hashCode());
        assertFalse(half.hashCode() == twoThirds.hashCode());
    }

    @Test
    public void testString() throws Exception {
        assertEquals("2/5", new Rational(2, 5).toString());
        assertEquals("1/2", new Rational(3, 6).toString());
    }

    @Test
    public void compare() throws Exception {
        assertFalse(half.lessThan(third));
        assertTrue(third.lessThan(half));

        assertFalse(third.greaterThan(half));
        assertTrue(half.greaterThan(third));

        assertTrue(half.lessThanOrEqual(half));
        assertTrue(half.lessThanOrEqual(twoFourths));

        assertTrue(third.greaterThanOrEqual(third));
        assertTrue(twoFourths.greaterThanOrEqual(half));
    }

    @Test
    public void compareTo() throws Exception {
        assertTrue(third.compareTo(half) < 0);

        assertTrue(half.compareTo(half) == 0);
        assertTrue(half.compareTo(twoFourths) == 0);

        assertTrue(half.compareTo(third) > 0);
    }

    @Test
    public void add() throws Exception {
        Rational sevenSixths = half.add(twoThirds);

        assertEquals(7, sevenSixths.numerator);
        assertEquals(6, sevenSixths.denominator);
    }

    @Test
    public void times() throws Exception {
        Rational twoSixths = half.times(twoThirds);

        assertEquals(1, twoSixths.numerator);
        assertEquals(3, twoSixths.denominator);
    }
}

This is a very simple example but shows how we can introduce Scala’s rich interface style of traits into our Java 8 code.

Leave a Reply

Your email address will not be published. Required fields are marked *