it-swarm.dev

Haben die Entwickler von Java RAII bewusst aufgegeben?

Als langjähriger C # -Programmierer habe ich kürzlich mehr über die Vorteile von Ressourcenbeschaffung ist Initialisierung (RAII) erfahren ). Insbesondere habe ich festgestellt, dass die C # -Sprache:

using (var dbConn = new DbConnection(connStr)) {
    // do stuff with dbConn
}

hat das C++ - Äquivalent:

{
    DbConnection dbConn(connStr);
    // do stuff with dbConn
}

dies bedeutet, dass es in C++ nicht erforderlich ist, daran zu denken, die Verwendung von Ressourcen wie DbConnection in einen using-Block einzuschließen! Dies scheint ein großer Vorteil von C++ zu sein. Dies ist umso überzeugender, wenn Sie beispielsweise eine Klasse mit einem Instanzmitglied vom Typ DbConnection betrachten

class Foo {
    DbConnection dbConn;

    // ...
}

In C # müsste Foo IDisposable als solches implementieren:

class Foo : IDisposable {
    DbConnection dbConn;

    public void Dispose()
    {       
        dbConn.Dispose();
    }
}

und was noch schlimmer ist, jeder Benutzer von Foo müsste daran denken, Foo in einen using-Block einzuschließen, wie:

   using (var foo = new Foo()) {
       // do stuff with "foo"
   }

Wenn wir uns nun C # und seine Java Wurzeln) ansehen, frage ich mich ... haben die Entwickler von Java voll und ganz verstanden, was sie aufgegeben haben, als sie den Stack in aufgegeben haben) Gunst des Haufens, also RAII aufgeben?

(Hat Stroustrup die Bedeutung von RAII in vollem Umfang erkannt?)

84
JoelFan

Wenn ich mir nun C # und seine Java Wurzeln) anschaue, frage ich mich ... haben die Entwickler von Java voll und ganz verstanden, was sie aufgegeben haben, als sie den Stack aufgegeben haben) Gunst des Haufens, also RAII aufgeben?

(Hat Stroustrup die Bedeutung von RAII ebenfalls voll und ganz erkannt?)

Ich bin mir ziemlich sicher, dass Gosling zu dem Zeitpunkt, als er Java entwarf, nicht die Bedeutung von RAII bekam. In seinen Interviews sprach er oft über Gründe, Generika und Operatorüberlastung wegzulassen, erwähnte jedoch nie deterministische Destruktoren und RAII.

Komischerweise war sich selbst Stroustrup der Bedeutung deterministischer Destruktoren zu dem Zeitpunkt, als er sie entwarf, nicht bewusst. Ich kann das Zitat nicht finden, aber wenn Sie es wirklich mögen, finden Sie es in seinen Interviews hier: http://www.stroustrup.com/interviews.html

38

Ja, die Designer von C # (und ich bin sicher, Java) haben sich ausdrücklich gegen eine deterministische Finalisierung entschieden. Ich habe Anders Hejlsberg zwischen 1999 und 2002 mehrmals danach gefragt.

Erstens widerspricht die Idee einer unterschiedlichen Semantik für ein Objekt, basierend darauf, ob es stapel- oder heapbasiert ist, zweifellos dem einheitlichen Entwurfsziel beider Sprachen, das darin bestand, Programmierer von genau solchen Problemen zu entlasten.

Zweitens, selbst wenn Sie anerkennen, dass es Vorteile gibt, sind mit der Buchhaltung erhebliche Komplexitäten und Ineffizienzen bei der Implementierung verbunden. Sie können nicht wirklich stapelähnliche Objekte in einer verwalteten Sprache auf den Stapel legen. Sie müssen nur noch "stapelähnliche Semantik" sagen und sich auf wichtige Arbeiten festlegen (Werttypen sind bereits schwierig genug. Denken Sie an ein Objekt, das eine Instanz einer komplexen Klasse ist, mit Referenzen, die in den verwalteten Speicher eingehen und wieder in den verwalteten Speicher zurückkehren).

