Java 8 not only gives us both default and static methods on interfaces. One of the consequences of this is you can create simple value objects using interfaces alone, without the need to define a class.
Here’s an example. We define a Paint type which is composed of an amount of red/green/blue paint. We can add operations that make use of these like mix which produces the result of mixing two paints together. We can also create a static factory method in lieu of a constructor to create us an instance of a paint.
interface Paint {
int red();
int green();
int blue();
default Paint mix(Paint other) {
return create(red() + other.red(), green() + other.green(), blue() + other.blue());
}
static Paint create(int red, int green, int blue) {
return new Paint() {
public int red() { return red; }
public int green() { return green; }
public int blue() { return blue; }
};
}
}
This is what using it looks like
@Test
public void mixingPaint() {
Paint red = Paint.create(100,0,0);
Paint green = Paint.create(0,100,0);
Paint mixed = red.mix(green);
assertEquals(100, mixed.red());
assertEquals(100, mixed.green());
}
While it may seem odd – there are advantages to doing this sort of thing. There’s a slight reduction in boilerplate due to not having to deal with fields. It’s a way of ensuring your value type is immutable because you can’t so easily introduce state. It also allows you to make use of multiple inheritance, because you can inherit from multiple interfaces.
There are also obvious disadvantages – we can only have public attributes and methods.
Adding equals/hashcode/toString is a bit harder because in an interface we cannot override methods defined on a class.
I’d like to be able to do the following, where equivalent paints are equal.
@Test
public void paintEquals() {
Paint red = Paint.create(100,0,0);
Paint green = Paint.create(0,100,0);
Paint mixed1 = red.mix(green);
Paint mixed2 = green.mix(red);
assertEquals(mixed1, mixed2);
assertNotEquals(red, green);
assertNotEquals(red, mixed1);
}
The least-verbose approach I’ve managed so far (without resorting to reflection) requires us to override the equals/hashCode/toString methods in our anonymous inner class.
We can, however, avoid having to implement them there and move the implementation to some helper interfaces.
The only additional boilerplate required is implementing a props() method that returns the properties we want to include in our equals/hashcode/toString.
interface Paint extends EqualsHashcode, ToString {
int red();
int green();
int blue();
default Paint mix(Paint other) {
return create(red() + other.red(), green() + other.green(), blue() + other.blue());
}
static Paint create(int red, int green, int blue) {
return new Paint() {
public int red() { return red; }
public int green() { return green; }
public int blue() { return blue; }
@Override public boolean equals(Object o) { return autoEquals(o); }
@Override public int hashCode() { return autoHashCode(); }
@Override public String toString() { return autoToString(); }
};
}
default List> props() {
return asList(Paint::red, Paint::green, Paint::blue);
}
}
This is still overly verbose. We can reduce it by moving the overrides to an abstract base class that we hide from the callers of our create() method. We simply move the equals/hashCode/toString overrides to a Value<T> base class, which provides a setter for the properties to use for the equals/hashCode.
This leaves us with the relatively consise
interface Paint {
int red();
int green();
int blue();
default Paint mix(Paint other) {
return create(red() + other.red(), green() + other.green(), blue() + other.blue());
}
static Paint create(int red, int green, int blue) {
abstract class PaintValue extends Value implements Paint {}
return new PaintValue() {
public int red() { return red; }
public int green() { return green; }
public int blue() { return blue; }
}.using(Paint::red, Paint::green, Paint::blue);
}
}
You will notice that paint now extends both EqualsHashcode and ToString, where we place the implementation of auto(Equals|HashCode|ToString).
Let’s look at toString first as it’s simpler. We define a default method that takes the value of the properties returned by our props() method above, and concatenates them together.
interface ToString {
default String autoToString() {
return "{" +
props().stream()
.map(prop -> (Object)prop.apply((T)this))
.map(Object::toString)
.collect(Collectors.joining(", ")) +
"}";
}
List> props();
}
EqualsHashcode is similar. For equals we can apply the property functions to “this” and also the supplied object for comparison. We require all properties to match on both objects for equality. In the same way we can calculate a hashcode based on the supplied properties.
public interface EqualsHashcode {
default boolean autoEquals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final T value = (T)o;
return props().stream()
.allMatch(prop -> Objects.equals(prop.apply((T) this), prop.apply(value)));
}
default int autoHashCode() {
return props().stream()
.map(prop -> (Object)prop.apply((T)this))
.collect(ResultCalculator::new, ResultCalculator::accept, ResultCalculator::combine)
.result;
}
static class ResultCalculator implements Consumer {
private int result = 0;
public void accept(Object value) {
result = 31 * result + (value != null ? value.hashCode() : 0);
}
public void combine(ResultCalculator other) {
result += other.result;
}
}
List> props();
}
What other reasons are there this is a crazy idea? Is there a better way of implementing equals/hashCode?