Warum lieber nicht statische innere Klassen als statische?

37

Bei dieser Frage geht es darum, ob eine verschachtelte Klasse in Java eine statische verschachtelte Klasse oder eine innere verschachtelte Klasse sein soll. Ich habe hier und auf Stack Overflow gesucht, konnte aber keine wirklichen Fragen zu den Auswirkungen dieser Entscheidung auf das Design finden.

Die Fragen, die ich gefunden habe, beziehen sich auf den Unterschied zwischen statischen und inneren verschachtelten Klassen, was mir klar ist. Ich habe jedoch noch keinen überzeugenden Grund gefunden, jemals eine statische verschachtelte Klasse in Java zu verwenden - mit Ausnahme von anonymen Klassen, die ich für diese Frage nicht berücksichtige.

Hier ist mein Verständnis der Auswirkungen der Verwendung statisch verschachtelter Klassen:

  • Weniger Kopplung: Im Allgemeinen erhalten wir weniger Kopplung, da die Klasse nicht direkt auf die Attribute ihrer äußeren Klasse zugreifen kann. Weniger Kopplung bedeutet im Allgemeinen eine bessere Codequalität, einfacheres Testen, Refactoring usw.
  • Single Class: Der Klassenlader muss sich nicht jedes Mal um eine neue Klasse kümmern, sondern erstellt ein Objekt der äußeren Klasse. Wir bekommen immer wieder neue Objekte für die gleiche Klasse.

Für eine innere Klasse empfinde ich den Zugang zu den Attributen der äußeren Klasse im Allgemeinen als einen Vorteil. Ich möchte diesbezüglich einen konstruktiven Unterschied machen, da dieser direkte Zugriff eine hohe Kopplung bedeutet. Wenn wir die verschachtelte Klasse jemals in eine separate Klasse der obersten Ebene extrahieren möchten, können wir dies erst tun, nachdem wir uns im Wesentlichen umgedreht haben es in eine statische verschachtelte Klasse.

Meine Frage lautet also: Liege ich falsch in der Annahme, dass der für nicht statische innere Klassen verfügbare Attributzugriff zu einer hohen Kopplung und damit zu einer geringeren Codequalität führt und dass ich daraus schließen sollte, dass (nicht anonyme) verschachtelte Klassen vorhanden sind in der Regel statisch sein?

Oder mit anderen Worten: Gibt es einen überzeugenden Grund, warum man eine verschachtelte innere Klasse bevorzugen würde?

Frank
quelle
2
aus dem Java-Tutorial : "Verwenden Sie eine nicht statisch verschachtelte Klasse (oder eine innere Klasse), wenn Sie Zugriff auf die nicht öffentlichen Felder und Methoden einer umschließenden Instanz benötigen. Verwenden Sie eine statisch verschachtelte Klasse, wenn Sie diesen Zugriff nicht benötigen."
gnat
5
Vielen Dank für das Tutorial-Zitat, aber dies wiederholt nur den Unterschied und nicht, warum Sie auf die Instanzfelder zugreifen möchten.
Frank
Es handelt sich eher um eine allgemeine Anleitung, die angibt, was Sie bevorzugen. Ich habe eine ausführlichere Erklärung, wie ich es in interpretieren diese Antwort
gnat
1
Wenn die innere Klasse nicht eng mit der einschließenden Klasse verbunden ist, sollte es wahrscheinlich überhaupt keine innere Klasse sein.
Kevin Krumwiede

Antworten:

23

Joshua Bloch in Punkt 22 seines Buches "Effective Java Second Edition" erklärt, wann welche Art von verschachtelter Klasse zu verwenden ist und warum. Es gibt einige Anführungszeichen unten:

