Perché "final" non è consentito nei metodi di interfaccia Java 8?


335

Una delle funzionalità più utili di Java 8 sono i nuovi defaultmetodi sulle interfacce. Ci sono essenzialmente due ragioni (potrebbero essercene altre) per cui sono state introdotte:

Dal punto di vista di un progettista API, mi sarebbe piaciuto poter usare altri modificatori su metodi di interfaccia, ad es final. Ciò sarebbe utile quando si aggiungono metodi di praticità, evitando sostituzioni "accidentali" nelle classi di implementazione:

interface Sender {

    // Convenience method to send an empty message
    default final void send() {
        send(null);
    }

    // Implementations should only implement this method
    void send(String message);
}

Quanto sopra è già una pratica comune se Senderfosse una classe:

abstract class Sender {

    // Convenience method to send an empty message
    final void send() {
        send(null);
    }

    // Implementations should only implement this method
    abstract void send(String message);
}

Ora, defaulte finalsono ovviamente in contraddizione con le parole chiave, ma la parola chiave predefinita stessa non sarebbe stata strettamente richiesta , quindi presumo che questa contraddizione sia intenzionale, per riflettere le sottili differenze tra "metodi di classe con body" (solo metodi) e "interfaccia" metodi con corpo " (metodi predefiniti), cioè differenze che non ho ancora capito.

Ad un certo punto, il supporto per modificatori come statice finalsui metodi di interfaccia non è stato ancora completamente esplorato, citando Brian Goetz :

L'altra parte è fino a che punto andremo a supportare gli strumenti di costruzione di classi nelle interfacce, come metodi finali, metodi privati, metodi protetti, metodi statici, ecc. La risposta è: non lo sappiamo ancora

Da quel momento alla fine del 2011, ovviamente, è staticstato aggiunto il supporto per i metodi nelle interfacce. Chiaramente, questo ha aggiunto molto valore alle librerie JDK stesse, come ad esempio con Comparator.comparing().

Domanda:

Qual è il motivo final(e anche static final) mai raggiunto le interfacce Java 8?


10
Mi dispiace essere una coperta bagnata, ma l'unico modo in cui alla domanda espressa nel titolo verrà data risposta nei termini di SO è tramite una citazione di Brian Goetz o del gruppo di esperti JSR. Comprendo che BG ha richiesto una discussione pubblica, ma questo è esattamente ciò che contravviene ai termini della comunicazione degli addebiti, in quanto è "principalmente basato sull'opinione". Mi sembra che le responsabilità siano state evitate qui. È compito del gruppo di esperti, e del più ampio processo della comunità Java, stimolare la discussione e elaborare razionali. Non di SO. Quindi voto per chiudere come "principalmente basato sull'opinione".
Marchese di Lorne,

2
Come tutti sappiamo, finalimpedisce che un metodo venga sovrascritto e, vedendo come DEVI scavalcare i metodi ereditati dalle interfacce, non vedo perché abbia senso renderlo definitivo. A meno che non significasse che il metodo è definitivo DOPO averlo ignorato una volta .. In quel caso, forse ci sono delle difficoltà? Se non capisco questo diritto, per favore lasciami kmow. Sembra interessante
Vince Emigh

22
@EJP: "Se posso farlo, lo puoi fare anche tu, e rispondere alla tua domanda, che quindi non avrebbe dovuto essere fatta." Ciò si applicherebbe praticamente a tutte le domande su questo forum, giusto? Potrei sempre Google qualche argomento per 5 ore da solo e quindi imparare qualcosa che tutti gli altri devono imparare nel modo altrettanto duro. Oppure aspettiamo un altro paio di minuti affinché qualcuno dia una risposta ancora migliore che tutti in futuro (compresi i 12 voti positivi e le 8 stelle finora) possano trarre vantaggio, perché SO è così ben referenziato su Google. Quindi sì. Questa domanda può adattarsi perfettamente al modulo di domande e risposte di SO.
Lukas Eder,

5
@VinceEmigh " ... vedendo come DEVI scavalcare i metodi ereditati dalle interfacce ... " Questo non è vero in Java 8. Java 8 ti consente di implementare metodi nelle interfacce, il che significa che non devi implementarli nell'implementazione classi. In questo caso, finalsarebbe utile impedire alle classi di implementazione di sovrascrivere l'implementazione predefinita di un metodo di interfaccia.
awksp,

