La risposta breve è: è sicuro se li usi in modo sicuro :)
La risposta snarky: dimmi che cosa si intende per tratti, e forse ti darò una risposta migliore :)
In tutta serietà, il termine "tratto" non è ben definito. Molti sviluppatori Java hanno più familiarità con i tratti così come sono espressi in Scala, ma Scala è ben lungi dall'essere il primo linguaggio ad avere tratti, sia nel nome che negli effetti.
Ad esempio, in Scala, i tratti sono stateful (possono avere var
variabili); in Fortress sono puro comportamento. Le interfacce di Java con i metodi predefiniti sono senza stato; questo significa che non sono tratti? (Suggerimento: quella era una domanda trabocchetto.)
Anche in questo caso, in Scala, i tratti sono composti attraverso la linearizzazione; se la classe A
estende tratti X
e Y
, l'ordine in cui X
e Y
sono mescolati in determina come i conflitti tra X
e Y
si risolvono. In Java, questo meccanismo di linearizzazione non è presente (è stato rifiutato, in parte, perché troppo "non simile a Java".)
La ragione approssimativa per aggiungere metodi predefiniti alle interfacce era supportare l' evoluzione dell'interfaccia , ma eravamo ben consapevoli che stavamo andando oltre. Che tu lo consideri "evoluzione dell'interfaccia ++" o "tratti--" è una questione di interpretazione personale. Quindi, per rispondere alla tua domanda sulla sicurezza ... fintanto che ti attieni a ciò che il meccanismo effettivamente supporta, piuttosto che cercare di estenderlo desiderosamente a qualcosa che non supporta, dovresti stare bene.
Uno degli obiettivi principali del progetto era che, dal punto di vista del client di un'interfaccia, i metodi predefiniti non fossero distinguibili dai metodi di interfaccia "normali". L'impostazione predefinita di un metodo, quindi, è interessante solo per il progettista e l' implementazione dell'interfaccia.
Ecco alcuni casi d'uso che rientrano perfettamente negli obiettivi di progettazione:
Evoluzione dell'interfaccia. Qui, stiamo aggiungendo un nuovo metodo a un'interfaccia esistente, che ha un'implementazione predefinita ragionevole in termini di metodi esistenti su quell'interfaccia. Un esempio potrebbe essere l'aggiunta del forEach
metodo a Collection
, dove l'implementazione predefinita è scritta in termini di iterator()
metodo.
Metodi "opzionali". Qui, il progettista di un'interfaccia sta dicendo: "Gli implementatori non hanno bisogno di implementare questo metodo se sono disposti a convivere con i limiti di funzionalità che ciò comporta". Ad esempio, è Iterator.remove
stato assegnato un valore predefinito che genera UnsupportedOperationException
; poiché la stragrande maggioranza delle implementazioni Iterator
ha comunque questo comportamento, l'impostazione predefinita rende questo metodo essenzialmente opzionale. (Se il comportamento da AbstractCollection
fosse espresso come predefinito su Collection
, potremmo fare lo stesso per i metodi mutativi.)
Metodi di convenienza. Questi sono metodi rigorosamente per comodità, ancora una volta generalmente implementati in termini di metodi non predefiniti sulla classe. Il logger()
metodo nel tuo primo esempio ne è un ragionevole esempio.
Combinatori. Questi sono metodi di composizione che istanziano nuove istanze dell'interfaccia in base all'istanza corrente. Ad esempio, i metodi Predicate.and()
o Comparator.thenComparing()
sono esempi di combinatori.
Se fornisci un'implementazione predefinita, dovresti anche fornire alcune specifiche per l'impostazione predefinita (nel JDK, usiamo il @implSpec
tag javadoc per questo) per aiutare gli implementatori a capire se vogliono sovrascrivere il metodo o meno. Alcuni valori predefiniti, come metodi di convenienza e combinatori, non vengono quasi mai sovrascritti; altri, come i metodi opzionali, sono spesso sovrascritti. È necessario fornire specifiche sufficienti (non solo documentazione) su ciò che il valore predefinito promette di fare, in modo che l'implementatore possa prendere una decisione sensata sulla necessità di sovrascriverlo.