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.