Java 8 Default Methods as Traits:Safe

Java 8 default methods as traits : safe?

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.

Java default interface methods concrete use cases

Brian Goetz and I covered some of this at our JavaOne 2015 talk, API Design with Java 8 Lambda and Streams. Despite the title, there is some material at the end about default methods.

Slides: https://stuartmarks.files.wordpress.com/2015/10/con6851-api-design-v2.pdf

Video: https://youtu.be/o10ETyiNIsM?t=24m

I'll summarize here what we said about default methods.

Interface Evolution

The primary use case of default methods is interface evolution. Mainly, this is the ability to add methods to interfaces without breaking backward compatibility. As noted in the question, this was most prominently employed to add methods allowing conversion of Collections to Streams and to add lambda-based APIs to Collections.

There are several other use cases, though.

Optional Methods

Sometimes interface methods are logically "optional". Consider mutator methods on immutable collections, for example. Of course, an implementation is required, but usually what it will do in such cases is to throw an exception. This can easily be done in a default method. Implementations can inherit the exception-throwing method if they don't want to provide it, or they can override it if they want to provide an implementation. Example: Iterator.remove.

Convenience Methods

Sometimes a method is provided for the convenience of callers, and there is an obvious and optimal implementation. This implementation can be provided by a default method. It's legal for an implementation to override the default, but there's generally no reason, so implementations will usually inherit it. Examples: Comparator.reversed, Spliterator.getExactSizeIfKnown, Spliterator.hasCharacteristics. Note that Spliterator was introduced in Java 8, including the default methods, so this clearly wasn't a case of interface evolution.

Simple Implementation, Intended to be Overridden

A default method can provide a simple, general implementation that works for all implementations, but that is probably suboptimal. This assists implementations during initial bring-up, because they can inherit the default and be assured of correct operation. However, in the long term, implementations will probably want to override the default and provide an improved, customized implementation.

Example: List.sort. The default implementation copies the list elements to a temporary array, sorts the array, and copies the elements back to the list. This is a correct implementation, and sometimes it can't be improved upon (e.g. for LinkedList). However, ArrayList overrides sort and sorts its internal array in-place. This avoids the copying overhead.

Now, obviously sort was retrofitted onto List and ArrayList in Java 8, so the evolution didn't happen this way. But you could easily imagine bringing up a new List implementation. You'd probably initially inherit the sort default implementation while you're getting the basics implemented properly. Later on, you might consider implementing a customized sort algorithm that's tuned to your new implementation's internal data organization.

Does Java have plan that default method (java8) Substitute for Abstract Class?

Default methods can't substitute abstract classes, as abstract classes can (and often do) have fields. Interfaces can only contain behaviour and not state, which is unlikely to change in the future as multiple inheritance of state in Java is seen (rightly or wrongly) as evil.

They can also have final methods, which is another thing you can't mimic with default methods.

If anything, interfaces with default methods resemble traits rather than abstract classes, but the match isn't perfect. Using interfaces as traits is something that has to be done very carefully and knowing the limitations they come with. (Such as any implementing class can override a default method, potentially ruining the trait.)

More on this here.

Are there any good examples of default methods and their usage in the Java 8 JDK?

There are several places in standard classes which I know beyond collections:

  • Java SQL API has interface enhancements. ResultSet.updateObject, PreparedStatement.setObject, PreparedStatement.executeLargeUpdate (differs from executeUpdate returning long instead of int) and so on were added. By default they throw UnsupportedOperationException or some other exception. This is "interface evolution": a way to enhance older interface with new features.

  • Functions in java.util.function have default methods which allow to combine them. These methods are not expected to be redefined as functional interfaces are mostly intended to work with lambdas. These methods are just for convenience. This is not the "interface evolution" as these interfaces appeared first in Java 8. However were these methods non-default, you would not be able to express these interfaces with lambdas as single-abstract-method rule would be violated.

  • Several default methods added to java.util.Comparator: reversed(), thenComparing() and so on. Also default implementations are fine and usually not intended to be replaced. This is similar to functions, but Comparator interface existed before, so it's also kind of interface evolution.

  • The java.util.Spliterator interface has default methods. This is actually good example as default methods of different purpose are shown here. The getComparator() method by default throws, but it must be overridden if spliterator uses SORTED characteristic. The hasCharacteristic() method is just a convenience way to use non-default characteristics() method and probably should never be redefined. The forEachRemaining() default implementation will always work correctly, but overriding it may produce much more effective result for some spliterators. The getExactSizeIfKnown() default implementation is also always correct and usually no need to redefine it. However if your spliterator is never sized, you can optimize it a little, just returning -1.

