Perché preferire le classi interne non statiche rispetto a quelle statiche?


37

Questa domanda riguarda se fare in modo che una classe nidificata in Java sia una classe nidificata statica o una classe nidificata interna. Ho cercato qui e su Stack Overflow, ma non sono riuscito a trovare alcuna domanda in merito alle implicazioni di progettazione di questa decisione.

Le domande che ho trovato pongono sulla differenza tra classi annidate statiche e interne, il che mi è chiaro. Tuttavia, non ho ancora trovato un motivo convincente per usare mai una classe annidata statica in Java, ad eccezione delle classi anonime, che non considero per questa domanda.

Ecco la mia comprensione dell'effetto dell'utilizzo di classi nidificate statiche:

  • Meno accoppiamento: generalmente si ottiene meno accoppiamento, poiché la classe non può accedere direttamente agli attributi della sua classe esterna. Meno accoppiamento generalmente significa migliore qualità del codice, test più facili, refactoring, ecc.
  • Singolo Class: il caricatore di classi non deve occuparsi di una nuova classe ogni volta, creiamo un oggetto della classe esterna. Riceviamo sempre nuovi oggetti per la stessa classe ancora e ancora.

Per una classe interna, generalmente trovo che le persone considerino un professionista l'accesso agli attributi della classe esterna. Mi permetto di differire da questo punto di vista dal punto di vista del design, poiché questo accesso diretto significa che abbiamo un elevato accoppiamento e se vogliamo mai estrarre la classe nidificata nella sua classe di livello superiore separata, possiamo farlo solo dopo aver essenzialmente girato in una classe nidificata statica.

Quindi la mia domanda si riduce a questo: sbaglio nel presumere che l'accesso all'attributo disponibile alle classi interne non statiche porti a un elevato accoppiamento, quindi a una qualità del codice inferiore, e che deduco da ciò che le classi nidificate (non anonime) dovrebbero generalmente essere statico?

O in altre parole: c'è una ragione convincente per cui si preferirebbe una classe interiore nidificata?


2
dal tutorial Java : "Utilizzare una classe nidificata non statica (o classe interna) se si richiede l'accesso ai campi e ai metodi non pubblici di un'istanza che racchiude. Utilizzare una classe nidificata statica se non si richiede questo accesso."
moscerino del

5
Grazie per la citazione del tutorial, ma questo ripete qual è la differenza, non il motivo per cui si vorrebbe accedere ai campi dell'istanza.
Frank,

è piuttosto una guida generale, suggerendo cosa preferire. Ho aggiunto una spiegazione più dettagliata su come la interpreto in questa risposta
moscerino del

1
Se la classe interna non è strettamente accoppiata alla classe chiusa, probabilmente non dovrebbe essere una classe interna in primo luogo.
Kevin Krumwiede,

Risposte:


23

Joshua Bloch nell'articolo 22 del suo libro "Effective Java Second Edition" spiega quando usare quale tipo di classe nidificata e perché. Di seguito alcune citazioni:

Un uso comune di una classe membro statica è come una classe di supporto pubblica, utile solo in combinazione con la sua classe esterna. Ad esempio, si consideri un enum che descrive le operazioni supportate da una calcolatrice. L'enum Operazione dovrebbe essere una classe pubblica statica della Calculatorclasse. I clienti di Calculatorpotrebbero quindi fare riferimento alle operazioni utilizzando nomi come Calculator.Operation.PLUSe Calculator.Operation.MINUS.

Un uso comune di una classe membro non statica è la definizione di un adattatore che consenta a un'istanza della classe esterna di essere vista come un'istanza di una classe non correlata. Ad esempio, le implementazioni Mapdell'interfaccia utilizzano in genere classi di membri non statiche per implementare le loro viste di raccolta , che vengono restituite daMap 's keySet, entrySete valuesmetodi. Allo stesso modo, le implementazioni delle interfacce di raccolta, come Sete List, in genere, utilizzano classi membro non statiche per implementare i loro iteratori:

// Typical use of a nonstatic member class
public class MySet<E> extends AbstractSet<E> {
    ... // Bulk of the class omitted

    public Iterator<E> iterator() {
        return new MyIterator();
    }

    private class MyIterator implements Iterator<E> {
        ...
    }
}

Se si dichiara una classe membro che non richiede l'accesso a un'istanza inclusa, sempre mettere il staticmodificatore nella sua dichiarazione, il che rende una statica piuttosto che una classe membro non statico.


1
Non sono sicuro di come / se questo adattatore cambi la vista delle MySetistanze di classe esterne ? Potrei immaginare che qualcosa come un tipo dipendente dal percorso sia una differenza, cioè myOneSet.iterator.getClass() != myOtherSet.iterator.getClass(), ma di nuovo, in Java questo non è effettivamente possibile, poiché la classe sarebbe la stessa per ciascuno.
Frank,