Eine häufig verwendete Verwendung einer statischen Memberklasse ist eine öffentliche Hilfsklasse, die nur in Verbindung mit ihrer äußeren Klasse nützlich ist. Betrachten Sie beispielsweise eine Aufzählung, die die von einem Taschenrechner unterstützten Vorgänge beschreibt. Die Operation enum sollte eine öffentliche statische Memberklasse der CalculatorKlasse sein. Clients von Calculatorkönnten dann mit Namen wie Calculator.Operation.PLUSund auf Operationen verweisen Calculator.Operation.MINUS.

Eine gebräuchliche Verwendung einer nicht statischen Memberklasse besteht darin, einen Adapter zu definieren , mit dem eine Instanz der äußeren Klasse als eine Instanz einer nicht verwandten Klasse betrachtet werden kann. Beispielsweise verwenden Implementierungen der MapSchnittstelle in der Regel nicht statische Memberklassen, um ihre Auflistungsansichten , die von Maps zurückgegeben keySetwerden entrySet, und valuesMethoden zu implementieren . In ähnlicher Weise verwenden Implementierungen der Auflistungsschnittstellen, z. B. Setund List, normalerweise nicht-statische Member-Klassen, um ihre Iteratoren zu implementieren:

// 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> {
        ...
    }
}

Wenn Sie ein Mitglied der Klasse deklarieren , die keinen Zugang zu einer umschließenden Instanz erfordern, immer setzt den staticModifikator in seiner Erklärung, es ist eine statisches machte eher als eine nicht statische Elementklasse.

Erakitin
quelle
1
Ich bin nicht ganz sicher, wie / ob dieser Adapter die Ansicht der äußeren Klasseninstanzen ändert MySet? Ich könnte mir vorstellen, dass so etwas wie ein pfadabhängiger Typ ein Unterschied ist myOneSet.iterator.getClass() != myOtherSet.iterator.getClass(), aber in Java ist dies eigentlich nicht möglich, da die Klasse für beide gleich wäre.
Frank
@Frank Dies ist eine recht typische Adapterklasse. Mit dieser Klasse können Sie die Menge als Stream ihrer Elemente (als Iterator) anzeigen.
user253751
@immibis Dank, bin ich sehr wohl bewusst , was der Iterator / Adapter der Fall ist, aber diese Frage war etwa , wie es umgesetzt wird.
Frank
@Frank Ich sagte, wie es die Ansicht der äußeren Instanz ändert. Genau das macht ein Adapter - er verändert die Art und Weise, wie Sie ein Objekt anzeigen. In diesem Fall ist das Objekt die äußere Instanz.
user253751
10

Sie gehen zu Recht davon aus, dass der für nicht statische innere Klassen verfügbare Attributzugriff zu einer hohen Kopplung und damit zu einer geringeren Codequalität führt und (nicht anonyme und nicht lokale ) innere Klassen im Allgemeinen statisch sein sollten.

Die Auswirkungen der Entscheidung, die innere Klasse nicht statisch zu machen, auf das Design sind in Java Puzzlers , Puzzle 90 (die fett gedruckte Schrift im folgenden Zitat stammt von mir) aufgeführt:

Wenn Sie eine Member-Klasse schreiben, fragen Sie sich, ob diese Klasse wirklich eine einschließende Instanz benötigt. Wenn die Antwort nein ist, machen Sie es static. Innere Klassen sind manchmal nützlich, aber sie können leicht zu Komplikationen führen, die das Verständnis eines Programms erschweren. Sie haben komplexe Wechselwirkungen mit Generika (Puzzle 89), Reflexion (Puzzle 80) und Vererbung (dieses Puzzle) . Wenn Sie dies angeben Inner1, verschwindet staticdas Problem. Wenn Sie dies auch erklären Inner2, können staticSie tatsächlich verstehen, was das Programm tut: ein netter Bonus in der Tat.

Zusammenfassend ist es selten angebracht, dass eine Klasse sowohl eine innere Klasse als auch eine Unterklasse einer anderen Klasse ist. Generell ist es selten angebracht, eine innere Klasse zu erweitern. Wenn Sie müssen, denken Sie lange und gründlich über die beiliegende Instanz nach. Ziehen Sie außerdem staticverschachtelte Klassen Nicht-Klassen vor static. Die meisten Mitgliederklassen können und sollten deklariert werden static.