Aus diesem Grund möchten Sie nicht für jedes Objekt in einem Programmiersystem, in dem "(fast) alles ein Objekt ist", eine deterministische Finalisierung. Sie müssen also eine programmierergesteuerte Syntax einführen, um ein normal verfolgtes Objekt von einem Objekt mit deterministischer Finalisierung zu trennen.

In C # haben Sie das Schlüsselwort using, das ziemlich spät im Design von C # 1.0 verwendet wurde. Das ganze IDisposable Ding ist ziemlich elend, und man fragt sich, ob es eleganter wäre, wenn using mit der C++ - Destruktorsyntax ~ Arbeiten würde, die die Klassen kennzeichnet, für die der Boiler- Platte IDisposable Muster könnte automatisch angewendet werden?

60
Larry OBrien

Beachten Sie, dass Java wurde 1991-1995 entwickelt, als C++ eine ganz andere Sprache war. Ausnahmen (die RAII notwendig machten -)) und Vorlagen (die die Implementierung intelligenter Zeiger vereinfachten) waren "neue" Funktionen. Die meisten C++ - Programmierer stammten aus C und waren an die manuelle Speicherverwaltung gewöhnt.

Daher bezweifle ich, dass die Entwickler von Java absichtlich beschlossen haben, RAII aufzugeben. Es war jedoch eine bewusste Entscheidung für Java, Referenzsemantik anstelle von Wertesemantik zu bevorzugen. Deterministische Zerstörung ist in einer Referenzsemantiksprache schwer zu implementieren.

Warum also Referenzsemantik anstelle von Wertesemantik verwenden?

Weil es die Sprache viel einfacher macht .

  • Eine syntaktische Unterscheidung zwischen Foo und Foo* Oder zwischen foo.bar Und foo->bar Ist nicht erforderlich.
  • Eine überladene Zuweisung ist nicht erforderlich, wenn nur ein Zeiger kopiert werden muss.
  • Kopierkonstruktoren sind nicht erforderlich. (Es gibt gelegentlich eine explizite Kopierfunktion wie clone(). Viele Objekte müssen einfach nicht kopiert werden Beispiel: Unveränderliche nicht.)
  • Es ist nicht erforderlich, private Kopierkonstruktoren und operator= Zu deklarieren, um eine Klasse nicht kopierbar zu machen. Wenn Sie nicht möchten, dass Objekte einer Klasse kopiert werden, schreiben Sie einfach keine Funktion, um sie zu kopieren.
  • swap -Funktionen sind nicht erforderlich. (Es sei denn, Sie schreiben eine Sortierroutine.)
  • Es sind keine R ++ - Referenzwerte im C++ 0x-Stil erforderlich.
  • Es besteht keine Notwendigkeit für (N) RVO.
  • Es gibt kein Schnittproblem.
  • Für den Compiler ist es einfacher, Objektlayouts zu bestimmen, da Referenzen eine feste Größe haben.

Der Hauptnachteil der Referenzsemantik besteht darin, dass es schwierig ist zu wissen, wann jedes Objekt gelöscht werden muss, wenn es möglicherweise mehrere Verweise darauf hat. Sie müssen so ziemlich automatisch speichern.

Java entschied sich für einen nicht deterministischen Garbage Collector.

Kann GC nicht deterministisch sein?

Ja, kann es. Beispielsweise verwendet die C-Implementierung von Python die Referenzzählung. Und später wurde Tracing GC hinzugefügt, um den zyklischen Müll zu behandeln, bei dem Nachzählungen fehlschlagen.

Aber das Nachzählen ist schrecklich ineffizient. Viele CPU-Zyklen haben damit verbracht, die Anzahl zu aktualisieren. Noch schlimmer in einer Multithread-Umgebung (wie der Art Java entwickelt wurde), in der diese Updates synchronisiert werden müssen. Viel besser, um den Null Garbage Collector bis zu verwenden Sie müssen zu einem anderen wechseln.