Safe to call Java 8 Collection default methods on results of Collections.synchronizedX?

The real state of the things in OpenJDK/OracleJDK is the following:

  • New spliterator(), stream() and parallelStream() methods are not synchronized and must be manually synchronized externally (similarly to iterator() or listIterator() existed before).

  • Other new methods are synchronized including forEach, removeIf, replaceAll, sort, getOrDefault, putIfAbsent, replace, computeIfAbsent, computeIfPresent, compute, merge.

Such behavior is actually specified:

It is imperative that the user manually synchronize on the returned collection when traversing it via Iterator, Spliterator or Stream

So you can expect that any other method except these explicitly mentioned exceptions is synchronized. The subtle problem is that it's specified for synchronizedCollection only, but not for other methods and it's not specified explicitly that, for example, synchronizedList inherits some behavior from synchronizedCollection (though in fact it does).

Note that bulk processing methods like forEach or replaceAll hold a monitor during the whole iteration, so you finally have a chance to safely iterate/update the whole collection. However you should be aware of possible deadlocks/starvation as collection might be locked for the long time.

Also note that current state introduces the difference between syncCollection.forEach(...) and syncCollection.stream().forEach(...): the second call is not synchronized.

UPDATE: I reported to the OpenJDK developers that specification of synchronizedXXX methods has to be updated and submitted a patch which was accepted for JDK-9.

Is it OK to add default implementations to methods of an interface which represents a listener?

But the library developer refuses to add default implementations,
stating that it would violate java best practices and using default
implementations in interface methods go against the purpose of
interfaces.

This argument was valid until default interface methods were introduced.
Now your colleague would have to argue that the JLS of Java 8 perverted interfaces and the JDK now contains classes which go against the purpose of interfaces. Still this is a viable extreme standpoint but fruitless.

You simply can avoid the discussion by deriving an own interface from the library interface and providing default empty implementations for all inherited methods.

public interface MyInterface extends LibraryInterface {
@Override default public void event1() {
}

...
}

Or you both can review the following design which seems dubious to me and which led to your discussion about default method in interfaces:

Even though a listener is only interested in few events, listener
has to implement all the methods when extending the interface.

A solution could be to simply split up the big interface in many smaller ones.

Why is final not allowed in Java 8 interface methods?

This question is, to some degree, related to What is the reason why “synchronized” is not allowed in Java 8 interface methods?

The key thing to understand about default methods is that the primary design goal is interface evolution, not "turn interfaces into (mediocre) traits". While there's some overlap between the two, and we tried to be accommodating to the latter where it didn't get in the way of the former, these questions are best understood when viewed in this light. (Note too that class methods are going to be different from interface methods, no matter what the intent, by virtue of the fact that interface methods can be multiply inherited.)

The basic idea of a default method is: it is an interface method with a default implementation, and a derived class can provide a more specific implementation. And because the design center was interface evolution, it was a critical design goal that default methods be able to be added to interfaces after the fact in a source-compatible and binary-compatible manner.

The too-simple answer to "why not final default methods" is that then the body would then not simply be the default implementation, it would be the only implementation. While that's a little too simple an answer, it gives us a clue that the question is already heading in a questionable direction.

Another reason why final interface methods are questionable is that they create impossible problems for implementors. For example, suppose you have:

interface A { 
default void foo() { ... }
}

interface B {
}

class C implements A, B {
}

Here, everything is good; C inherits foo() from A. Now supposing B is changed to have a foo method, with a default:

interface B { 
default void foo() { ... }
}

Now, when we go to recompile C, the compiler will tell us that it doesn't know what behavior to inherit for foo(), so C has to override it (and could choose to delegate to A.super.foo() if it wanted to retain the same behavior.) But what if B had made its default final, and A is not under the control of the author of C? Now C is irretrievably broken; it can't compile without overriding foo(), but it can't override foo() if it was final in B.

This is just one example, but the point is that finality for methods is really a tool that makes more sense in the world of single-inheritance classes (generally which couple state to behavior), than to interfaces which merely contribute behavior and can be multiply inherited. It's too hard to reason about "what other interfaces might be mixed into the eventual implementor", and allowing an interface method to be final would likely cause these problems (and they would blow up not on the person who wrote the interface, but on the poor user who tries to implement it.)