Wenn Sie interessiert sind, finden Sie in dieser Antwort unter Stapelüberlauf eine detailliertere Analyse von Puzzle 90 .


Es ist erwähnenswert, dass es sich bei der obigen Beschreibung im Wesentlichen um eine erweiterte Version der Anleitung handelt, die im Lernprogramm für Java- Klassen und -Objekte enthalten ist :

Verwenden Sie eine nicht statische verschachtelte Klasse (oder innere Klasse), wenn Sie Zugriff auf die nicht öffentlichen Felder und Methoden einer einschließenden Instanz benötigen. Verwenden Sie eine statische verschachtelte Klasse, wenn Sie diesen Zugriff nicht benötigen.

Die Antwort auf die Frage, die Sie in anderen Worten pro Lernprogramm gestellt haben, ist der einzige überzeugende Grund für die Verwendung von nicht statisch, wenn der Zugriff auf die nicht öffentlichen Felder und Methoden einer einschließenden Instanz erforderlich ist .

Der Wortlaut des Tutorials ist ziemlich weit gefasst (dies könnte der Grund sein, warum Java-Puzzler versuchen, es zu stärken und einzugrenzen). Insbesondere der direkte Zugriff auf umschließende Instanzfelder war nach meiner Erfahrung nie wirklich erforderlich - in dem Sinne, dass alternative Methoden wie die Übergabe als Konstruktor- / Methodenparameter immer einfacher zu debuggen und zu warten waren.


Insgesamt machte meine (ziemlich schmerzhafte) Begegnung mit dem Debuggen innerer Klassen, die direkt auf Felder umschließender Instanzen zugreifen, den starken Eindruck, dass diese Praxis der Verwendung des globalen Staates und der damit verbundenen bekannten Übel ähnelt .

Natürlich macht Java es so, dass der Schaden einer solchen "quasi globalen" Klasse in der einschließenden Klasse enthalten ist, aber als ich eine bestimmte innere Klasse ausprüfen musste, fühlte es sich so an, als würde eine solche Pflasterung nicht dazu beitragen, die Schmerzen zu lindern "fremde" Semantik und Details im Auge zu behalten, anstatt sich ganz auf die Analyse eines bestimmten störenden Objekts zu konzentrieren.


Der Vollständigkeit halber kann es Fälle geben, in denen die obigen Überlegungen nicht zutreffen. Beispielsweise deutet diese Funktion beim Lesen von " map.keySet" -Javadocs auf eine enge Kopplung hin und macht daher Argumente gegen nicht statische Klassen ungültig:

Gibt eine SetAnsicht der in dieser Map enthaltenen Schlüssel zurück. Das Set wird von der Karte unterstützt, sodass Änderungen an der Karte in das Set übernommen werden und umgekehrt.

Nicht das obige würde es irgendwie einfacher machen, den beteiligten Code zu warten, zu testen und zu debuggen, aber es könnte einem zumindest erlauben zu argumentieren, dass die Komplikation mit der beabsichtigten Funktionalität übereinstimmt / gerechtfertigt ist.

Mücke
quelle
Ich teile Ihre Erfahrungen mit den Problemen, die durch diese hohe Kopplung verursacht werden, aber ich bin mit der Verwendung von require im Tutorial nicht einverstanden . Sie sagen selbst, dass Sie den Feldzugang nie wirklich benötigt haben, so dass dies leider auch meine Frage nicht vollständig beantwortet.
Frank
@Frank Wie Sie sehen, ist meine Interpretation der Tutorial-Anleitung (die anscheinend von Java Puzzlers unterstützt wird) so, dass Sie nicht-statische Klassen nur dann verwenden, wenn Sie nachweisen können, dass dies erforderlich ist, und insbesondere nachweisen, dass es alternative Möglichkeiten gibt schlimmer . Erwähnenswert, dass in meiner Erfahrung alternative Möglichkeiten immer besser stellte sich heraus
gnat
Das ist der Punkt. Wenn es immer besser ist, warum stören wir uns dann?
Frank
@Frank Eine andere Antwort liefert überzeugende Beispiele dafür, dass dies nicht immer so ist. Nach meiner Lektüre von map.keySet javadocs deutet diese Funktion auf eine enge Kopplung hin und macht daher Argumente gegen nicht statische Klassen ungültig ...“
gnat
1