28
@EJP non lo sai mai: Brian Goetz potrebbe rispondere !
Assylias,

Risposte:


419

Questa domanda è, in una certa misura, correlata a Qual è il motivo per cui la "sincronizzazione" non è consentita nei metodi di interfaccia Java 8?

La cosa chiave da capire sui metodi predefiniti è che l'obiettivo principale del progetto è l' evoluzione dell'interfaccia , non "trasformare le interfacce in tratti (mediocri)". Sebbene ci sia una certa sovrapposizione tra i due, e abbiamo cercato di essere accomodanti verso il secondo in cui non si è ostacolato il primo, queste domande sono meglio comprese se viste in questa luce. (Nota anche che i metodi di classe stanno andando essere diverso da metodi di interfaccia, indipendentemente l'intento, in virtù del fatto che i metodi di interfaccia possono essere moltiplicano ereditate.)

L'idea di base di un metodo predefinito è: si tratta di un metodo di interfaccia con un'implementazione predefinita e una classe derivata può fornire un'implementazione più specifica. E poiché il centro di progettazione era l'evoluzione dell'interfaccia, era un obiettivo di progettazione fondamentale che i metodi predefiniti potessero essere aggiunti alle interfacce dopo il fatto in modo compatibile con la sorgente e binaria.

La risposta troppo semplice al "perché non i metodi predefiniti finali" è che allora il corpo non sarebbe semplicemente l'implementazione predefinita, sarebbe l'unica implementazione. Anche se questa è una risposta un po 'troppo semplice, ci dà un indizio che la domanda sta già andando in una direzione discutibile.

Un altro motivo per cui i metodi di interfaccia finali sono discutibili è che creano problemi impossibili per gli implementatori. Ad esempio, supponiamo di avere:

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

interface B { 
}

class C implements A, B { 
}

Qui va tutto bene; Ceredita foo()da A. Ora supponiamo che Bsia cambiato per avere un foometodo, con un valore predefinito:

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

Ora, quando andremo a ricompilare C, il compilatore ci dirà che non sa per quale comportamento ereditare foo(), quindi Cdeve sovrascriverlo (e potrebbe scegliere di delegare A.super.foo()se voleva mantenere lo stesso comportamento). Baveva fatto il suo default finale Anon è sotto il controllo dell'autore di C? Ora Cè irrimediabilmente rotto; non può essere compilato senza foo()eseguire l' override , ma non può eseguire l'override foo()se è stato finalizzato B.