Man könnte sagen, dass Java) den allgemeinen Fall (Speicher) auf Kosten nicht fungibler Ressourcen wie Dateien und Sockets optimiert hat. Angesichts der Einführung von RAII in C++ kann dies heute der Fall sein scheinen die falsche Wahl zu sein. Aber denken Sie daran, dass ein Großteil der Zielgruppe für Java war C (oder "C mit Klassen") Programmierer, die es gewohnt waren, diese Dinge explizit zu schließen.

Aber was ist mit C++/CLI "Stack-Objekten"?

Sie sind nur syntaktischer Zucker für Dispose ( rsprünglicher Link ), ähnlich wie C # using. Das allgemeine Problem der deterministischen Zerstörung wird jedoch nicht gelöst, da Sie eine anonyme gcnew FileStream("filename.ext") erstellen können und C++/CLI diese nicht automatisch entsorgt.

42
dan04

Java7 führte etwas Ähnliches wie C # ein using : Die Anweisung zum Ausprobieren mit Ressourcen

eine try Anweisung, die eine oder mehrere Ressourcen deklariert. Eine Ressource ist ein Objekt, das geschlossen werden muss, nachdem das Programm damit fertig ist. Die Anweisung try- with-resources stellt sicher, dass jede Ressource am Ende der Anweisung geschlossen wird. Jedes Objekt, das Java.lang.AutoCloseable Implementiert, einschließlich aller Objekte, die Java.io.Closeable Implementieren, kann als Ressource verwendet werden ...

Ich denke, sie haben sich entweder nicht bewusst dafür entschieden, RAII nicht zu implementieren, oder sie haben es sich inzwischen anders überlegt.

20
Patrick

Java hat absichtlich keine stapelbasierten Objekte (auch Wertobjekte genannt). Diese sind notwendig, damit das Objekt am Ende der Methode automatisch zerstört wird.

Aus diesem Grund und aufgrund der Tatsache, dass Java ist Müllsammlung), ist eine deterministische Finalisierung mehr oder weniger unmöglich (z. B. Was ist, wenn mein "lokales" Objekt? wurde woanders referenziert? Wenn die Methode endet, wollen wir nicht, dass sie zerstört wird .

Dies ist jedoch für die meisten von uns in Ordnung, da es fast nie einen Bedarf für eine deterministische Finalisierung gibt, außer bei der Interaktion mit nativen (C++) Ressourcen!


Warum hat Java keine stapelbasierten Objekte?

(Andere als Primitive ..)

Weil stapelbasierte Objekte eine andere Semantik haben als heapbasierte Referenzen. Stellen Sie sich den folgenden Code in C++ vor; was tut es?

return myObject;
  • Wenn myObject ein lokales stapelbasiertes Objekt ist, wird der Kopierkonstruktor aufgerufen (wenn das Ergebnis etwas zugewiesen ist).
  • Wenn myObject ein lokales stapelbasiertes Objekt ist und wir eine Referenz zurückgeben, ist das Ergebnis undefiniert.
  • Wenn myObject ein Mitglied/globales Objekt ist, wird der Kopierkonstruktor aufgerufen (wenn das Ergebnis etwas zugewiesen ist).
  • Wenn myObject ein Mitglied/globales Objekt ist und wir eine Referenz zurückgeben, wird die Referenz zurückgegeben.
  • Wenn myObject ein Zeiger auf ein lokales stapelbasiertes Objekt ist, ist das Ergebnis undefiniert.
  • Wenn myObject ein Zeiger auf ein Mitglied/globales Objekt ist, wird dieser Zeiger zurückgegeben.
  • Wenn myObject ein Zeiger auf ein Heap-basiertes Objekt ist, wird dieser Zeiger zurückgegeben.

Was macht nun der gleiche Code in Java?

return myObject;
  • Der Verweis auf myObject wird zurückgegeben. Es spielt keine Rolle, ob die Variable lokal, Mitglied oder global ist. und es gibt keine stapelbasierten Objekte oder Zeigerfälle, über die Sie sich Sorgen machen müssen.

Das Obige zeigt, warum stapelbasierte Objekte eine sehr häufige Ursache für Programmierfehler in C++ sind. Aus diesem Grund haben die Designer Java) sie herausgenommen, und ohne sie macht es keinen Sinn, RAII in Java zu verwenden.

