Java 8 default methods as traits : safe?
Asked Answered
T

1

120

Is it a safe practice to use default methods as a poor's man version of traits in Java 8?

Some claim it may make pandas sad if you use them just for the sake of it, because it's cool, but that's not my intention. It is also often reminded that default methods were introduced to support API evolution and backward compatibility, which is true, but this does not make it wrong or twisted to use them as traits per se.

I have the following practical use case in mind:

public interface Loggable {
    default Logger logger() {
        return LoggerFactory.getLogger(this.getClass());
    }
}

Or perhaps, define a PeriodTrait:

public interface PeriodeTrait {
    Date getStartDate();
    Date getEndDate();
    default isValid(Date atDate) {
        ...
    }
}

Admitedly, composition could be used (or even helper classes) but it seems more verbose and cluttered and does not allow to benefit from polymorphism.

So, is it ok/safe to use default methods as basic traits, or should I be worried about unforeseen side effects?

Several questions on SO are related to Java vs Scala traits; that's not the point here. I'm not asking merely for opinions either. Instead, I'm looking for an authoritative answer or at least field insight: if you've used default methods as traits on your corporate project, did it turn out to be a timebomb?

Telegony answered 23/2, 2015 at 19:25 Comment(4)
It seems to me that you could get the same benefits from inheriting an abstract class and not have to worry about making pandas cry... The only reason I can see to use a default method in an Interface is that you need the functionality and cannot modify a bunch of legacy code which is based on the Interface.Clueless
I agree with @infosec812 about extending an abstract class that defines its own static logger field. Wouldn't your logger() method instantiate a new logger instance every time its called?Neelon
For logging, you might want to look at Projectlombok.org and their @Slf4j annotation.Clueless
Related question: Why is “final” not allowed in Java 8 interface methods?Pepsin
K
130

The short answer is: it's safe if you use them safely :)

The snarky answer: tell me what you mean by traits, and maybe I'll give you a better answer :)

In all seriousness, the term "trait" is not well-defined. Many Java developers are most familiar with traits as they are expressed in Scala, but Scala is far from the first language to have traits, either in name or in effect.

For example, in Scala, traits are stateful (can have var variables); in Fortress they are pure behavior. Java's interfaces with default methods are stateless; does this mean they are not traits? (Hint: that was a trick question.)

Again, in Scala, traits are composed through linearization; if class A extends traits X and Y, then the order in which X and Y are mixed in determines how conflicts between X and Y are resolved. In Java, this linearization mechanism is not present (it was rejected, in part, because it was too "un-Java-like".)

The proximate reason for adding default methods to interfaces was to support interface evolution, but we were well aware that we were going beyond that. Whether you consider that to be "interface evolution++" or "traits--" is a matter of personal interpretation. So, to answer your question about safety ... so long as you stick to what the mechanism actually supports, rather than trying to wishfully stretch it to something it does not support, you should be fine.

A key design goal was that, from the perspective of the client of an interface, default methods should be indistinguishable from "regular" interface methods. The default-ness of a method, therefore, is only interesting to the designer and implementor of the interface.

Here are some use cases that are well within the design goals:

  • Interface evolution. Here, we are adding a new method to an existing interface, which has a sensible default implementation in terms of existing methods on that interface. An example would be adding the forEach method to Collection, where the default implementation is written in terms of the iterator() method.

  • "Optional" methods. Here, the designer of an interface is saying "Implementors need not implement this method if they are willing to live with the limitations in functionality that entails". For example, Iterator.remove was given a default which throws UnsupportedOperationException; since the vast majority of implementations of Iterator have this behavior anyway, the default makes this method essentially optional. (If the behavior from AbstractCollection were expressed as defaults on Collection, we might do the same for the mutative methods.)

  • Convenience methods. These are methods that are strictly for convenience, again generally implemented in terms of non-default methods on the class. The logger() method in your first example is a reasonable illustration of this.

  • Combinators. These are compositional methods that instantiate new instances of the interface based on the current instance. For example, the methods Predicate.and() or Comparator.thenComparing() are examples of combinators.

If you provide a default implementation, you should also provide some specification for the default (in the JDK, we use the @implSpec javadoc tag for this) to aid implementors in understanding whether they want to override the method or not. Some defaults, like convenience methods and combinators, are almost never overridden; others, like optional methods, are often overridden. You need to provide enough specification (not just documentation) about what the default promises to do, so the implementor can make a sensible decision about whether they need to override it.

Knobloch answered 23/2, 2015 at 22:38 Comment(6)
Thank you Brian for this comprehensive answer. Now I can use default methods with a light heart. Readers: more info from Brian Goetz on interface evolution and default methods can be found in NightHacking Worldwide Lambdas for instance.Telegony
Thanks @brian-goetz. From what you've said, I think Java's default methods are closer to the notion of traditional traits as defined in the Ducasse et al paper (scg.unibe.ch/archive/papers/Duca06bTOPLASTraits.pdf). Scala "traits" don't seem like traits to me at all, since they have state, use linearization in their composition, and it would appear that they also implicitly resolve method conflicts - and these are all the things that traditional traits don't have. In fact, I would say Scala traits are more like mixins than traits. What do you think? PS: I've never coded in Scala.Pneumonectomy
How about using empty default implementations for methods in an interface which acts as a listener? The listener implementation may only be interested in listening for few interface methods, so by making methods default, implementer only has to implement the methods which it needs to listen.Ossy
@LahiruChandima Like with MouseListener? To the extent that this API style makes sense, this fits into the "optional methods" bucket. Make sure to document the optional-ness clearly!Knobloch
Yes. Just as in MouseListener. Thanks for the response.Ossy
I would argue that optional-ness is okay for utility type methods (such as in OP's example) that are orthogonal to the domain in question. But I would argue that business rules should not be implemented as default methods and that the investment in the use of OOP, Design Patterns etc. for implementing the rules of the domain are well worth it--to aid in reasoning and maintaining the constantly changing state of the domain as it's implemented in the code.Unwholesome

© 2022 - 2024 — McMap. All rights reserved.