Questo è solo un esempio, ma il punto è che la finalità dei metodi è in realtà uno strumento che ha più senso nel mondo delle classi a eredità singola (generalmente quali coppie dichiarano al comportamento), piuttosto che alle interfacce che semplicemente contribuiscono al comportamento e possono essere moltiplicate ereditato. È troppo difficile ragionare su "quali altre interfacce potrebbero essere mescolate nell'eventuale implementatore", e consentire che un metodo di interfaccia sia definitivo causerebbe probabilmente questi problemi (e si farebbero saltare in aria non sulla persona che ha scritto l'interfaccia, ma sulla povero utente che cerca di implementarlo.)

Un altro motivo per non consentire loro è che non significhino ciò che pensi che significhino. Un'implementazione predefinita viene considerata solo se la classe (o le sue superclassi) non forniscono una dichiarazione (concreta o astratta) del metodo. Se un metodo predefinito fosse definitivo, ma una superclasse già implementasse il metodo, il valore predefinito verrebbe ignorato, il che probabilmente non è quello che l'autore predefinito si aspettava quando lo avesse dichiarato definitivo. (Questo comportamento di ereditarietà riflette il centro di progettazione per i metodi predefiniti - evoluzione dell'interfaccia. Dovrebbe essere possibile aggiungere un metodo predefinito (o un'implementazione predefinita a un metodo di interfaccia esistente) alle interfacce esistenti che dispongono già di implementazioni, senza modificare comportamento delle classi esistenti che implementano l'interfaccia,


88
Fantastico vederti rispondere alle domande sulle nuove funzionalità della lingua! È molto utile ottenere chiarimenti sull'intento e sui dettagli del progetto quando si capisce come dovremmo usare le nuove funzionalità. Altre persone coinvolte nella progettazione contribuiscono alla SO - o lo stai facendo tu stesso? Seguirò le tue risposte sotto il tag java-8 - Mi piacerebbe sapere se ci sono altre persone che fanno lo stesso in modo da poterle seguire anche io.
Dannato

10
@Shorn Stuart Marks è stato attivo nel tag java-8. Jeremy Manson ha pubblicato in passato. Ricordo anche di aver visto i messaggi di Joshua Bloch ma non riesco a trovarli in questo momento.
Assylias,

1
Congratulazioni per aver escogitato metodi di interfaccia predefiniti come un modo molto più elegante di realizzare ciò che fa C # con i suoi metodi di estensione mal concepiti e piuttosto brutti implementati. Per quanto riguarda questa domanda, la risposta sull'impossibilità di risolvere un conflitto di nome in qualche modo risolve il problema, ma il resto dei motivi forniti erano filologia poco convincente. (Se voglio che un metodo di interfaccia sia definitivo, allora dovresti presumere che devo avere le mie dannatamente buone ragioni per voler impedire a chiunque di fornire qualsiasi implementazione diversa dalla mia.)
Mike Nakis,

1
Tuttavia, la risoluzione del conflitto di nomi avrebbe potuto essere raggiunta in un modo simile a come viene fatta in C #, aggiungendo il nome dell'interfaccia particolare che viene implementata alla dichiarazione del metodo di implementazione. Quindi, ciò che porto a casa da tutto ciò è che i metodi di interfaccia predefiniti non possono essere definitivi in ​​Java perché ciò avrebbe richiesto ulteriori modifiche alla sintassi, di cui non ti sei sentito bene. (Troppo nitido forse?)
Mike Nakis,

18
@ Trying Le classi astratte sono ancora l'unico modo per introdurre lo stato o implementare i metodi Object principali. I metodi predefiniti sono per puro comportamento ; le classi astratte sono per il comportamento associato allo stato.
Brian Goetz,

43

Nella mailing list lambda ci sono molte discussioni al riguardo . Uno di quelli che sembrano contenere molte discussioni su tutto ciò è il seguente: Sulla visibilità del metodo dell'interfaccia variegata (erano i difensori finali) .

In questa discussione, Talden, l'autore della domanda originale pone qualcosa di molto simile alla tua domanda:

La decisione di rendere pubblici tutti i membri dell'interfaccia è stata davvero una decisione sfortunata. Che qualsiasi uso dell'interfaccia nella progettazione interna esponga dettagli privati ​​dell'implementazione è un aspetto importante.

È difficile da correggere senza aggiungere sfumature oscure o di compatibilità alla lingua. Un'interruzione della compatibilità di tale entità e potenziale sottigliezza sarebbe inconcepibile, quindi deve esistere una soluzione che non violi il codice esistente.

Potrebbe essere possibile reintrodurre la parola chiave "pacchetto" come specificatore di accesso. L'assenza di uno specificatore in un'interfaccia implicherebbe l'accesso del pubblico e l'assenza di uno specificatore in una classe implica l'accesso al pacchetto. Quali specificatori abbiano senso in un'interfaccia non sono chiari, specialmente se, per ridurre al minimo l'onere della conoscenza per gli sviluppatori, dobbiamo assicurarci che gli specificatori di accesso significino la stessa cosa sia in classe che in interfaccia se sono presenti.

In assenza di metodi predefiniti avrei ipotizzato che l'identificatore di un membro in un'interfaccia deve essere almeno visibile come l'interfaccia stessa (quindi l'interfaccia può essere effettivamente implementata in tutti i contesti visibili) - con metodi predefiniti che non sono così certo.

C'è stata qualche chiara comunicazione sul fatto che si tratti di una possibile discussione nell'ambito? In caso contrario, dovrebbe essere tenuto altrove.

Alla fine la risposta di Brian Goetz è stata:

Sì, questo è già stato esplorato.

Tuttavia, lasciatemi impostare alcune aspettative realistiche: le funzionalità di linguaggio / VM hanno tempi di consegna lunghi, anche se apparentemente banali come questo. Il tempo per proporre idee per nuove funzionalità linguistiche per Java SE 8 è praticamente passato.

Quindi, molto probabilmente non è mai stato implementato perché non faceva mai parte del campo di applicazione. Non è mai stato proposto in tempo per essere considerato.