Another reason to disallow them is that they wouldn't mean what you think they mean. A default implementation is only considered if the class (or its superclasses) don't provide a declaration (concrete or abstract) of the method. If a default method were final, but a superclass already implemented the method, the default would be ignored, which is probably not what the default author was expecting when declaring it final. (This inheritance behavior is a reflection of the design center for default methods -- interface evolution. It should be possible to add a default method (or a default implementation to an existing interface method) to existing interfaces that already have implementations, without changing the behavior of existing classes that implement the interface, guaranteeing that classes that already worked before default methods were added will work the same way in the presence of default methods.)

Using (empty) default method to make FunctionalInterface

As said in this answer, allowing to create interfaces with more than one method still being functional interfaces, is one of the purposes of default methods. As also mentioned there, you will find examples within the Java API itself, say Comparator, Predicate, or Function, having default methods and intentionally being functional interfaces.

It doesn’t matter whether the default implementation is doing nothing or not, the more important question is, how natural is this default implementation. Does it feel like a kludge, just to make lambdas possible, or is it indeed what some or even most implementations would use any way (regardless of how they are implemented)?

Not needing a special clean up action might be indeed a common case, even if you follow the suggestion made in a comment, to let your interface extend AutoCloseable and name the method close instead of end. Note that likewise, Stream implements AutoCloseable and its default behavior is to do nothing on close(). You could even follow the pattern to allow specifying the cleanup action as separate Runnable, similar to Stream.onClose(Runnable):

public interface StringTransformer extends UnaryOperator<String>, AutoCloseable {
static StringTransformer transformer(Function<String,String> f) {
return f::apply;
}
String transform(String s);
@Override default String apply(String s) { return transform(s); }
@Override default void close() {}
default StringTransformer onClose(Runnable r) {
return new StringTransformer() {
@Override public String transform(String s) {
return StringTransformer.this.transform(s);
}
@Override public void close() {
try(StringTransformer.this) { r.run(); }
}
};
}
}

This allows registering a cleanup action via onClose, so the following works:

try(StringTransformer t = 
StringTransformer.transformer(String::toUpperCase)
.onClose(()->System.out.println("close"))) {
System.out.println(t.apply("some text"));
}

resp.

try(StringTransformer t = transformer(String::toUpperCase)
.onClose(()->System.out.println("close 1"))) {
System.out.println(t.apply("some text"));
}

if you use import static. It also ensures safe closing if you chain multiple actions like

try(StringTransformer t = transformer(String::toUpperCase)
.onClose(()->System.out.println("close 1"))
.onClose(()->{ throw new IllegalStateException(); })) {
System.out.println(t.apply("some text"));
}

or

try(StringTransformer t = transformer(String::toUpperCase)
.onClose(()->{ throw new IllegalStateException("outer fail"); })
.onClose(()->{ throw new IllegalStateException("inner fail"); })){
System.out.println(t.apply("some text"));
}

Note that try(StringTransformer.this) { r.run(); } is Java 9 syntax. For Java 8, you would need try(StringTransformer toClose = StringTransformer.this) { r.run(); }.

Throw exception in interface default method

One could starte here to see what Brian Goetz, one of the "acting fathers" of Java has to say about "how to use default methods".

So, according "to the books"; one could use them like in your example: to throw an exception for a method regarded "optional". But that would mean: you have already some methods; and you are adding new ones.

The main purpose of adding default methods to the Java language was to allow for non-breaking enhancements of existing interfaces. They were not meant to be used as some sort of "mixin" / multi-inheritance / traits-like providing construct.

But beyond that: in your example, you have a new interface ... that only has that one method. I think this does not fall under those "intended usages".

On the other hand; don't be too much "about the books". When a whole team of people agrees "this is what we do", and everybody understands and buys into that; why not?!

But there is one caveat ... my C++ coworkers have a strict policy: they allow for exactly one implementation of any abstract method within an inheritance tree; as it is very hard to debug a problem when you are looking at the wrong implementation of some method. And now that we can inherit default methods in Java, too ... debugging problems could become harder in Java for us in the same way. So be careful how such things are used!

Long story short: it is a good practice if the big majority of your development team finds it a helpful practice. If not, it is not.



Related Topics



Leave a reply



Submit