Ihre Beschreibung der Löcher von using ist unvollständig. Betrachten Sie das folgende Problem:

interface Bar {
    ...
}
class Foo : Bar, IDisposable {
    ...
}

Bar b = new Foo();

// Where's the Dispose?

Meiner Meinung nach war es eine schlechte Idee, nicht sowohl RAII als auch GC zu haben. Wenn es darum geht, Dateien in Java zu schließen, sind es malloc() und free() dort drüben.

17
DeadMG

Ich bin ziemlich alt Ich war dort und habe es gesehen und viele Male meinen Kopf darüber geschlagen.

Ich war auf einer Konferenz in Hursley Park, wo uns die IBM-Jungs erzählten, wie wunderbar diese brandneue Java Sprache war, nur jemand fragte ... warum gibt es keinen Destruktor für diese Objekte. Er meinte nicht das, was wir als Destruktor in C++ kennen, aber es gab kein Finalisierer entweder (oder es hatte Finalisierer, aber sie funktionierten im Grunde nicht). Dies ist weit zurück und wir entschieden, dass Java zu diesem Zeitpunkt eine Art Spielzeugsprache war.

jetzt fügten sie Finalisers zur Sprachspezifikation hinzu und Java sah eine gewisse Akzeptanz.

Natürlich wurde später jedem gesagt, er solle keine Finalisierer auf seine Objekte setzen, da dies den GC enorm verlangsamte. (da es nicht nur den Heap sperren, sondern die zu finalisierenden Objekte in einen temporären Bereich verschieben musste, da diese Methoden nicht aufgerufen werden konnten, da der GC die Ausführung der App angehalten hat. Stattdessen würden sie unmittelbar vor dem nächsten aufgerufen GC-Zyklus) (und schlimmer noch, manchmal wurde der Finaliser beim Herunterfahren der App überhaupt nicht aufgerufen. Stellen Sie sich vor, Sie hätten Ihr Dateihandle niemals geschlossen.)

Dann hatten wir C # und ich erinnere mich an das Diskussionsforum auf MSDN, in dem uns gesagt wurde, wie wunderbar diese neue C # -Sprache ist. Jemand fragte, warum es keine deterministische Finalisierung gebe, und die MS-Jungs sagten uns, dass wir solche Dinge nicht brauchten. Dann sagten sie uns, wir müssten unsere Art, Apps zu entwerfen, ändern. Dann sagten sie uns, wie großartig GC sei und wie all unsere alten Apps seien Müll und nie wegen all der Zirkelverweise gearbeitet. Dann gaben sie dem Druck nach und sagten uns, sie hätten dieses IDispose-Muster zu der Spezifikation hinzugefügt, die wir verwenden könnten. Ich dachte, es war zu diesem Zeitpunkt so ziemlich die manuelle Speicherverwaltung für uns in C # -Apps.

Natürlich stellten die MS-Jungs später fest, dass alles, was sie uns erzählt hatten, ... nun, sie machten IDispose ein bisschen mehr als nur eine Standardschnittstelle und fügten später die using-Anweisung hinzu. W00t! Sie erkannten, dass die deterministische Finalisierung doch in der Sprache fehlte. Natürlich muss man immer noch daran denken, es überall einzulegen, also ist es immer noch ein bisschen manuell, aber es ist besser.

Warum haben sie das getan, wenn sie von Anfang an automatisch Semantik im Verwendungsstil auf jeden Bereichsblock setzen konnten? Wahrscheinlich Effizienz, aber ich denke gerne, dass sie es einfach nicht realisiert haben. Genau wie sie schließlich erkannten, dass Sie in .NET (google SafeHandle) immer noch intelligente Zeiger benötigen, dachten sie, dass der GC wirklich alle Probleme lösen würde. Sie haben vergessen, dass ein Objekt mehr als nur Speicher ist und dass GC in erster Linie für die Speicherverwaltung ausgelegt ist. Sie waren in die Idee verwickelt, dass der GC damit umgehen würde, und vergaßen, dass Sie andere Dinge hineinstecken. Ein Objekt ist nicht nur ein Speicherklumpen, der keine Rolle spielt, wenn Sie es für eine Weile nicht löschen.