In un'altra accesa discussione sui metodi di difensore finale sull'argomento, Brian ha ripetuto :

E hai ottenuto esattamente quello che desideravi. Questo è esattamente ciò che aggiunge questa funzionalità: l'ereditarietà multipla del comportamento. Naturalmente capiamo che le persone li useranno come tratti. E abbiamo lavorato duramente per garantire che il modello di eredità che offrono sia abbastanza semplice e pulito da consentire alle persone di ottenere buoni risultati in un'ampia varietà di situazioni. Allo stesso tempo, abbiamo scelto di non spingerli oltre il limite di ciò che funziona in modo semplice e pulito, e ciò porta in alcuni casi a reazioni "aw, non sei andato abbastanza lontano". Ma in realtà, la maggior parte di questo filo sembra lamentarsi del fatto che il vetro è solo pieno al 98%. Prenderò quel 98% e andrò avanti!

Quindi questo rafforza la mia teoria che semplicemente non faceva parte dello scopo o parte del loro design. Quello che hanno fatto è stato fornire funzionalità sufficienti per affrontare i problemi dell'evoluzione delle API.


4
Vedo che avrei dovuto includere il vecchio nome, "metodi di difesa", nella mia odissea su Google questa mattina. +1 per scavare questo.
Marco13,

1
Bello scoprire i fatti storici. Le tue conclusioni sono in linea con la risposta ufficiale
Lukas Eder,

Non vedo perché si rompa la compatibilità all'indietro. finale non era permesso prima. ora consentono per privati, ma non protetti. myeh ... private non può implementare ... un'interfaccia che estende un'altra interfaccia potrebbe implementare parti di un'interfaccia padre, ma deve esporre la capacità di sovraccarico ad altri ...
mmm

17

Sarà difficile da trovare e identificare "la" risposta, per le resons menzionati nelle osservazioni di @EJP: Ci sono circa 2 (+/- 2) di persone nel mondo che possono dare la risposta definitiva a tutti . E nel dubbio, la risposta potrebbe essere qualcosa del tipo "Supportare i metodi predefiniti finali non sembra valere lo sforzo di ristrutturare i meccanismi interni di risoluzione delle chiamate". Questa è una speculazione, ovviamente, ma è almeno supportata da evidenti prove, come questa Dichiarazione (di una delle due persone) nella mailing list di OpenJDK :

"Suppongo che se fossero consentiti metodi" final default ", potrebbe essere necessario riscriverli dall'interfaccia interna all'invocazione visibile dall'utente."

e fatti banali come il fatto che un metodo non è semplicemente considerato un metodo (veramente) finale quando è un defaultmetodo, come attualmente implementato nel metodo Method :: is_final_method in OpenJDK.

Ulteriori informazioni veramente "autorevoli" sono davvero difficili da trovare, anche con ricerche web eccessive e leggendo i log di commit. Ho pensato che potrebbe essere correlato a potenziali ambiguità durante la risoluzione delle chiamate del metodo di interfaccia con l' invokeinterfaceistruzione e le chiamate al metodo di classe, corrispondenti invokevirtualall'istruzione: Per l' invokevirtualistruzione, potrebbe esserci una semplice ricerca vtable , poiché il metodo deve essere ereditato da una superclasse o implementato direttamente dalla classe. Al contrario, una invokeinterfacechiamata deve esaminare il rispettivo sito di chiamata per scoprire a quale interfaccia si riferisce effettivamente questa chiamata (questo è spiegato più dettagliatamente nella pagina InterfaceCalls del Wiki di HotSpot). Però,finali metodi non vengono affatto inseriti nella vtable , o sostituiscono le voci esistenti nella vtable (vedere klassVtable.cpp. Riga 333 ) e allo stesso modo, i metodi predefiniti sostituiscono le voci esistenti nella vtable (vedi klassVtable.cpp, Riga 202 ) . Quindi la ragione reale (e quindi la risposta) deve essere nascosta più in profondità all'interno dei (piuttosto complessi) meccanismi di risoluzione delle chiamate del metodo, ma forse questi riferimenti saranno comunque considerati utili, sia solo per gli altri che riescono a ricavare la risposta effettiva da quello.