@Frank È una classe adattatore piuttosto tipica: ti consente di visualizzare il set come un flusso dei suoi elementi (un Iteratore).
user253751,

@immibis grazie, sono ben consapevole di ciò che fa l'iteratore / adattatore, ma questa domanda era su come è implementato.
Frank,

@Frank Stavo dicendo come cambia la vista dell'istanza esterna. Questo è esattamente ciò che fa un adattatore: cambia il modo in cui vedi un oggetto. In questo caso, quell'oggetto è l'istanza esterna.
user253751,

10

Hai ragione nel ritenere che l'accesso all'attributo disponibile alle classi interne non statiche porti a un elevato accoppiamento, quindi a una qualità del codice inferiore e (non anonimo e classi interne non locali ) dovrebbero essere generalmente statiche.

Le implicazioni progettuali della decisione di rendere la classe interna non statica sono illustrate in Java Puzzlers , Puzzle 90 (il carattere in grassetto nella citazione seguente è mio):

Ogni volta che scrivi una classe membro, chiediti: questa classe ha davvero bisogno di un'istanza chiusa? Se la risposta è no, fallo static. Le classi interne sono talvolta utili, ma possono facilmente introdurre complicazioni che rendono difficile la comprensione di un programma. Hanno interazioni complesse con generici (Puzzle 89), riflessione (Puzzle 80) ed eredità (questo puzzle) . Se dichiari Inner1di esserlo static, il problema scompare. Se anche tu dichiari Inner2di essere static, puoi effettivamente capire cosa fa il programma: un bel bonus davvero.

In breve, è raramente appropriato che una classe sia sia una classe interna che una sottoclasse di un'altra. Più in generale, è raramente appropriato estendere una classe interna; se è necessario, riflettere a lungo sull'istanza che la contiene. Inoltre, preferisci staticle classi nidificate a non static. La maggior parte delle classi membro può e deve essere dichiarata static.

Se sei interessato, un'analisi più dettagliata di Puzzle 90 è fornita in questa risposta su Stack Overflow .


Vale la pena notare che sopra è essenzialmente una versione estesa della guida fornita nel tutorial Classi e oggetti Java :

Utilizzare una classe nidificata non statica (o classe interna) se si richiede l'accesso ai campi e ai metodi non pubblici di un'istanza che lo racchiude. Utilizzare una classe nidificata statica se non si richiede questo accesso.

Quindi, la risposta alla domanda che hai posto in altre parole per tutorial è, l'unica ragione convincente per usare non statico è quando l'accesso ai campi e ai metodi non pubblici di un'istanza che racchiude è necessari .

La formulazione del tutorial è piuttosto ampia (questo potrebbe essere il motivo per cui i puzzle di Java fanno un tentativo di rafforzarlo e restringerlo). In particolare, l'accesso diretto ai campi di istanza racchiusi non è mai stato realmente richiesto nella mia esperienza nel senso che modi alternativi come passare questi come parametri di costruzione / metodo sono sempre diventati più facili da eseguire per il debug e la manutenzione.


Nel complesso, i miei (piuttosto dolorosi) incontri con le classi interne di debug che accedono direttamente ai campi dell'istanza chiusa hanno fatto una forte impressione che questa pratica assomigli all'uso dello stato globale, insieme ai mali noti ad essa associati.

Certo, Java fa in modo che il danno di un tale "quasi globale" sia contenuto all'interno di una classe chiusa, ma quando ho dovuto eseguire il debug di una particolare classe interna, mi è sembrato che un tale aiuto di fascia non aiutasse a ridurre il dolore: avevo ancora tenere presente la semantica e i dettagli "estranei" anziché concentrarsi completamente sull'analisi di un particolare oggetto problematico.


Per completezza, potrebbero esserci casi in cui il ragionamento di cui sopra non si applica. Ad esempio, secondo la mia lettura di map.keySet javadocs , questa funzione suggerisce un accoppiamento stretto e, di conseguenza, invalida gli argomenti rispetto alle classi non statiche:

Restituisce a Set vista delle chiavi contenute in questa mappa. Il set è supportato dalla mappa, quindi le modifiche alla mappa si riflettono nel set e viceversa ...

Non che quanto sopra renderebbe in qualche modo il codice coinvolto più facile da mantenere, testare ed eseguire il debug, ma almeno potrebbe consentire di sostenere che le complicazioni corrispondano / siano giustificate dalla funzionalità prevista.


Condivido la vostra esperienza sui problemi causati da questo accoppiamento alto, ma io non sto bene con l'uso del tutorial richiedono . Dici tu stesso che non hai mai veramente richiesto l'accesso sul campo, quindi purtroppo anche questo non risponde completamente alla mia domanda.
Frank,