Einige Missverständnisse:

Weniger Kopplung: Im Allgemeinen erhalten wir weniger Kopplung, da die [statische innere] Klasse nicht direkt auf die Attribute ihrer äußeren Klasse zugreifen kann.

IIRC - Eigentlich kann es. (Wenn es sich natürlich um Objektfelder handelt, muss kein äußeres Objekt betrachtet werden. Wenn es jedoch ein solches Objekt von einer beliebigen Stelle erhält, kann es direkt auf diese Felder zugreifen.)

Einzelne Klasse: Der Klassenlader muss sich nicht jedes Mal um eine neue Klasse kümmern, sondern erstellt ein Objekt der äußeren Klasse. Wir bekommen immer wieder neue Objekte für die gleiche Klasse.

Ich bin mir nicht ganz sicher, was du hier meinst. Der Klassenlader ist insgesamt nur zweimal beteiligt, einmal für jede Klasse (wenn die Klasse geladen ist).


Wandern folgt:

In Javaland scheinen wir ein bisschen Angst zu haben, Klassen zu schaffen. Ich denke, es muss sich wie ein wichtiges Konzept anfühlen. Es gehört in der Regel in eine eigene Datei. Es braucht eine signifikante Kesselplatte. Es muss auf sein Design geachtet werden.

Gegen andere Sprachen - zB Scala oder vielleicht C ++: Es gibt absolut kein Drama, das einen winzigen Halter (Klasse, Struktur, was auch immer) schafft, wenn zwei Datenbits zusammengehören. Flexible Zugriffsregeln helfen Ihnen dabei, die Heizplatte zu verkleinern und den zugehörigen Code physisch näher zu halten. Dies verringert Ihre "Gedankendistanz" zwischen den beiden Klassen.

Wir können Design-Bedenken in Bezug auf den direkten Zugang abwenden, indem wir die innere Klasse privat halten.

(... Wenn Sie gefragt hätten, warum eine nicht-statische öffentliche innere Klasse?)

Sie können sie jederzeit verwenden, um die Lesbarkeit des Codes zu verbessern und DRY-Verstöße und Boilerplate zu vermeiden. Ich habe kein fertiges Beispiel, weil es meiner Erfahrung nach nicht allzu oft vorkommt, aber es passiert.


Statische innere Klassen sind übrigens immer noch ein guter Standardansatz . Nicht statisch wird ein zusätzliches verstecktes Feld generiert, durch das sich die innere Klasse auf die äußere bezieht.

Iain
quelle
0

Es kann verwendet werden, um ein Factory-Muster zu erstellen. Ein Factory-Muster wird häufig verwendet, wenn die erstellten Objekte zusätzliche Konstruktorparameter benötigen, um zu funktionieren.

Erwägen:

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);
    }
}

Benutzt als:

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

q.performQuery();

Dasselbe könnte mit einer nicht statischen inneren Klasse erreicht werden:

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);
        }
    }
}

Benutzen als:

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

q.performQuery();

Fabriken profitieren von speziell genannten Verfahren mit, wie create, createFromSQLoder createFromTemplate. Dasselbe kann auch hier in Form mehrerer innerer Klassen geschehen:

public class QueryFactory {

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

}

Es ist weniger Parameterübergabe von der Fabrik zur konstruierten Klasse erforderlich, aber die Lesbarkeit kann ein wenig leiden.

Vergleichen Sie:

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

vs

Query q = queryFactory.createNative("select * from employees");
john16384
quelle