Hallo habr Ich präsentiere Ihnen die Übersetzung des Artikels "
5 Hidden Secrets in Java " von
Justin Albano .
Willst du ein Java Jedi werden? Lüfte die alten Geheimnisse von Java. Wir werden uns auf das Erweitern von Annotationen, Initialisierungen, Kommentaren und Enum-Schnittstellen konzentrieren.
Mit der Entwicklung der Programmiersprachen tauchen auch versteckte Funktionen auf, und Konstruktionen, an die die Gründer nie gedacht hatten, werden für den allgemeinen Gebrauch immer weiter verbreitet. Einige dieser Funktionen werden in der Sprache allgemein akzeptiert, während andere in die dunkelsten Winkel der Sprachgemeinschaft rücken. In diesem Artikel werden wir uns fünf Geheimnisse ansehen, die von vielen Java-Entwicklern oft übersehen werden (fairerweise haben einige gute Gründe dafür). Wir betrachten sowohl die Verwendungsmöglichkeiten als auch die Gründe, die zum Erscheinen der einzelnen Funktionen geführt haben, sowie einige Beispiele, die zeigen, wann es ratsam ist, diese Funktionen zu verwenden.
Der Leser sollte verstehen, dass nicht alle diese Funktionen tatsächlich verborgen sind, sie werden im alltäglichen Programmieren einfach nicht oft verwendet. Einige von ihnen können zum richtigen Zeitpunkt sehr nützlich sein, während die Verwendung anderer fast immer eine schlechte Idee ist, und sie werden in diesem Artikel gezeigt, um den Leser zu interessieren (und ihn oder sie möglicherweise zum Lachen zu bringen). Der Leser muss auch entscheiden, wann die in diesem Artikel beschriebenen Funktionen verwendet werden sollen: "Die Tatsache, dass dies möglich ist, bedeutet nicht, dass dies erforderlich ist."
1. Implementieren Sie Anmerkungen
Ab dem Java Development Kit (JDK) 5 sind Anmerkungen ein wesentlicher Bestandteil vieler Java-Anwendungen und -Umgebungen. In den allermeisten Fällen beziehen sich Anmerkungen auf Konstrukte wie Klassen, Felder, Methoden usw. Sie können jedoch auch als implementierte Schnittstellen verwendet werden. Angenommen, wir haben die folgende Annotationsdefinition:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Test { String name(); }
Wir wenden diese Annotation normalerweise auf eine der folgenden Methoden an:
public class MyTestFixure { @Test public void givenFooWhenBarThenBaz() {
Anschließend können wir diese Annotation wie unter
Erstellen von Annotationen in Java beschrieben verarbeiten . Wenn wir auch eine Schnittstelle erstellen möchten, mit der wir Tests als Objekte erstellen können, müssen wir eine neue Schnittstelle erstellen, die etwas anderes nennt, und nicht Test:
public interface TestInstance { public String getName(); }
Als Nächstes können wir eine Instanz des TestInstance-Objekts erstellen:
public class FooTestInstance implements TestInstance { @Override public String getName() { return "Foo"; } } TestInstance myTest = new FooTestInstance();
Obwohl unsere Annotation und Benutzeroberfläche nahezu identisch sind, scheint es keine Möglichkeit zu geben, diese beiden Konstrukte zu kombinieren. Glücklicherweise täuscht der Schein, und es gibt eine Methode zum Kombinieren dieser beiden Konstrukte: Implementieren von Anmerkungen:
public class FooTest implements Test { @Override public String name() { return "Foo"; } @Override public Class<? extends Annotation> annotationType() { return Test.class; } }
Beachten Sie, dass wir die annotationType-Methode implementieren und auch den Annotationstyp zurückgeben müssen, da dies ein impliziter Teil der Annotation-Schnittstelle ist. Obwohl in fast allen Fällen die Implementierung von Annotationen nicht die richtige Lösung für das Design ist (der Java-Compiler zeigt bei der Implementierung der Schnittstelle eine Warnung an), kann dies in einigen Fällen hilfreich sein, z. B. im annotationsgesteuerten Framework.
2. Nicht statische Initialisierungsblöcke.
In Java werden Objekte wie in den meisten objektorientierten Programmiersprachen ausschließlich mit dem Konstruktor erstellt (mit einigen Ausnahmen, z. B. beim Deserialisieren von Java-Objekten). Selbst wenn wir statische Factory-Methoden zum Erstellen von Objekten erstellen, fügen wir einfach einen Aufruf in den Konstruktor des Objekts ein, um es zu instanziieren. Zum Beispiel:
public class Foo { private final String name; private Foo(String name) { this.name = name; } public static Foo withName(String name) { return new Foo(name); } } Foo foo = Foo.withName("Bar");
Wenn wir ein Objekt initialisieren möchten, kombinieren wir daher die Initialisierungslogik im Konstruktor des Objekts. Zum Beispiel setzen wir das Namensfeld der Foo-Klasse in ihrem parametrisierten Konstruktor. Obwohl anzunehmen scheint, dass sich die gesamte Initialisierungslogik im Konstruktor oder in der Gruppe von Konstruktoren für die Klasse befindet, ist dies in Java nicht der Fall. Stattdessen können wir beim Erstellen eines Objekts
nicht statische Initialisierungsblöcke verwenden , um Code auszuführen:
public class Foo { { System.out.println("Foo:instance 1"); } public Foo() { System.out.println("Foo:constructor"); } }
Nicht statische Initialisierungsblöcke werden durch Hinzufügen von Initialisierungslogik zu einem Satz geschweifter Klammern in der Klassendefinition angegeben. Wenn ein Objekt erstellt wird, werden zuerst nicht statische Initialisierungsblöcke und dann die Konstruktoren des Objekts aufgerufen. Beachten Sie, dass Sie mehr als einen nicht statischen Initialisierungsblock angeben können. In diesem Fall wird jeder in der Reihenfolge aufgerufen, in der er in der Klassendefinition angegeben ist. Zusätzlich zu nicht statischen Initialisierungsblöcken können statische Blöcke erstellt werden, die ausgeführt werden, wenn die Klasse in den Speicher geladen wird. Um einen statischen Initialisierungsblock zu erstellen, fügen wir einfach das statische Schlüsselwort hinzu:
public class Foo { { System.out.println("Foo:instance 1"); } static { System.out.println("Foo:static 1"); } public Foo() { System.out.println("Foo:constructor"); } }
Wenn alle drei Initialisierungsmethoden in der Klasse vorhanden sind (Konstruktoren, nicht statische Initialisierungsblöcke und statische Initialisierungsblöcke), werden statische immer zuerst (wenn die Klasse in den Speicher geladen wird) in der Reihenfolge ihrer Deklaration ausgeführt, und nicht statische Initialisierungsblöcke werden in der Reihenfolge ausgeführt, in der sie deklariert wurden nach ihnen - die Designer. Wenn eine Superklasse eingeführt wird, ändert sich die Ausführungsreihenfolge ein wenig:
- Statische Initialisierungsblöcke der Oberklasse in der Reihenfolge ihrer Deklaration
- Initialisierungsblöcke für statische Unterklassen in der Reihenfolge ihrer Deklaration
- Nicht statische Initialisierungsblöcke der Oberklasse, in der Reihenfolge, in der sie deklariert sind
- Superklasse-Konstruktor
- Nicht statische Unterklassen-Initialisierungsblöcke, in der Reihenfolge, in der sie deklariert sind
- Unterklassenkonstruktor
Beispielsweise können wir die folgende Anwendung erstellen:
public abstract class Bar { private String name; static { System.out.println("Bar:static 1"); } { System.out.println("Bar:instance 1"); } static { System.out.println("Bar:static 2"); } public Bar() { System.out.println("Bar:constructor"); } { System.out.println("Bar:instance 2"); } public Bar(String name) { this.name = name; System.out.println("Bar:name-constructor"); } } public class Foo extends Bar { static { System.out.println("Foo:static 1"); } { System.out.println("Foo:instance 1"); } static { System.out.println("Foo:static 2"); } public Foo() { System.out.println("Foo:constructor"); } public Foo(String name) { super(name); System.out.println("Foo:name-constructor"); } { System.out.println("Foo:instance 2"); } public static void main(String... args) { new Foo(); System.out.println(); new Foo("Baz"); } }
Wenn wir diesen Code ausführen, erhalten wir die folgende Ausgabe:
Bar:static 1 Bar:static 2 Foo:static 1 Foo:static 2 Bar:instance 1 Bar:instance 2 Bar:constructor Foo:instance 1 Foo:instance 2 Foo:constructor Bar:instance 1 Bar:instance 2 Bar:name-constructor Foo:instance 1 Foo:instance 2 Foo:name-constructor
Beachten Sie, dass statische Initialisierungsblöcke nur einmal ausgeführt wurden, selbst wenn zwei Foo-Objekte erstellt wurden. Obwohl nicht statistische und statische Initialisierungsblöcke nützlich sein können, sollte die Initialisierungslogik in den Konstruktoren platziert werden, und Methoden (oder statische Methoden) sollten in Fällen verwendet werden, in denen komplexe Logik die Initialisierung des Status des Objekts erfordert.
3. Initialisierung in doppelten Klammern
Viele Programmiersprachen enthalten einen Syntaxmechanismus zum schnellen und kurzen Erstellen einer Liste oder Map (oder eines Wörterbuchs) ohne Verwendung von detailliertem Vorlagencode. Beispielsweise enthält C ++ eine
Initialisierung in Klammern , mit der Entwickler schnell eine Liste von Aufzählungswerten erstellen oder sogar ganze Objekte initialisieren können, wenn der Konstruktor für das Objekt diese Funktion unterstützt. Leider wurde eine solche Funktion vor JDK 9 nicht implementiert (dazu später mehr). Um einfach eine Liste von Objekten zu erstellen, würden wir Folgendes tun:
List<Integer> myInts = new ArrayList<>(); myInts.add(1); myInts.add(2); myInts.add(3);
Dies erfüllt zwar unser Ziel, eine neue Liste zu erstellen, die mit drei Werten initialisiert ist, ist jedoch zu ausführlich, und der Entwickler muss den Namen der Listenvariablen für jedes Hinzufügen wiederholen. Um diesen Code zu verkürzen, können wir die
doppelte Initialisierung von Klammern verwenden :
List < Integer >List<Integer> myInts = new ArrayList<>() {{ add(1); add(2); add(3); }};
Eine doppelte Klammer-Initialisierung, die ihren Namen von einem Satz aus zwei offenen und geschlossenen geschweiften Klammern ableitet, besteht eigentlich aus mehreren Syntaxelementen. Zunächst erstellen wir
eine anonyme innere Klasse , die die ArrayList-Klasse erweitert. Da ArrayList keine abstrakten Methoden hat, können wir einen leeren Body für eine anonyme Implementierung erstellen:
List<Integer> myInts = new ArrayList<>() {};
Mit diesem Code erstellen wir im Wesentlichen eine anonyme Unterklasse. ArrayList entspricht genau der ursprünglichen ArrayList. Einer der Hauptunterschiede besteht darin, dass unsere innere Klasse implizit auf die enthaltende Klasse verweist (in Form einer von dieser erfassten Variablen), weil Wir schaffen eine nicht-statische innere Klasse. Dies ermöglicht es uns, einige interessante, wenn nicht verwirrende Logik zu schreiben. Beispiel: Hinzufügen dieser Variablen zu einer anonymen inneren Klasse, die mit einer doppelten Klammer initialisiert wurde:
public class Foo { public List<Foo> getListWithMeIncluded() { return new ArrayList<Foo>() {{ add(Foo.this); }}; } public static void main(String... args) { Foo foo = new Foo(); List<Foo> fooList = foo.getListWithMeIncluded(); System.out.println(foo.equals(fooList.get(0))); } }
Wenn diese innere Klasse als statisch definiert wäre, hätten wir keinen Zugriff auf Foo.this. Der folgende Code, der eine statische innere Klasse FooArrayList erstellt, hat beispielsweise keinen Zugriff auf den Link Foo.this und wird daher nicht kompiliert:
public class Foo { public List<Foo> getListWithMeIncluded() { return new FooArrayList(); } private static class FooArrayList extends ArrayList<Foo> {{ add(Foo.this); }} }
Wenn wir die Konstruktion mit unserer in doppelte Klammern gesetzten initialisierten ArrayList fortsetzen, verwenden wir, nachdem eine nicht statische innere Klasse erstellt wurde, nicht statische Initialisierungsblöcke, um die drei Anfangselemente beim Instanziieren einer anonymen inneren Klasse hinzuzufügen. Wenn eine anonyme innere Klasse erstellt wird und es nur ein Objekt einer anonymen inneren Klasse gibt, können wir sagen, dass wir ein nicht statisches inneres Objekt erstellt haben, das beim Erstellen drei Anfangselemente hinzufügt. Dies wird sichtbar, wenn wir ein Paar geschweifte Klammern trennen, wobei eine geschweifte Klammer die Definition einer anonymen inneren Klasse darstellt und die andere den Beginn der Instanzinitialisierungslogik markiert:
List<Integer> myInts = new ArrayList<>() { { add(1); add(2); add(3); } };
Obwohl dieser Trick nützlich sein mag, hat JDK 9 (
JEP 269 ) den Nutzen dieses Tricks durch eine Reihe statischer Factory-Methoden für List (sowie viele andere Arten von Sammlungen) ersetzt. Zum Beispiel könnten wir eine Liste früher mit diesen statischen Factory-Methoden erstellen, wie unten gezeigt:
List<Integer> myInts = List.of(1, 2, 3);
Diese statische Factory-Technik wird aus zwei Hauptgründen verwendet: (1) Es wird keine anonyme innere Klasse erstellt und (2) um den zum Erstellen einer Liste erforderlichen Standardcode zu reduzieren. Es ist zu beachten, dass in diesem Fall das Ergebnis von List unverändert bleibt und nach seiner Erstellung nicht mehr geändert werden kann. Um eine veränderbare Listendatei mit Anfangselementen zu erstellen, müssen Sie eine reguläre Methode oder eine Methode mit einer doppelten Initialisierungsklammer verwenden.
Beachten Sie, dass die statischen Factory-Methoden für einfache Initialisierung, doppelte Klammer und JDK 9 nicht nur für List verfügbar sind. Sie sind für Set- und Map-Objekte verfügbar, wie im folgenden Snippet gezeigt:
Es ist wichtig zu verstehen, wie die Doppelklammer initialisiert wird, bevor Sie sich für eine Verwendung entscheiden. Dadurch wird die Lesbarkeit des Codes verbessert, es können jedoch einige Nebenwirkungen auftreten.
4. Ausführbare Kommentare
Kommentare sind ein wesentlicher Bestandteil fast jedes Programms, und der Hauptvorteil von Kommentaren besteht darin, dass sie nicht ausgeführt werden. Dies wird noch deutlicher, wenn wir eine Codezeile in unserem Programm auskommentieren: Wir möchten den Code in unserer Anwendung speichern, aber nicht ausführen. Das folgende Programm zeigt beispielsweise "5" als Ergebnis an:
public static void main(String args[]) { int value = 5;
Viele Leute denken, dass Kommentare nie ausgeführt werden, aber das ist nicht ganz richtig. Was wird beispielsweise der folgende Codeausschnitt ausgeben?
public static void main(String args[]) { int value = 5;
Sie könnten annehmen, dass dies wieder 5 ist, aber wenn wir den obigen Code ausführen, sehen wir 8 am Ausgang. Der Grund für diesen "Fehler" ist das Unicode-Zeichen \ u000d; Dieses Zeichen ist eigentlich ein
Unicode-Wagenrücklauf und der Java-Quellcode wird vom Compiler als Textdatei im Unicode-Format verwendet. Durch die Hinzufügung zum Code wird in der Zeile nach dem Kommentar der Wert = 8 festgelegt, um die Ausführung sicherzustellen. Dies bedeutet, dass das obige Codefragment tatsächlich wie folgt lautet:
public static void main(String args[]) { int value = 5;
Obwohl dies wie ein Java-Fehler erscheint, ist es tatsächlich ein spezielles Feature für die Sprache. Das ursprüngliche Ziel bestand darin, eine plattformunabhängige Sprache zu erstellen (daher die Erstellung einer virtuellen Java- oder JVM-Maschine), und die Interoperabilität des Quellcodes ist ein Schlüsselaspekt dieses Ziels. Indem wir zulassen, dass Java-Quellcode Unicode-Zeichen enthält, können wir nicht-lateinische Zeichen universell verwenden. Auf diese Weise wird sichergestellt, dass Code, der in einer Region der Welt geschrieben wurde (der möglicherweise nicht-lateinische Zeichen enthält, z. B. in Kommentaren), in einer anderen ausgeführt werden kann. Weitere Informationen finden Sie in
Abschnitt 3.3, Java- oder JLS-Sprachspezifikationen .
Wir können dies auf die Spitze treiben und sogar eine ganze Anwendung in Unicode schreiben. Was macht beispielsweise das folgende Programm (von
Java abgeleiteter Quellcode
: Codeausführung in Kommentaren ?! )?
\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020\u0020 \u0063\u006c\u0061\u0073\u0073\u0020\u0055\u0067\u006c\u0079 \u007b\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020 \u0020\u0020\u0020\u0020\u0073\u0074\u0061\u0074\u0069\u0063 \u0076\u006f\u0069\u0064\u0020\u006d\u0061\u0069\u006e\u0028 \u0053\u0074\u0072\u0069\u006e\u0067\u005b\u005d\u0020\u0020 \u0020\u0020\u0020\u0020\u0061\u0072\u0067\u0073\u0029\u007b \u0053\u0079\u0073\u0074\u0065\u006d\u002e\u006f\u0075\u0074 \u002e\u0070\u0072\u0069\u006e\u0074\u006c\u006e\u0028\u0020 \u0022\u0048\u0065\u006c\u006c\u006f\u0020\u0077\u0022\u002b \u0022\u006f\u0072\u006c\u0064\u0022\u0029\u003b\u007d\u007d
Wenn Sie den obigen Code in eine Datei mit dem Namen Ugly.java einfügen und ausführen, wird Hello world auf der Standardausgabe gedruckt. Wenn wir diese Unicode-Zeichen in Zeichen des
amerikanischen Standardcodes für den Informationsaustausch (ASCII) konvertieren, erhalten wir das folgende Programm:
public class Ugly { public static void main(String[] args){ System.out.println("Hello w"+"orld"); } }
Daher können Unicode-Zeichen im Java-Quellcode enthalten sein. Wenn sie jedoch nicht erforderlich sind, wird dringend empfohlen, sie nicht zu verwenden (z. B. um nicht-lateinische Zeichen in Kommentare aufzunehmen). Wenn sie dennoch erforderlich sind, stellen Sie sicher, dass sie keine Zeichen wie Wagenrücklauf enthalten, die das erwartete Verhalten des Quellcodes ändern.
5. Implementierung der Enum-Schnittstelle
Eine der Einschränkungen von Aufzählungen (eine Aufzählungsliste) im Vergleich zu anderen Klassen in Java besteht darin, dass Aufzählungen keine andere Klasse erweitern können oder sich selbst auflisten. Beispielsweise können Sie Folgendes nicht ausführen:
public class Speaker { public void speak() { System.out.println("Hi"); } } public enum Person extends Speaker { JOE("Joseph"), JIM("James"); private final String name; private Person(String name) { this.name = name; } } Person.JOE.speak();
Wir können jedoch unsere Aufzählungen zwingen, die Schnittstelle wie folgt zu implementieren und eine Implementierung für ihre abstrakten Methoden bereitzustellen:
public interface Speaker { public void speak(); } public enum Person implements Speaker { JOE("Joseph"), JIM("James"); private final String name; private Person(String name) { this.name = name; } @Override public void speak() { System.out.println("Hi"); } } Person.JOE.speak();
Jetzt können wir eine Instanz von Person überall dort verwenden, wo ein Speaker-Objekt benötigt wird. Darüber hinaus können wir auch die kontinuierliche Implementierung abstrakter Schnittstellenmethoden sicherstellen (die sogenannten Methoden, die für Konstanten spezifisch sind):
public interface Speaker { public void speak(); } public enum Person implements Speaker { JOE("Joseph") { public void speak() { System.out.println("Hi, my name is Joseph"); } }, JIM("James"){ public void speak() { System.out.println("Hey, what's up?"); } }; private final String name; private Person(String name) { this.name = name; } @Override public void speak() { System.out.println("Hi"); } } Person.JOE.speak();
Im Gegensatz zu einigen anderen Geheimnissen in diesem Artikel sollte diese Technik nur bei Bedarf eingesetzt werden. Wenn beispielsweise eine Enum-Konstante wie JOE oder JIM anstelle einer Schnittstelle wie Speaker verwendet werden kann, muss die Enum-Definition einer Konstante diesen Schnittstellentyp implementieren. Weitere Informationen finden Sie in Paragraph 38 (S. 177-9)
Effective Java, 3rd Edition .
Fazit
In diesem Artikel haben wir fünf versteckte Geheimnisse in Java untersucht, nämlich: (1) Annotationen können erweitert werden, (2) nicht statische Initialisierungsblöcke können zum Konfigurieren eines Objekts verwendet werden, wenn es erstellt wird, (3) Initialisierung mit doppelten Klammern kann zum Ausführen von Anweisungen beim Erstellen verwendet werden Eine anonyme innere Klasse, (4) Kommentare können manchmal ausgeführt werden, und (5) Aufzählungen können Schnittstellen implementieren. Obwohl diese Funktionen von einem bestimmten Aufgabentyp verwendet werden, sollten einige davon vermieden werden (z. B. das Erstellen ausführbarer Kommentare). Beachten Sie bei der Verwendung dieser Geheimnisse die Regel: "Die Tatsache, dass dies möglich ist, bedeutet nicht, dass dies erforderlich ist."