Ich denke aber auch, dass das Fehlen einer Finalisierungsmethode im ursprünglichen Java etwas mehr damit zu tun hatte - dass es bei den von Ihnen erstellten Objekten nur um Speicher ging und ob Sie etwas anderes löschen wollten (wie eine Datenbank) Griff oder eine Steckdose oder was auch immer) dann wurde von Ihnen erwartet manuell.

Denken Sie daran, dass Java für eingebettete Umgebungen entwickelt wurde, in denen Benutzer daran gewöhnt waren, C-Code mit vielen manuellen Zuordnungen zu schreiben. Daher war es kein großes Problem, keine automatische Freigabe zu haben - sie haben es noch nie getan. Warum sollten Sie das tun? brauche es in Java? Das Problem hatte nichts mit Threads oder Stack/Heap zu tun, sondern war wahrscheinlich nur dazu da, die Speicherzuweisung (und damit die Zuweisung) etwas zu vereinfachen. Insgesamt ist die Anweisung try/finally wahrscheinlich ein besserer Ort für den Umgang mit Nicht-Speicherressourcen.

Meiner Meinung nach ist die Art und Weise, wie .NET einfach Javas größten Fehler kopiert hat, seine größte Schwäche. .NET sollte ein besseres C++ sein, kein besseres Java.

14
gbjbaanb

Bruce Eckel, Autor von "Thinking in Java" und "Thinking in C++" und Mitglied des C++ Standards Committee, ist der Meinung, dass Gosling und das Java in vielen Bereichen (nicht nur RAII) Team hat ihre Hausaufgaben nicht gemacht.

... Um zu verstehen, wie unangenehm und kompliziert die Sprache sein kann und gleichzeitig gut gestaltet ist, müssen Sie die primäre Entwurfsentscheidung berücksichtigen, an der alles in C++ hing: Kompatibilität mit C. Stroustrup - und das richtig Es scheint, dass der Weg, um die Massen von C-Programmierern dazu zu bringen, sich zu Objekten zu bewegen, darin bestand, die Verschiebung transparent zu machen: damit sie ihren C-Code unter C++ unverändert kompilieren können. Dies war eine enorme Einschränkung und war schon immer die größte Stärke von C++ ... und sein Fluch. Dies hat C++ so erfolgreich und komplex gemacht.

Es hat auch die Java Designer getäuscht, die C++ nicht gut genug verstanden haben. Zum Beispiel dachten sie, dass das Überladen von Operatoren für Programmierer zu schwierig sei, um es richtig zu verwenden. Dies gilt grundsätzlich für C++, da C++ sowohl über eine Stapelzuweisung als auch über eine Heapzuweisung verfügt und Sie Ihre Operatoren überlasten müssen, um alle Situationen zu bewältigen und keine Speicherverluste zu verursachen. In der Tat schwierig. Java verfügt jedoch über einen einzelnen Speicherzuweisungsmechanismus und einen Garbage Collector, wodurch das Überladen von Operatoren trivial wird - wie in C # gezeigt wurde (aber bereits in Python gezeigt wurde, das vor Java war). Aber für viele Jahre lautete die teilweise Zeile des Java -Teams "Überlastung des Bedieners ist zu kompliziert." Diese und viele andere Entscheidungen, bei denen jemand offensichtlich seine Hausaufgaben nicht gemacht hat, sind der Grund, warum ich den Ruf habe, viele der von Gosling und dem Java -Team getroffenen Entscheidungen zu verachten.