Grazie per l'intuizione interessante. Il pezzo di John Rose è una traccia interessante. Tuttavia, non sono ancora d'accordo con @EJP. Come contro-esempio, dai un'occhiata alla mia risposta a una domanda molto interessante, molto simile, di Peter Lawrey . Si è possibile scavare fatti storici, e io sono sempre felice di trovare qui su Stack Overflow (dove altro?). Certo, la tua risposta è ancora speculativa, e non sono convinto al 100% che i dettagli dell'implementazione di JVM sarebbero il motivo finale (gioco di parole inteso) per la JLS da scrivere in un modo o nell'altro ...
Lukas Eder

@LukasEder Certo, questi tipi di domande sono interessanti e l'IMHO si adatta al modello di domande e risposte . Penso che ci siano due punti cruciali che hanno causato la disputa qui: il primo è che hai chiesto "il motivo". In molti casi ciò può non essere documentato ufficialmente . Ad esempio, non v'è alcuna "ragione" di cui i JLS perciò non ci sono senza segno ints, eppure vedere stackoverflow.com/questions/430346 ... ...
Marco13

... ... Il secondo è che hai chiesto solo "citazioni autorevoli", che riduce il numero di persone che hanno il coraggio di scrivere una risposta da "poche decine" a ... "approssimativamente zero". A parte questo, non sono sicuro di come si intrecciano lo sviluppo della JVM e la scrittura della JLS, cioè in che misura lo sviluppo influenza ciò che è scritto nella JLS, ma ... Eviterò qui qualsiasi speculazione; -)
Marco13,

1
Riposo ancora il mio caso. Vedere chi ha risposto alla mia altra domanda :-) Ora sarà per sempre chiaro con una risposta autorevole, qui su Stack Overflow per questo si è deciso di non sostenere synchronizedil defaultmetodo.
Lukas Eder,

3
@LukasEder vedo, lo stesso qui. Chi se lo sarebbe aspettato? Le ragioni sono piuttosto convincenti, in particolare per questa finaldomanda, ed è piuttosto umiliante che nessun altro abbia pensato a esempi simili (o, forse, alcuni hanno pensato a questi esempi, ma non si sono sentiti abbastanza autorevoli per rispondere). Quindi ora la (scusa, devo fare questo :) viene pronunciata l' ultima parola.
Marco13,

4

Non credo che sia necessario specificare finalun metodo di interfaccia di convivialità, posso concordare sul fatto che può essere utile, ma a quanto pare i costi hanno superato i benefici.

Ciò che dovresti fare, in entrambi i casi, è scrivere javadoc corretto per il metodo predefinito, mostrando esattamente quale sia il metodo e non è consentito farlo. In questo modo le classi che implementano l'interfaccia "non sono autorizzate" a cambiare l'implementazione, sebbene non ci siano garanzie.

Chiunque potrebbe scrivere un oggetto Collectionche aderisce all'interfaccia e quindi fare cose con metodi assolutamente controintuitivi, non c'è modo di proteggersi da questo, se non quello di scrivere test unitari approfonditi.


2
I contratti Javadoc sono una valida soluzione all'esempio concreto che ho elencato nella mia domanda, ma la domanda in realtà non riguarda il caso d'uso del metodo dell'interfaccia di convenienza. La domanda riguarda un motivo autorevole per cui finalè stato deciso di non consentire i interfacemetodi Java 8 . Un rapporto costi / benefici insufficiente è un buon candidato, ma finora è una speculazione.
Lukas Eder,

0

Aggiungiamo una defaultparola chiave al nostro metodo all'interno di un interfacequando sappiamo che la classe che estende interfaceo meno la overridenostra implementazione. E se volessimo aggiungere un metodo che non vogliamo che nessuna classe di implementazione abbia la precedenza? Bene, due opzioni erano disponibili per noi:

  1. Aggiungi un default finalmetodo
  2. Aggiungi un staticmetodo

Ora, Java afferma che se abbiamo classdue o più implementazioni interfacestali da avere un defaultmetodo con esattamente lo stesso nome e firma del metodo, cioè che sono duplicati, allora dobbiamo fornire un'implementazione di quel metodo nella nostra classe. Ora, nel caso dei default finalmetodi, non possiamo fornire un'implementazione e siamo bloccati. Ed è per questo che la finalparola chiave non viene utilizzata nelle interfacce.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.