Aus dem Titel "Java-Sprache aus der Perspektive von Kotlin und C #" denke ich, dass einige Leute, die mit Kotlin und C # vertraut sind, so etwas wie einen Bericht über die Ergebnisse des Erlebens von Java erwarten würden, aber das habe ich getan. Kein Satz. Es wurde von einer Person geschrieben, die sowohl mit Java als auch mit C # gespielt hatte, inspiriert vom Buch "Kotlin In Actin" (und aus geschäftlichen Gründen auch ein C # -Buch gelesen hatte). Dieses Buch war ein sehr interessantes Buch und gab mir ein tieferes Verständnis der Java-Sprache sowie Kotlins Kenntnisse.
Ich denke, dass dieser Artikel ein Material zum Nachdenken über Programmiersprachen sein wird, und ich werde es verlassen.
Warum gibt Ihnen das Studium von Kotlin und C # einen Einblick in Java? Dies liegt daran, dass beide Sprachen mit Java als Referenz entwickelt wurden, um die Verwendung zu vereinfachen. Wenn Sie sich also die Teile ansehen, die durch beide Sprachen ergänzt werden, können Sie sehen, was Java als Schwäche erkannt hat.
Was sind die Schwächen von Java? Es gibt viele, aber hier werde ich mich auf drei Dinge konzentrieren: (1) fehlende Werttypdefinitionsfunktion, (2) Vererbung virtueller Funktionen und (3) mangelnde NULL-Sicherheit. Die drei sind eng miteinander verwandt.
Ein Werttyp ist ein Typ, der durch seinen tatsächlichen Wert dargestellt wird, und eine Werttypvariable enthält direkt ihren Wert. Werttypen existieren nur in grundlegenden Datentypen wie boolean, int und double in Java. Das Gegenteil ist ein Referenztyp, und Variablen dieses Typs enthalten keinen direkten Wert, sondern haben diese Referenz als Wert. In Java sind alle Typen außer dem Basisdatentyp Referenztypen.
In Java sind alle Typen außer den Basisdatentypen, sowohl vorbereitete als auch benutzerdefinierte Typen, Referenztypen. Das heißt, der Benutzer kann den Werttyp nicht definieren.
Im Gegensatz dazu bietet C #, das mit Bezug auf Java erstellt wurde, seit der ersten Version eine Sprachfunktion namens Struktur, mit der Benutzer Werttypen definieren können.
Mit dem Werttyp können Sie leicht bestätigen, dass die beiden Werte identisch sind (Äquivalenz genannt), und eine Kopie davon erstellen. In Java-Basistypen kann beispielsweise der Gleichheitsoperator (==) verwendet werden, um zu bestätigen, dass zwei Werte gleich sind, und der Zuweisungsoperator (=) kann verwendet werden, um eine Kopie des Werts zu erstellen.
Im Fall eines Referenztyps prüft der Äquivalenzoperator, ob die Referenzwerte gleich sind, dh sie beziehen sich auf dasselbe (in diesem Fall beziehen sie sich auf dasselbe), und der Zuweisungsoperator prüft, ob der Referenzwert gleich ist. Erstellen Sie eine Kopie, keine Kopie des tatsächlichen Werts.
Um die Äquivalenz mit Java-Referenztypen zu überprüfen, müssen Sie die Methode equals (und gleichzeitig hashCode) überschreiben. Wenn Sie das Objekt kopieren müssen, müssen Sie die Klonmethode überschreiben oder einen Kopierkonstruktor bereitstellen. Es gibt.
Die Schwäche des Mangels an Werttypen besteht darin, dass Anwendungen viele Möglichkeiten haben, diese beiden Funktionen zu nutzen. Aus diesem Grund kann beim Erstellen einer Anwendung in Java eine große Anzahl von Kesselplatten (Standardcode gemäß Sprachspezifikationen erforderlich) in den Quellcode eingebettet werden (tatsächlich Java Die große Anzahl von im Quellcode eingebetteten Kesselplatten ist größtenteils auf das Fehlen von Sprachmerkmalen zurückzuführen, die in Kotlin und C # als Eigenschaften bezeichnet werden. Hier ist sie jedoch kompliziert, daher werde ich sie hier weglassen.
Warum hat Java alle Typen außer dem Basisdatentyp als Referenztypen verwendet? Der Grund ist, den Java-Autor James Gothlin zu fragen, aber ich kann es mir vorstellen.
Java ist eine vielgepriesene Sprache als objektorientierte Sprache. Zum Zeitpunkt der Geburt von Java sollten die folgenden drei Funktionen in objektorientierten Sprachen enthalten sein.
Von diesen realisiert Java Polymorphismus, indem es virtuelle Funktionen erbt und überschreibt. Diese Funktion kann nur durch den Referenztyp in Java realisiert werden. Aus diesem Grund hat Java, das sich an objektorientierten Sprachen orientiert, möglicherweise versucht, Werttypen so weit wie möglich zu eliminieren. Vielmehr kann gesagt werden, dass die Verwendung des Basisdatentyps als Wertetyp unter Berücksichtigung der Leistung und anderer Faktoren eine unvermeidbare Wahl war.
Darüber hinaus verfügt Java über die Funktion "Operatoren können nicht überladen werden", die anscheinend mit der Funktion zusammenhängt, dass benutzerdefinierte Werttypen nicht erstellt werden können.
In Bezug auf die Mehrfachdefinition von Operatoren sagt der Java-Autor James Gothlin Folgendes:
Vielleicht betrachten 20-30% die Überlastung des Bedieners als die Wurzel allen Übels. Dies ist mit einem großen Kreuz gekennzeichnet, weil irgendwo jemand eine Operatorüberladung verwendet hat, zum Beispiel ein "+", um eine Liste einzufügen, was mein Leben enorm durcheinander gebracht hat. Es muss ... gewesen sein. Viele der Probleme bestehen darin, dass es höchstens ein halbes Dutzend Operatoren gibt, die auf sinnvolle Weise überladen werden können, aber es gibt Tausende oder Millionen von Operatoren, die Sie definieren möchten. Ich muss wählen, aber die Wahl selbst ist gegen meine Intuition.
Das heißt, James Gothlin hasst das Überladen von Bedienern.
Wenn Java jedoch benutzerdefinierte Werttypen als Sprachfunktionen übernommen hätte, wäre es nicht möglich gewesen, keine Operatorüberladung zu verwenden. Zumindest sollte es als Sprachfehler aufgefallen sein.
Einer der typischen Werttypen ist beispielsweise der komplexe Zahlentyp. Wenn dies definiert werden könnte, wäre es ein sprachlicher Fehler, dass die Plus- und Minusoperatoren für diesen Typ nicht definiert werden könnten. Wenn es jedoch nur als Referenztyp definiert werden kann, selbst wenn der Bediener nicht überladen werden kann, wird es nicht als Sprachfehler wahrgenommen.
Durch Eliminieren der Funktion zum Definieren des Wertetyps ist es möglich, die Überlastungsfunktion des Operators, den ich hasse, wegzulassen. Ich denke, das war sehr praktisch für Java.
Wie bereits erwähnt, ist Java eine Sprache, die weithin als objektorientierte Sprache angepriesen wird. Die Sprache soll den Polymorphismus aktiv unterstützen, indem sie virtuelle Funktionen erbt und überschreibt. Das heißt, wenn nichts angegeben ist, wird die Klasse zu einer vererbbaren Klasse, und die in der Klasse definierte Funktion wird zu einer überschreibbaren virtuellen Funktion.
Was ist also falsch an dieser Sprachspezifikation?
In der Tat gibt es ein großes Problem. Virtuelle Funktionen verursachen ein bekanntes Problem, das als "fragile Basisklasse" bezeichnet wird. Aus diesem Grund wird jetzt empfohlen, es nicht zu verwenden.
Das "anfällige Basisklassenproblem" ist das Problem, dass beim Ändern des Codes der Basisklasse die Änderung nicht mehr den Erwartungen der Unterklasse entspricht, was zu böswilligem Verhalten in der Unterklasse führt. (Einzelheiten finden Sie unter https://en.wikipedia.org/wiki/Fragile_base_class usw.)
Um dieses Problem zu vermeiden, empfiehlt das bekannte Java-Meisterwerk Effective Java eine Methode zum "Entwerfen und Dokumentieren für die Vererbung oder zum Verbot der Vererbung". Mit anderen Worten, wenn Polymorphismus durch Vererbung verwendet wird, wird empfohlen, den Implementierungsinhalt der Basisklasse als Dokument zu veröffentlichen (andernfalls wird Vererbung überhaupt nicht verwendet). Dies bedeutet, das "Verstecken von Informationen" aufzugeben, was ein wichtiges Element der Objektorientierung sein soll.
Es gibt einen Sprachfehler, der auffällt, weil alle Typen außer dem Basistyp Referenztypen sind. Das ist der Mangel an NULL-Sicherheit.
Ein Java-Referenztyp ist ein wertreferenzierender Typ, kann jedoch einen nicht referenzierenden Wert (NUL) haben. Zu diesem Zeitpunkt tritt unter der Voraussetzung, dass auf einen Normalwert verwiesen wird, eine Ausnahme namens NullPointerException (sogenannter Nullpo) auf, wenn auf ein Mitglied (Methode oder Feld) dieses Typs verwiesen wird. Einfach ausgedrückt ist NULL-Sicherheit ein Mechanismus, der diese NullPointerException nicht generiert.
Die Tatsache, dass Java nicht NULL-sicher ist, ist andererseits ein sprachlicher Fehler, da Java eine typsichere Sprache ist (z. B. ist C keine typsichere Sprache, und NULL-Referenzen sind ebenfalls ein Problem. Nicht durchgeführt).
Die Typensicherheit ist ein Mechanismus, der unbefugte Operationen an Typen verhindert, indem Kompilierungsfehler erkannt werden. Wenn der Typ einer Variablen beispielsweise vom Typ String ist, führt der Versuch, eine andere Operation als die für den Typ String zulässige durchzuführen, zu einem Kompilierungsfehler in Java. NULL-Referenzen sind die einzige Ausnahme von der Typensicherheit von Java.
Das Problem mit dem Mangel an NULL-Sicherheit in Java ist, dass Java eine sehr weit verbreitete Sprache ist und viele Programmierer von diesem Problem betroffen sind, aber alle außer den grundlegenden Datentypen sind Referenztypen. Drängt auch das.
Um der Ehre von Java ein Wort hinzuzufügen, glaube ich nicht, dass es ein Wort gab, das NULL sicher war, als Java geboren wurde. Zumindest war es kein allgemein bekanntes Merkmal. Daher kann gesagt werden, dass Java nicht NULL-sicher ist.
Wie versucht Java, diese Schwächen zu überwinden? Lassen Sie uns auch sehen, wie Kotlin und C # Maßnahmen gegen diese ergreifen.
In Java sind alle benutzerdefinierten Typen Referenztypen. Daher ist es nicht möglich, einen Werttyp streng zu definieren, aber es ist möglich, einen Typ zu definieren, der sich wie ein Wert verhält. Mit anderen Worten, es ist ein Typ, der einen Vergleich gleicher Werte mit der Methode equals ermöglicht und das Erstellen einer Kopie erleichtert. In diesem Fall müssen jedoch viele Kesselplatten zur Quelle hinzugefügt werden, wie oben beschrieben.
Als Lösung für dieses Problem wurde als erstes Verfahren die automatische Erzeugung von Kesselplatten durch IDE herausgebracht. Dies verringert zwar den Aufwand für die Erstellung der Quelle, verringert jedoch nicht den Aufwand für die spätere Überprüfung.
Dort kam die Bibliothek namens Lombok heraus. Wenn Sie diese Bibliothek installieren, können Sie der Klasse einfach eine Datenanmerkung hinzufügen. Dadurch wird automatisch eine Kesselplatte generiert, die an einer Stelle gleich ist, die im Quellcode nicht sichtbar ist.
Darüber hinaus führte Java 14 einen neuen Mechanismus namens Recod ein, der Javas Schwäche, keinen Werttyp zu haben, weiter ergänzte.
Kotlin ist wie Java eine Sprache, die für die Ausführung auf JVMs entwickelt wurde. Daher haben alle Typen außer dem Basisdatentyp die Eigenschaft, Referenztypen zu sein, und der Werttyp kann nicht definiert werden. Es ist jedoch möglich, eine Klasse zu definieren, die sich wie ein Werttyp wie Java verhält.
Durch einfaches Hinzufügen der Qualifikationsdaten zur Klasse werden Methoden wie equals, hashCode, toString und copy automatisch von Anfang an generiert (diese Methoden sind in der Quelle nicht sichtbar).
Wie bereits erwähnt, verfügt C # über einen Mechanismus zum Definieren eines Werttyps, der als Struktur bezeichnet wird.
Darüber hinaus kann ein Typ, der wie ein Literal verwendet werden kann, das als anonymer Typ bezeichnet wird, auch wie ein Werttyp verwendet werden, da die Funktion Gleich für den Vergleich gleicher Werte automatisch zugewiesen wird.
Ab C # 7.0 kann ein Typ namens tapple verwendet werden. Dies ist auch ein Typ, der leicht zu vergleichen und denselben Wert wie der Werttyp zu kopieren ist.
Der Versuch, eine Klasse mit dem letzten Modifikator zu erben oder eine Methode mit demselben Modifikator zu überschreiben, führt zu einem Kompilierungsfehler. Dies ist eine Funktion, die Java von Anfang an hatte. Grundsätzlich können Sie das Problem der "anfälligen Basisklasse" vermeiden, indem Sie allen Klassen und Methoden final hinzufügen.
Mit den in Java 5.0 eingeführten Annotationen wird jetzt empfohlen, die Annotation Override hinzuzufügen, wenn eine Methode überschrieben wird. Dies ist eine Funktion, die hinzugefügt wurde, um den Fehler zu vermeiden, unbeabsichtigt eine neue Methode zu erstellen, die nicht überschreibt, aber das Überlagern etwas umständlicher macht.
Das zuvor eingeführte "Effective Java" empfiehlt die Methode "Auswahl der Komposition gegenüber der Vererbung". Anstatt eine vorhandene Klasse zu erben, gibt es der neuen Klasse ein privates Feld, das auf eine Instanz der vorhandenen Klasse verweist, und jede Methode in der neuen Klasse ruft die entsprechende Methode der vorhandenen Klasse auf, die sie enthält. Das Ergebnis wird zurückgegeben (dies wird als Delegierung bezeichnet). Dies vermeidet das Problem der "anfälligen Basisklasse". Außerdem verfügen die meisten IDEs über Funktionen, die diese Methode unterstützen. Dieses Verfahren erzeugt jedoch eine große Anzahl von Kesselplatten.
Kotlin behandelt Klassen standardmäßig als endgültig. Das heißt, eine Klasse ohne Modifikatoren kann keine abgeleitete Klasse erstellen. Der Modifikator open muss hinzugefügt werden, um die Vererbung zu ermöglichen.
Ebenso wird die Methode standardmäßig als endgültig behandelt. Mit anderen Worten, eine Methode ohne Modifikator kann nicht überschrieben werden. Der Modifikator open muss hinzugefügt werden, um Überschreibungen zu ermöglichen. Wenn Sie die Methode in einer abgeleiteten Klasse überschreiben, muss der Überschreibungsmodifikator hinzugefügt werden (die Überschreibungsanmerkung der entsprechenden Funktion in Java ist optional).
Kotlin unterstützt aktiv die Methode "Komposition statt Vererbung auswählen". Das Schlüsselwort by erleichtert das Delegieren von Methoden, die von der Schnittstelle geerbt wurden, an andere Klassen.
Kotlin verfügt über einen Mechanismus namens Erweiterungsfunktionen, um die Funktionalität von Klassen ohne Vererbung zu erweitern. Es ist kein Ersatz für die Vererbung und restriktiver als eine reguläre Methodendefinition, aber es ist eine leistungsstarke Möglichkeit, einer vorhandenen Klasse später Methoden hinzuzufügen.
In C # können Sie die Vererbung einer Klasse verhindern, indem Sie der Klasse den versiegelten Modifikator hinzufügen. Im Gegensatz zu Kotlin ist dies jedoch nicht die Standardeinstellung.
Wie Kotlin können Methoden standardmäßig nicht überschrieben werden. Um überschreibbar zu sein, müssen Sie den virtuellen Modifikator hinzufügen. Wenn Sie die Methode in einer abgeleiteten Klasse überschreiben, müssen Sie den Überschreibungsmodifikator hinzufügen.
Es gibt keinen Mechanismus zur Delegation von Klassen wie Kotlin. Wenn Sie eine Klassendelegation durchführen, müssen Sie wie in Java eine Kesselplatte schreiben.
Ähnlich wie bei Kotlin gibt es einen Mechanismus namens Erweiterungsmethode, um einer vorhandenen Klasse eine Methode hinzuzufügen. Da diese Funktion jedoch in C # 3.0 hinzugefügt wurde, verwendete Kotlin sie als Referenz, um einen Mechanismus, der als Erweiterungsfunktion bezeichnet wird, in die Sprachspezifikationen aufzunehmen.
Wenn Sie eine zuvor eingeführte Bibliothek wie JSR305 oder Lombok installieren, können Sie die Annotation Nullable und die Annotation NonNull verwenden. Diese helfen bei der Erkennung von NullPointerException (die Spezifikationen variieren von Bibliothek zu Bibliothek).
Ab Java8 kann die optionale Klasse als Wrapper-Klasse für potenziell null Referenztypen verwendet werden. Die Methoden dieser Klasse unterscheiden zwischen Null und anderen Fällen, wodurch das Auftreten von NullPointerException verringert werden kann.
Das Typsystem von Kotlin unterscheidet zwischen null-toleranten Typen (möglicherweise null-Typen) und nicht null-toleranten Typen (Typen, die keine Nullen tolerieren), wodurch es einfacher wird, NullPointerExceition zu vermeiden. Wenn Sie einen Typ (z. B. String) in Kotlin mit dem Typnamen deklarieren, handelt es sich um einen nicht zulässigen Nulltyp. Nach dem Typnamen muss ein Fragezeichen (z. B. String?) Hinzugefügt werden, um einen nulltoleranten Typ zu erhalten.
Der nicht zulässige Typ null hat keine Null, daher wird keine NullPointerExceition generiert. Darüber hinaus bietet Kotlin die folgende Syntax für nulltolerante Typen, damit das Auftreten von NullPointerExceition weniger wahrscheinlich wird.
Ab C # 8.0, das im September 2019 veröffentlicht wurde, sind nulltolerante Referenztypen verfügbar. Wie bei Kotlin ist dies eine Funktion, die nur dann Nullen im Rückgabewert einer Variablen oder Funktion zulässt, wenn nach dem Typnamen ein Fragezeichen hinzugefügt wird. Der Unterschied zu Kotlin besteht darin, dass Sie null-tolerante Referenztypen nur verwenden können, wenn Sie den null-toleranten Anmerkungskontext aktivieren, um die Kompatibilität mit früheren Versionen zu gewährleisten.
Jedes C # hat eine andere Einführungszeit, aber es gibt die folgenden Operatoren, die NULL berücksichtigen.
In seinem Buch Design and Evolution of C ++ stellt Strawstrap, der Designer der C ++ - Sprache, fest:
"Wie ich vorhergesagt habe, hat Java im Laufe der Jahre neue Funktionen erworben und infolgedessen den" ursprünglichen Vorteil "der Einfachheit zunichte gemacht, aber die Leistung nicht verbessert. Neu Sprachen verkaufen sich immer "einfach" und überleben dann, wobei Größe und Komplexität für reale Anwendungen zunehmen. "
Ich denke genau das ist der Fall. Java und C # sind jetzt komplex genug, und Kotlin, das noch in den Kinderschuhen steckt, wird bald zu einer komplexen Sprache heranwachsen. Das ist eine lebendige Sprache.
Recommended Posts