Es gibt viele andere Beispiele. Primitive "mussten aus Effizienzgründen einbezogen werden." Die richtige Antwort ist, "alles ist ein Objekt" treu zu bleiben und eine Falltür bereitzustellen, um Aktivitäten auf niedrigerer Ebene auszuführen, wenn Effizienz erforderlich war (dies hätte es den Hotspot-Technologien auch ermöglicht, die Dinge transparent effizienter zu machen, als dies letztendlich der Fall wäre haben). Oh, und die Tatsache, dass Sie den Gleitkommaprozessor nicht direkt zur Berechnung transzendentaler Funktionen verwenden können (dies geschieht stattdessen in Software). Ich habe so viel wie möglich über solche Themen geschrieben, und die Antwort, die ich höre, war immer eine tautologische Antwort auf den Effekt, dass "dies der Java Weg ist".

Als ich darüber schrieb, wie schlecht Generika entworfen wurden, erhielt ich die gleiche Antwort, zusammen mit "Wir müssen abwärtskompatibel mit früheren (schlechten) Entscheidungen sein, die in Java getroffen wurden." In letzter Zeit haben immer mehr Leute genug Erfahrung mit Generics gesammelt, um zu sehen, dass sie wirklich sehr schwer zu verwenden sind - tatsächlich sind C++ - Vorlagen viel leistungsfähiger und konsistenter (und viel einfacher zu verwenden, da Compiler-Fehlermeldungen tolerierbar sind). Die Menschen haben die Verdinglichung sogar ernst genommen - etwas, das hilfreich wäre, aber ein Design, das durch selbst auferlegte Zwänge verkrüppelt ist, nicht so stark beeinträchtigt.

Die Liste geht bis zu dem Punkt, an dem es nur langweilig ist ...

11
Gnawme

Der beste Grund ist viel einfacher als die meisten Antworten hier.

Sie können keine vom Stapel zugewiesenen Objekte an andere Threads übergeben.

Halte inne und denke darüber nach. Denken Sie weiter ... Jetzt hatte C++ keine Threads, als alle so begeistert von RAII waren. Sogar Erlang (separate Haufen pro Thread) wird schwierig, wenn Sie zu viele Objekte herumreichen. C++ hat nur in C++ 2011 ein Speichermodell erhalten; Jetzt können Sie fast über Parallelität in C++ nachdenken, ohne auf die "Dokumentation" Ihres Compilers zurückgreifen zu müssen.

Java wurde vom (fast) ersten Tag an für mehrere Threads entwickelt.

Ich habe immer noch meine alte Kopie von "The C++ Programming Language", in der Stroustrup mir versichert, dass ich keine Threads benötige.

Der zweite schmerzhafte Grund ist, das Schneiden zu vermeiden.

10
Tim Williscroft

In C++ verwenden Sie allgemeinere Sprachfunktionen auf niedrigerer Ebene (Destruktoren, die automatisch für stapelbasierte Objekte aufgerufen werden), um eine übergeordnete (RAII) zu implementieren. Dieser Ansatz ist C #/Java Leute scheinen nicht allzu gern zu sein. Sie möchten lieber bestimmte Tools auf hoher Ebene für bestimmte Anforderungen entwerfen und sie den Programmierern zur Verfügung stellen, die in die Sprache integriert sind. Das Problem mit solchen spezifischen Tools ist, dass Es ist oft unmöglich, sie anzupassen (zum Teil ist sie deshalb so einfach zu erlernen). Wenn Sie aus kleineren Blöcken bauen, kann sich mit der Zeit eine bessere Lösung ergeben, wenn Sie nur über ein hohes Niveau verfügen, eingebaute Konstrukte ist dies weniger wahrscheinlich.

Also ja, ich denke (ich war eigentlich nicht da ...), es war eine bewusste Entscheidung mit dem Ziel, das Erlernen der Sprachen zu erleichtern, aber meiner Meinung nach war es eine schlechte Entscheidung. Andererseits bevorzuge ich im Allgemeinen, dass C++ den Programmierern die Möglichkeit gibt, ihre eigene Philosophie zu entwickeln, daher bin ich etwas voreingenommen.

5
imre