Un gran caso de uso es lo que yo llamo interfaces de "palanca": interfaces que solo tienen un pequeño número de métodos abstractos (idealmente 1), pero proporcionan un gran "apalancamiento" en el sentido de que le brindan mucha funcionalidad. : solo necesitas implementar 1 método en tu clase pero obtienes muchos otros métodos "gratis". Piense en una interfaz de colección, por ejemplo, con un solo método abstracto foreach
y métodos default
como map
, fold
, reduce
, filter
, partition
, groupBy
, sort
, sortBy
, etc.
Aquí hay un par de ejemplos. Comencemos con java.util.function.Function<T, R>
. Tiene un solo método abstracto R apply<T>
. Y tiene dos métodos predeterminados que le permiten componer la función con otra función de dos maneras diferentes, antes o después. Ambos métodos de composición se implementan utilizando solo apply
:
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
return (T t) -> after.apply(apply(t));
}
También puedes crear una interfaz para objetos similares, algo como esto:
interface MyComparable<T extends MyComparable<T>> {
int compareTo(T other);
default boolean lessThanOrEqual(T other) {
return compareTo(other) <= 0;
}
default boolean lessThan(T other) {
return compareTo(other) < 0;
}
default boolean greaterThanOrEqual(T other) {
return compareTo(other) >= 0;
}
default boolean greaterThan(T other) {
return compareTo(other) > 0;
}
default boolean isBetween(T min, T max) {
return greaterThanOrEqual(min) && lessThanOrEqual(max);
}
default T clamp(T min, T max) {
if (lessThan( min)) return min;
if (greaterThan(max)) return max;
return (T)this;
}
}
class CaseInsensitiveString implements MyComparable<CaseInsensitiveString> {
CaseInsensitiveString(String s) { this.s = s; }
private String s;
@Override public int compareTo(CaseInsensitiveString other) {
return s.toLowerCase().compareTo(other.s.toLowerCase());
}
}
O un marco de colecciones extremadamente simplificado, donde todas las operaciones de colecciones devuelven Collection
, independientemente de cuál era el tipo original:
interface MyCollection<T> {
void forEach(java.util.function.Consumer<? super T> f);
default <R> java.util.Collection<R> map(java.util.function.Function<? super T, ? extends R> f) {
java.util.Collection<R> l = new java.util.ArrayList();
forEach(el -> l.add(f.apply(el)));
return l;
}
}
class MyArray<T> implements MyCollection<T> {
private T[] array;
MyArray(T[] array) { this.array = array; }
@Override public void forEach(java.util.function.Consumer<? super T> f) {
for (T el : array) f.accept(el);
}
@Override public String toString() {
StringBuilder sb = new StringBuilder("(");
map(el -> el.toString()).forEach(s -> { sb.append(s); sb.append(", "); } );
sb.replace(sb.length() - 2, sb.length(), ")");
return sb.toString();
}
public static void main(String... args) {
MyArray<Integer> array = new MyArray<>(new Integer[] {1, 2, 3, 4});
System.out.println(array);
// (1, 2, 3, 4)
}
}
Esto se vuelve muy interesante en combinación con las lambdas, porque dicha interfaz "palanca" puede ser implementada por una lambda (es una interfaz SAM).
Este es el mismo caso de uso para el que se agregaron los métodos de extensión en C♯, pero los métodos predeterminados tienen una ventaja distinta: son métodos de instancia "adecuados", lo que significa que tienen acceso a los detalles de implementación privada de la interfaz (% Los métodos de interfaz private
vienen en Java 9), mientras que los métodos de extensión son solo azúcar sintáctica para métodos estáticos.
Si Java alguna vez recibiera la inyección de interfaz, también permitiría la instalación de parches modulares de tipo seguro, de ámbito y de tipo. Esto sería muy interesante para los implementadores de lenguaje en la JVM: en este momento, por ejemplo, JRuby hereda o envuelve las clases de Java para proporcionarles semántica de Ruby adicional, pero lo ideal es que quieran usar las mismas clases. Con la inyección de interfaz y los métodos predeterminados, se podrían inyectar, por ejemplo. una interfaz RubyObject
en java.lang.Object
, de modo que un Java Object
y un Ruby Object
son exactamente lo mismo .