@Frank come puoi vedere, la mia interpretazione della guida tutorial (che sembra essere supportata da Java Puzzlers) è come, usare classi non statiche solo quando puoi provare che questo è richiesto, e in particolare, dimostrare che modi alternativi sono peggio . Vale la pena notare che nella mia esperienza i modi alternativi si sono sempre rivelati migliori
moscerino del

Questo è il punto. Se è sempre meglio, allora perché preoccuparci?
Frank,

@Frank un'altra risposta fornisce esempi convincenti che non è sempre così. Secondo la mia lettura di map.keySet javadocs , questa funzione suggerisce un accoppiamento stretto e, di conseguenza, invalida gli argomenti rispetto alle classi non statiche "L'insieme è supportato dalla mappa, quindi le modifiche alla mappa si riflettono nell'insieme e viceversa ... "
moscerino del

1

Alcune idee sbagliate:

Meno accoppiamento: generalmente si ottiene meno accoppiamento, poiché la classe [statica interna] non può accedere direttamente agli attributi della sua classe esterna.

IIRC - In realtà, può. (Ovviamente se si tratta di campi oggetto, non avrà un oggetto esterno da guardare. Tuttavia, se ottiene tale oggetto da qualsiasi luogo, allora può accedere direttamente a tali campi).

Singola classe: il caricatore di classi non deve occuparsi di una nuova classe ogni volta, creiamo un oggetto della classe esterna. Riceviamo sempre nuovi oggetti per la stessa classe ancora e ancora.

Non sono del tutto sicuro di cosa intendi qui. Il caricatore di classi è coinvolto solo due volte in totale, una volta per ogni classe (quando la classe è caricata).


Il gioco segue:

In Javaland sembra che abbiamo un po 'paura di creare lezioni. Penso che debba sembrare un concetto significativo. Appartiene generalmente al suo stesso file. Ha bisogno di un significativo boilerplate. Ha bisogno di attenzione al suo design.

Contro altri linguaggi - ad esempio Scala, o forse C ++: non c'è assolutamente nessun dramma che crea un piccolo supporto (classe, struttura, qualunque cosa) ogni volta che due bit di dati si uniscono. Le regole di accesso flessibili aiutano a ridurre la piastra della caldaia, consentendo di mantenere fisicamente il codice correlato più vicino. Ciò riduce la "distanza mentale" tra le due classi.

Possiamo evitare le preoccupazioni di progettazione sull'accesso diretto, mantenendo la classe interna privata.

(... Se avessi chiesto "perché un pubblico non statico classe interna ?", Questa è un'ottima domanda - non credo di aver ancora trovato un caso giustificato per quelli).

Puoi usarli ogni volta che aiuta con la leggibilità del codice ed evitando violazioni DRY e boilerplate. Non ho un esempio pronto perché non succede così spesso nella mia esperienza, ma succede.


Le classi interne statiche sono ancora un buon approccio predefinito , a proposito. Non statico ha un campo nascosto aggiuntivo generato attraverso il quale la classe interna si riferisce a quella esterna.


0

Può essere utilizzato per creare un modello di fabbrica. Un modello di fabbrica viene spesso utilizzato quando gli oggetti creati richiedono parametri costruttori aggiuntivi per funzionare, ma sono noiosi da fornire ogni volta:

Ritenere:

public class QueryFactory {
    @Inject private Database database;

    public Query create(String sql) {
        return new Query(database, sql);
    }
}

public class Query {
    private final Database database;
    private final String sql;

    public Query(Database database, String sql) {
        this.database = database;
        this.sql = sql;
    } 

    public List performQuery() {
        return database.query(sql);
    }
}

Usato come:

Query q = queryFactory.create("SELECT * FROM employees");

q.performQuery();

La stessa cosa si potrebbe ottenere con una classe interna non statica:

public class QueryFactory {
    @Inject private Database database;

    public class Query {
        private final String sql;

        public Query(String sql) {
            this.sql = sql;
        }

        public List performQuery() {
            return database.query(sql);
        }
    }
}

Usare come:

Query q = queryFactory.new Query("SELECT * FROM employees");

q.performQuery();

Le fabbriche sono interessati ad avere i metodi indicati in modo specifico, come create, createFromSQLo createFromTemplate. Lo stesso può essere fatto anche qui sotto forma di più classi interne:

public class QueryFactory {

    public class SQL { ... }
    public class Native { ... }
    public class Builder { ... }

}

È necessario meno passaggio di parametri dalla fabbrica alla classe costruita, ma la leggibilità potrebbe risentirne un po '.

Confrontare:

Queries.Native q = queries.new Native("select * from employees");

vs

Query q = queryFactory.createNative("select * from employees");
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.