Les développeurs de Java ont-ils consciemment abandonné RAII?


82

En tant que programmeur C # de longue date, je suis récemment venu en savoir plus sur les avantages de l’ initialisation d’acquisition de ressources (RAII). En particulier, j'ai découvert que l'idiome C #:

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

a l'équivalent C ++:

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

ce qui signifie que se rappeler d’inclure l’utilisation de ressources comme DbConnectiondans un usingbloc n’est pas nécessaire en C ++! Cela semble être un avantage majeur du C ++. Ceci est encore plus convaincant quand on considère une classe qui a un membre d'instance de type DbConnection, par exemple

class Foo {
    DbConnection dbConn;

    // ...
}

En C #, il faudrait que Foo implémente en IDisposabletant que tel:

class Foo : IDisposable {
    DbConnection dbConn;

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

Et ce qui est pire, chaque utilisateur de Foodevrait avoir à se rappeler de mettre Foodans un usingbloc, comme:

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

Maintenant, en regardant C # et ses racines Java, je me demande ... les développeurs de Java ont-ils pleinement compris ce qu’ils abandonnaient lorsqu’ils ont abandonné la pile au profit du tas, abandonnant ainsi RAII?

(De même, Stroustrup a-t-il pleinement compris la signification de RAII?)


5
Je ne suis pas sûr de ce dont vous parlez avec ne pas inclure des ressources en C ++. L'objet DBConnection gère probablement la fermeture de toutes les ressources de son destructeur.
maple_shaft

16
@maple_shaft, c'est exactement ce que je veux dire! C’est l’avantage du C ++ que j’adresse dans cette question. En C #, vous devez inclure des ressources dans "utiliser" ... mais pas dans C ++.
JoelFan

12
D'après ce que je comprends, RAII, en tant que stratégie, n'a été comprise que lorsque les compilateurs C ++ étaient suffisamment bons pour utiliser des modèles avancés, ce qui est bien après Java. le C ++ qui était réellement disponible lors de la création de Java était un style très primitif, de type "C avec classes", avec peut-être des modèles de base, si vous aviez de la chance.
Sean McMillan

6
"D'après ce que je comprends, RAII, en tant que stratégie, n'a été comprise que lorsque les compilateurs C ++ ont été suffisamment bons pour utiliser des modèles avancés, ce qui est bien après Java." - Ce n'est pas vraiment correct. Les constructeurs et les destructeurs sont des fonctionnalités essentielles du C ++ depuis le premier jour, bien avant l’utilisation généralisée des modèles et bien avant Java.
Jim In Texas

8
@JimInTexas: Je pense que Sean a quelque part un germe de vérité (bien que ce ne soient pas des modèles, mais des exceptions, c'est le point crucial). Les constructeurs / destructeurs étaient présents depuis le début, mais leur importance et le concept de RAII n'étaient pas initialement compris (quel que soit le mot que je cherche). Il a fallu quelques années et un certain temps pour que les compilateurs s’expriment bien avant de comprendre à quel point le RAII est crucial.
Martin York

Réponses:


38

Maintenant, en regardant C # et ses racines Java, je me demande ... les développeurs de Java ont-ils pleinement compris ce qu’ils abandonnaient lorsqu’ils ont abandonné la pile au profit du tas, abandonnant ainsi RAII?

(De même, Stroustrup a-t-il pleinement compris la signification de RAII?)

Je suis à peu près sûr que Gosling n’a pas compris l’importance de RAII au moment où il a conçu Java. Dans ses entretiens, il a souvent évoqué les raisons pour lesquelles on omettait les génériques et la surcharge d'opérateurs, sans jamais mentionner les destructeurs déterministes et la RAII.

Assez drôle, même Stroustrup n'était pas conscient de l'importance des destructeurs déterministes au moment où il les a conçus. Je ne trouve pas la citation, mais si vous y tenez vraiment, vous pouvez la trouver parmi ses interviews ici: http://www.stroustrup.com/interviews.html


11
@maple_shaft: En bref, ce n'est pas possible. Sauf si vous avez inventé un moyen d'avoir un garbage collection déterministe (ce qui semble impossible en général et invalide toutes les optimisations GC de ces dernières décennies), vous devez introduire des objets alloués à la pile, mais cela ouvre plusieurs boîtes de Worms: ces objets ont besoin de sémantique, de "problème de découpage" avec sous-typage (et par conséquent NO polymorphisme), de pointeurs pendants, sauf peut-être si vous y appliquez des restrictions importantes ou si vous apportez des modifications massives au système de types. Et ça me vient à l’esprit.

13
@DeadMG: Vous proposez donc de revenir à la gestion manuelle de la mémoire. C'est une approche valable de la programmation en général et, bien sûr, cela permet une destruction déterministe. Mais cela ne répond pas à cette question, qui concerne un paramètre réservé au GC, qui vise à garantir la sécurité de la mémoire et un comportement bien défini, même si nous agissons tous comme des idiots. Cela nécessite GC pour tout et aucun moyen de lancer manuellement la destruction d'objet (et tout le code Java existant repose au moins sur l'ancien), vous devez donc rendre le GC déterministe ou ne pas avoir de chance.

26
@delan. Je n'appellerais pas la manualgestion de la mémoire des pointeurs intelligents C ++ . Ils ressemblent davantage à un éboueur déterministe à grain fin contrôlable. S'ils sont utilisés correctement, les pointeurs intelligents sont les genoux des abeilles.
Martin York

10
@LokiAstari: Eh bien, je dirais qu'ils sont légèrement moins automatiques que le GC intégral (vous devez penser au type d'intelligence que vous voulez réellement) et que les mettre en oeuvre en tant que bibliothèque nécessite des pointeurs bruts (et donc une gestion de mémoire manuelle) . De plus, je ne connais pas de pointeur intelligent qui gère automatiquement les références cycliques, ce qui est une exigence stricte pour la récupération de place dans mes livres. Les pointeurs intelligents sont certes incroyablement cool et utiles, mais vous devez faire face, ils ne peuvent fournir certaines garanties (que vous les considériez utiles ou non) d’un langage entièrement et exclusivement GC.

11
@ Delan: Je ne suis pas d'accord là-bas. Je pense qu'ils sont plus automatiques que GC car ils sont déterministes. D'ACCORD. Pour être efficace, vous devez vous assurer d’utiliser le bon (je vous le donnerai). std :: faible_ptr gère parfaitement les cycles. Les cycles sont toujours passés au crible, mais en réalité, ils ne posent pratiquement aucun problème, car l'objet de base est généralement basé sur une pile et, le cas échéant, le reste sera rangé. Dans les rares cas où il peut s'agir d'un problème, std :: faible_ptr.
Martin York

60

Oui, les concepteurs de C # (et, j'en suis sûr, de Java) se sont spécifiquement prononcés contre la finalisation déterministe. J'ai interrogé Anders Hejlsberg à ce sujet plusieurs fois vers 1999-2002.

Premièrement, l'idée d'une sémantique différente pour un objet, selon qu'il repose ou non sur un tas, va certainement à l'encontre de l'objectif de conception unificateur des deux langages, qui était de soulager les programmeurs de tels problèmes.

Deuxièmement, même si vous reconnaissez qu'il y a des avantages, la comptabilité comporte des complexités et des inefficacités importantes en matière de mise en œuvre. Vous ne pouvez pas vraiment mettre des objets de type pile sur la pile dans un langage géré. Il vous reste à dire «sémantique semblable à une pile» et à vous engager dans un travail important (les types de valeur sont déjà assez difficiles, pensez à un objet qui est une instance d'une classe complexe, avec des références entrant et sortant dans la mémoire gérée).

Pour cette raison, vous ne voulez pas de finalisation déterministe sur chaque objet d'un système de programmation où "(presque) tout est un objet". Donc , vous ne devez introduire une sorte de syntaxe contrôlée programmeur pour séparer un objet normalement suivi d'un qui a la finalisation déterministe.

En C #, vous avez le usingmot - clé, qui est entré assez tard dans la conception de ce qui est devenu C # 1.0. Tout IDisposablecela est plutôt misérable, et on se demande s'il serait plus élégant de usingtravailler avec la syntaxe de destructeur C ++ ~marquant les classes auxquelles le IDisposablemotif de plaque de chaudière pourrait être automatiquement appliqué?


2
Qu'en est-il de ce que C ++ / CLI (.NET) a fait, où les objets du segment de mémoire géré ont également un "handle" basé sur une pile, qui fournit la RIAA?
JoelFan

3
C ++ / CLI a un ensemble très différent de décisions de conception et de contraintes. Certaines de ces décisions signifient que vous pouvez exiger plus de réflexion de la part des programmeurs sur l’allocation de mémoire et les implications en termes de performances: le compromis "donnez-leur assez de corde pour se pendre". Et j'imagine que le compilateur C ++ / CLI est considérablement plus complexe que celui de C # (en particulier dans les premières générations).
Larry OBrien

5
+1 c'est la seule réponse correcte à ce jour - c'est parce que Java n'a intentionnellement pas d'objets de pile (non primitifs).
BlueRaja - Danny Pflughoeft

8
@Peter Taylor - droit. Mais j'estime que le destructeur non déterministe de C # a très peu de valeur, car vous ne pouvez pas compter sur lui pour gérer un quelconque type de ressource contrainte. Donc, à mon avis, il aurait peut-être été préférable d'utiliser la ~syntaxe comme sucre syntaxique pourIDisposable.Dispose()
Larry OBrien

3
@ Larry: Je suis d'accord. C ++ / CLI n'utilise que le sucre syntaxique pour , et il est beaucoup plus pratique que la syntaxe C #. ~IDisposable.Dispose()
Dan04

41

N'oubliez pas que Java a été développé en 1991-1995, alors que C ++ était un langage très différent. Les exceptions (qui rendaient RAII nécessaire ) et les modèles (facilitant la mise en œuvre de pointeurs intelligents) constituaient des fonctionnalités "nouvelles". La plupart des programmeurs C ++ étaient issus du C et étaient habitués à la gestion manuelle de la mémoire.

Je doute donc que les développeurs de Java aient délibérément décidé d'abandonner RAII. Toutefois, Java a pris la décision délibérée de préférer la sémantique de référence à la sémantique de valeur. La destruction déterministe est difficile à implémenter dans un langage de sémantique de référence.

Alors, pourquoi utiliser la sémantique de référence au lieu de la sémantique de valeur?

Parce que cela rend la langue beaucoup plus simple.

  • Il n'est pas nécessaire d'établir une distinction syntaxique entre Fooet Foo*ou entre foo.baret foo->bar.
  • Une affectation surchargée n'est pas nécessaire, lorsque toute affectation est copiée, un pointeur.
  • Il n'y a pas besoin de constructeurs de copie. (Il est parfois nécessaire de recourir à une fonction de copie explicite telle que clone(). De nombreux objets n'ont simplement pas besoin d'être copiés. Par exemple, les immuables ne le sont pas.)
  • Il n'est pas nécessaire de déclarer les privateconstructeurs de copie et operator=de rendre une classe non copiable. Si vous ne voulez pas que les objets d'une classe soient copiés, vous n'écrivez pas une fonction pour la copier.
  • Il n'y a pas besoin de swapfonctions. (Sauf si vous écrivez une routine de tri.)
  • Il n'y a aucun besoin de références rvalue de style C ++ 0x.
  • Il n'y a pas besoin de (N) RVO.
  • Il n'y a pas de problème de tranchage.
  • Il est plus facile pour le compilateur de déterminer la disposition des objets, car les références ont une taille fixe.

Le principal inconvénient de la sémantique de référence est que, lorsque chaque objet a potentiellement plusieurs références, il devient difficile de savoir quand le supprimer. Vous devez très bien avoir une gestion automatique de la mémoire.

Java a choisi d'utiliser un ramasse-miettes non déterministe.

GC ne peut-il pas être déterministe?

Oui il peut. Par exemple, l'implémentation C de Python utilise le comptage de références. Et plus tard, nous avons ajouté le suivi GC pour traiter les déchets cycliques où refcount échoue.

Mais recomptage est horriblement inefficace. Beaucoup de cycles du processeur ont été consacrés à la mise à jour des comptes. Pire encore, dans un environnement multithread (comme celui pour lequel Java a été conçu) où ces mises à jour doivent être synchronisées. Il est préférable d’utiliser le collecteur de déchets null jusqu'à ce que vous deviez passer à un autre.

On pourrait dire que Java a choisi d’optimiser le cas commun (mémoire) aux dépens de ressources non fongibles comme des fichiers et des sockets. Aujourd'hui, à la lumière de l'adoption de RAII en C ++, cela peut sembler être un mauvais choix. Mais rappelez-vous qu'une grande partie du public cible de Java était les programmeurs C (ou "C avec classes") habitués à fermer ces choses explicitement.

Mais qu'en est-il des "objets de pile" C ++ / CLI?

Ils ne sont que du sucre syntaxique pourDispose ( lien d'origine ), un peu comme le C # using. Cependant, cela ne résout pas le problème général de la destruction déterministe, car vous pouvez créer un fichier anonyme gcnew FileStream("filename.ext")et que C ++ / CLI ne le supprime pas automatiquement.


3
En outre, de bons liens (en particulier le premier, qui est très pertinent pour cette discussion) .
BlueRaja - Danny Pflughoeft

La usingdéclaration traite bien de nombreux problèmes liés au nettoyage, mais il en reste beaucoup. Je suggérerais que la bonne approche pour un langage et un cadre serait de faire une distinction déclarative entre les emplacements de stockage qui "possèdent" une référence et IDisposableceux qui ne le sont pas; écraser ou abandonner un emplacement de stockage qui possède un référencé IDisposabledevrait éliminer la cible en l'absence de directive contraire.
Supercat

1
"Pas besoin de constructeurs de copie" sonne bien, mais échoue mal en pratique. java.util.Date et Calendar sont peut-être les exemples les plus notoires. Rien de plus beau que new Date(oldDate.getTime()).
kevin cline

2
iow RAII n’était pas "abandonné", il n’existait tout simplement pas pour être abandonné :) Pour ce qui est de copier les constructeurs, je ne les ai jamais aimés, trop facile de se tromper, ils sont une source constante de maux de tête quand on est au fond de quelqu'un (else) a oublié de faire une copie en profondeur, ce qui a entraîné le partage de ressources entre des copies qui ne devraient pas l'être.
Je me suis rendu

20

Java7 a introduit quelque chose de similaire au C # using: La déclaration try-with-resources

une trydéclaration qui déclare une ou plusieurs ressources. Une ressource est un objet qui doit être fermé une fois que le programme est terminé. L' tryinstruction -with-resources garantit que chaque ressource est fermée à la fin de l'instruction. Tout objet qui implémente java.lang.AutoCloseable, qui inclut tous les objets qui implémentent java.io.Closeable, peut être utilisé comme ressource ...

Je suppose donc qu’ils n’ont pas consciemment choisi de ne pas mettre en œuvre la norme RAII ou qu’ils ont changé d’avis en attendant.


Intéressant, mais il semble que cela ne fonctionne qu'avec les objets qui implémentent java.lang.AutoCloseable. Probablement pas un gros problème, mais je n'aime pas la sensation de contrainte. Peut-être ai-je un autre objet qui devrait être supprimé automatiquement, mais il est très sémantique de le faire mettre en œuvre AutoCloseable...
FrustratedWithFormsDesigner

9
@ Patrick: Euh, alors? usingn’est pas la même chose que RAII - dans un cas, l’appelant s’inquiète de la possibilité de disposer des ressources, dans l’autre cas, l’appelant s’en occupe.
BlueRaja - Danny Pflughoeft

1
+1 je ne connaissais pas d'essayer avec des ressources; cela devrait être utile pour décharger davantage
jprete

3
-1 pour using/ try-with-resources n'étant pas identique à RAII.
Sean McMillan

4
@Sean: D'accord. usinget il est loin d'être RAII.
DeadMG

18

Java n'a intentionnellement pas d'objets basés sur des piles (objets de valeur). Celles-ci sont nécessaires pour que l'objet soit automatiquement détruit à la fin de la méthode.

À cause de cela et du fait que Java est une ordure ménagée, la finalisation déterministe est plus ou moins impossible (ex. Et si mon objet "local" était référencé ailleurs? Alors quand la méthode se termine, nous ne voulons pas qu'elle soit détruite ) .

Cependant, cela convient à la plupart d'entre nous, car la finalisation déterministe n'est presque jamais nécessaire , sauf en cas d'interaction avec des ressources natives (C ++)!


Pourquoi Java n'a-t-il pas d'objets en pile?

(Autres que les primitives ..)

Parce que les objets basés sur des piles ont une sémantique différente de celle des références basées sur des tas. Imaginez le code suivant en C ++; Qu'est ce que ça fait?

return myObject;
  • Si myObjectest un objet local basé sur une pile, le constructeur de copie est appelé (si le résultat est assigné à quelque chose).
  • Si myObjectest un objet local basé sur une pile et que nous renvoyons une référence, le résultat est indéfini.
  • Si myObjectest un membre / objet global, le constructeur de copie est appelé (si le résultat est assigné à quelque chose).
  • Si myObjectest un membre / objet global et que nous renvoyons une référence, la référence est renvoyée.
  • Si myObjectest un pointeur sur un objet local basé sur une pile, le résultat est indéfini.
  • Si myObjectest un pointeur sur un membre / objet global, ce pointeur est renvoyé.
  • Si myObjectest un pointeur sur un objet basé sur un tas, ce pointeur est renvoyé.

Maintenant, que fait le même code en Java?

return myObject;
  • La référence à myObjectest renvoyée. Peu importe que la variable soit locale, membre ou globale; et il n'y a pas d'objet basé sur la pile ni de cas de pointeur à craindre.

Ce qui précède montre pourquoi les objets basés sur des piles sont une cause très fréquente d’erreurs de programmation en C ++. À cause de cela, les concepteurs de Java les ont sortis; et sans eux, il est inutile d'utiliser RAII en Java.


6
Je ne sais pas ce que vous entendez par "cela ne sert à rien de RAII" ... Je pense que vous voulez dire "il n'y a aucune possibilité de fournir du RAII en Java" ... RAII est indépendant de tout langage ... ce n'est pas le cas devenir "inutile" parce
qu'une

4
Ce n'est pas une raison valable. Un objet n'a pas besoin de vivre sur la pile pour utiliser le RAII basé sur la pile. S'il existe une "référence unique", le destructeur peut être déclenché une fois qu'il est hors de portée. Voir, par exemple, comment cela fonctionne avec le langage de programmation D: d-programming-language.org/exception-safe.html
Nemanja Trifunovic

3
@Nemanja: Un objet n'a pas besoin de vivre sur la pile pour avoir une sémantique basée sur la pile, et je n'ai jamais dit que c'était le cas. Mais ce n'est pas le problème. le problème, comme je l'ai mentionné, est la sémantique basée sur la pile elle-même.
BlueRaja - Danny Pflughoeft

4
@Aaronaught: Le diable est "presque toujours" et "la plupart du temps". Si vous ne fermez pas votre connexion à la base de données et laissez le GC en charge de déclencher le finaliseur, cela fonctionnera parfaitement avec vos tests unitaires et se brisera sévèrement lors du déploiement en production. Le nettoyage déterministe est important quelle que soit la langue.
Nemanja Trifunovic

8
@NemanjaTrifunovic: Pourquoi testez-vous vos unités sur une connexion à une base de données active? Ce n'est pas vraiment un test unitaire. Non, désolé, je ne l'achète pas. De toute façon, vous ne devriez pas créer de connexions à la base de données, vous devriez plutôt les transmettre par le biais de constructeurs ou de propriétés, ce qui signifie que vous ne voulez pas de sémantique de destruction automatique de type pile. Très peu d'objets dépendant d'une connexion à une base de données devraient en être propriétaires. Si le nettoyage non déterministe vous pique si souvent, c'est parce que la conception des applications est mauvaise, pas la mauvaise conception du langage.
Aaronaught

17

Votre description des trous de usingest incomplète. Considérez le problème suivant:

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

Bar b = new Foo();

// Where's the Dispose?

À mon avis, ne pas avoir à la fois RAII et GC était une mauvaise idée. Quand il s'agit de fermer des fichiers en Java, c'est malloc()et free()là-bas.


2
Je conviens que RAII est les genoux des abeilles. Mais la usingclause est un grand pas en avant pour C # par rapport à Java. Cela permet une destruction déterministe et donc une gestion correcte des ressources (ce n’est pas aussi bon que RAII, il faut bien le garder à l’esprit, mais c’est une bonne idée).
Martin York

8
“Lorsqu'il s'agit de fermer des fichiers en Java, c'est malloc () et free () là-bas.” - Absolument.
Konrad Rudolph

9
@ KonradRudolph: C'est pire que malloc et gratuit. Au moins en C, vous n’avez pas d’exception.
Nemanja Trifunovic

1
@Nemanja: Soyons justes, vous pouvez free()le finally.
DeadMG

4
@Loki: Le problème de la classe de base est beaucoup plus important en tant que problème. Par exemple, l'original IEnumerablen'a pas hérité de IDisposable, et il y avait un tas d'itérateurs spéciaux qui ne pourraient jamais être implémentés.
DeadMG

14

Je suis assez vieux J'y suis allé et l'ai vu et me suis cogné la tête à plusieurs reprises.

J'étais à une conférence à Hursley Park où les garçons d'IBM nous disaient à quel point ce nouveau langage Java était merveilleux. Seule une personne a demandé ... pourquoi n'y a-t-il pas de destructeur pour ces objets? Il ne voulait pas dire ce que nous savons en tant que destructeur en C ++, mais il n'y avait pas non plus de finaliseur (ou il avait des finaliseurs mais ils ne fonctionnaient pas fondamentalement). Cela fait longtemps, et nous avons décidé que Java était un langage de jouet à ce moment-là.

maintenant, ils ont ajouté les finaliseurs aux spécifications de langage et Java a été adopté.

Bien sûr, plus tard, on a dit à tout le monde de ne pas mettre de finaliseurs sur leurs objets car cela ralentissait énormément le GC. (car il fallait non seulement verrouiller le tas, mais aussi déplacer les objets à finaliser dans une zone temporaire, car ces méthodes ne pouvaient pas être appelées car le GC avait suspendu l'exécution de l'application. Au lieu de cela, elles seraient appelées immédiatement avant la prochaine Cycle GC) (et pire encore, le finaliseur n’était jamais appelé du tout lorsque l’application était en train de s’arrêter. Imaginez que votre fichier ne soit pas fermé, jamais)

Ensuite, nous avons eu C #, et je me souviens du forum de discussion sur MSDN où on nous a dit à quel point ce nouveau langage C # était merveilleux. Quelqu'un a demandé pourquoi il n'y avait pas de finalisation déterministe et les gars de la SP nous ont dit que nous n'avions pas besoin de telles choses, puis nous ont dit que nous devions changer notre façon de concevoir les applications, puis nous ont dit à quel point le GC était incroyable et toutes nos anciennes applications. ordures et jamais travaillé à cause de toutes les références circulaires. Ensuite, ils ont cédé à la pression et nous ont dit qu'ils avaient ajouté ce modèle IDispose à la spécification que nous pouvions utiliser. Je pensais que c'était à peu près le retour à la gestion de mémoire manuelle pour nous dans les applications C # à ce stade.

Bien sûr, les gars de la SP ont découvert plus tard que tout ce qu'ils nous avaient dit était… eh bien, ils ont fait qu'Idispose était un peu plus qu'une simple interface standard, et ils ont ensuite ajouté la déclaration using. W00t! Ils ont compris que la finalisation déterministe était quelque chose qui manquait dans la langue après tout. Bien sûr, vous devez toujours vous rappeler de le mettre partout, donc c'est toujours un peu manuel, mais c'est mieux.

Alors, pourquoi l'ont-ils fait alors qu'ils auraient pu dès le départ placer une sémantique de style utilisateur dans chaque bloc de portée? Probablement l'efficacité, mais j'aime penser qu'ils ne se sont pas rendus compte. Tout comme finalement, ils se sont rendu compte que vous aviez encore besoin de pointeurs intelligents dans .NET (google SafeHandle), ils pensaient que le GC résoudrait réellement tous les problèmes. Ils ont oublié qu'un objet est plus qu'une simple mémoire et que le CPG est principalement conçu pour gérer la gestion de la mémoire. ils ont été pris au dépourvu par l'idée que le GC s'en chargerait et ont oublié que vous y mettiez d'autres éléments, un objet n'est pas simplement une goutte de mémoire qui n'a pas d'importance si vous ne le supprimez pas pendant un certain temps.

Mais je pense aussi que l’absence de méthode de finalisation dans le langage Java d’origine avait quelque chose de plus important: les objets que vous avez créés étaient tous liés à la mémoire, et si vous vouliez supprimer autre chose (comme un descripteur de base de données, un socket ou autre). ) alors vous deviez le faire manuellement .

Rappelez-vous que Java a été conçu pour les environnements embarqués où les utilisateurs étaient habitués à écrire du code C avec beaucoup d'allocations manuelles. Par conséquent, le fait de ne pas avoir la gratuité automatique n'était pas un problème - ils ne l'avaient jamais fait auparavant, alors pourquoi en auriez-vous besoin en Java? Le problème n'avait rien à voir avec les threads, ou pile / tas, il était probablement juste là pour faciliter l'allocation de mémoire (et donc le dé-allouer). En tout, l'instruction try / finally est probablement un meilleur endroit pour gérer les ressources non-mémoire.

Donc, IMHO, la façon dont .NET a simplement copié le plus gros défaut de Java est sa plus grande faiblesse. .NET aurait dû être un meilleur C ++, pas un meilleur Java.


IMHO, des choses comme «utiliser» des blocs sont la bonne approche pour le nettoyage déterministe, mais quelques autres choses sont également nécessaires: (1) un moyen de s'assurer que les objets sont éliminés si leurs destructeurs lèvent une exception; (2) un moyen de générer automatiquement une méthode de routine pour appeler Disposetous les champs marqués d'une usingdirective, et spécifier si elle IDisposable.Disposedoit automatiquement l'appeler; (3) une directive similaire à using, mais qui n'appellerait Disposequ'en cas d'exception; (4) une variation de IDisposablece qui prendrait un Exceptionparamètre, et ...
Supercat

... qui serait utilisé automatiquement par le usingcas échéant; le paramètre serait nullsi le usingbloc quittait normalement, ou indiquerait quelle exception était en attente s'il sortait via une exception. Si de telles choses existaient, il serait beaucoup plus facile de gérer efficacement les ressources et d’éviter les fuites.
Supercat

11

Bruce Eckel, auteur de "Thinking in Java" et "Thinking in C ++" et membre du comité de normalisation C ++, est d'avis que, dans de nombreux domaines (pas seulement le RAII), Gosling et l'équipe de Java n'ont pas fait leur possible. devoirs.

... Pour comprendre en quoi un langage peut être à la fois désagréable et compliqué, et bien conçu, vous devez garder à l’esprit la décision principale en matière de conception sur laquelle tout repose en C ++: compatibilité avec C. Stroustrup décidée - et correctement , il semblerait - que pour amener les masses de programmeurs C à migrer vers des objets, il fallait rendre le déplacement transparent: leur permettre de compiler leur code C sans modification en C ++. C'était une contrainte énorme, et a toujours été la plus grande force de C ++ ... et son fléau. C’est ce qui a rendu le C ++ aussi performant et complexe.

Cela a également trompé les concepteurs Java qui ne comprenaient pas suffisamment le C ++. Par exemple, ils pensaient que la surcharge des opérateurs était trop difficile à utiliser correctement par les programmeurs. Ce qui est fondamentalement vrai en C ++, car le C ++ a à la fois une allocation de pile et une allocation de tas et vous devez surcharger vos opérateurs pour gérer toutes les situations et ne pas causer de fuites de mémoire. Difficile en effet. Java, cependant, possède un mécanisme d'allocation de stockage unique et un collecteur de mémoire, ce qui rend la surcharge d'opérateur triviale - comme cela a été montré en C # (mais cela avait déjà été montré en Python, antérieur à Java). Mais pendant de nombreuses années, l’équipe Java a eu pour ligne de mire que "la surcharge des opérateurs est trop compliquée". Ceci et beaucoup d'autres décisions où quelqu'un n'a clairement pas

Il y a beaucoup d'autres exemples. Les primitifs "devaient être inclus pour plus d'efficacité". La bonne réponse est de rester fidèle à «tout est un objet» et de fournir une trappe pour effectuer des activités de niveau inférieur lorsque l’efficacité était requise (cela aurait également permis aux technologies de point chaud d’augmenter de manière transparente l’efficacité, avoir). Oh, et le fait que vous ne pouvez pas utiliser le processeur à virgule flottante directement pour calculer des fonctions transcendantales (c'est fait à l'aide d'un logiciel). J'ai écrit sur des sujets comme celui-ci autant que je peux supporter, et la réponse que j'ai entendue a toujours été une réponse tautologique à l'effet que "ceci est la méthode Java".

Quand j’ai écrit sur la mauvaise conception des génériques, j’ai eu la même réponse: "nous devons être rétrocompatibles avec les précédentes (mauvaises) décisions prises en Java". Dernièrement, de plus en plus de gens ont acquis suffisamment d'expérience avec Generics pour constater qu'ils sont vraiment difficiles à utiliser. En effet, les modèles C ++ sont beaucoup plus puissants et cohérents (et beaucoup plus faciles à utiliser maintenant que les messages d'erreur du compilateur sont tolérables). Les gens ont même pris la réification au sérieux, ce qui serait utile, mais ne nuirait guère à une conception paralysée par des contraintes auto-imposées.

La liste continue au point où c'est juste fastidieux ...


5
Cela ressemble à une réponse Java versus C ++, plutôt que de se concentrer sur RAII. Je pense que C ++ et Java sont des langages différents, chacun avec ses forces et ses faiblesses. De plus, les concepteurs C ++ n'ont pas fait leurs devoirs dans de nombreux domaines (principe KISS non appliqué, mécanisme d'importation simple pour les classes manquantes, etc.). Mais la question portait sur RAII: cela manque en Java et vous devez le programmer manuellement.
Giorgio

4
@Giorgio: Le but de l'article est que Java semble avoir manqué le bateau sur un certain nombre de problèmes, dont certains sont directement liés à RAII. Eckels note à propos du C ++ et de son impact sur Java: "Vous devez garder à l’esprit la décision principale en matière de conception sur laquelle tout repose en C ++: la compatibilité avec C. C’était une contrainte énorme, qui a toujours été la plus grande force du C ++ ... Il a également dupé les concepteurs de Java qui ne comprenaient pas suffisamment le C ++. " La conception de C ++ a directement influencé Java, tandis que C # a eu l'occasion d'apprendre des deux. (Si c'est le cas, c'est une autre question.)
Gnawme

2
@Giorgio L'étude des langues existantes dans un paradigme et une famille de langues spécifiques fait en effet partie des devoirs nécessaires au développement d'une nouvelle langue. Ceci est un exemple où ils ont simplement humé avec Java. Ils avaient à regarder C ++ et Smalltalk. C ++ n'avait pas Java à regarder quand il a été développé.
Jeremy

1
@Gnawme: "Java semble avoir manqué le coche sur un certain nombre de problèmes, dont certains sont directement liés à la RAII": pouvez-vous mentionner ces problèmes? L'article que vous avez posté ne mentionne pas RAII.
Giorgio

2
@Giorgio Certes, il y a eu des innovations depuis le développement du C ++ qui expliquent bon nombre des fonctionnalités qui vous manquent. Existe-t-il une ou plusieurs de ces fonctionnalités qu’ils auraient dû trouver en consultant des langages établis avant le développement de C ++? C’est le genre de travail dont nous parlons avec Java - il n’ya aucune raison pour qu’ils ne considèrent pas toutes les fonctionnalités C ++ dans l’évolution de Java. Certains, comme l'héritage multiple, ont été volontairement laissés de côté, alors que d'autres, comme RAII, semblent avoir été négligés.
Jeremy

10

La meilleure raison est beaucoup plus simple que la plupart des réponses ici.

Vous ne pouvez pas transmettre des objets alloués à d'autres threads.

Arrêtez-vous et réfléchissez-y. Continuez à penser .... Maintenant, le C ++ n'avait pas de fil lorsque tout le monde était si enthousiaste dans RAII. Même Erlang (des tas distincts par thread) est gênant lorsque vous passez trop d'objets. C ++ n'a qu'un modèle de mémoire en C ++ 2011; maintenant vous pouvez presque raisonner sur la concurrence en C ++ sans avoir à vous référer à la "documentation" de votre compilateur.

Java a été conçu dès le premier jour (ou presque) pour plusieurs threads.

J'ai toujours mon ancien exemplaire du "Langage de programmation C ++" où Stroustrup m'assure que je n'aurai pas besoin de threads.

La deuxième raison douloureuse est d'éviter de trancher.


1
Java conçu pour plusieurs threads explique également pourquoi le CPG n'est pas basé sur le comptage de références.
Dan04

4
@NemanjaTrifunovic: Vous ne pouvez pas comparer C ++ / CLI à Java ou C #, il a été conçu presque dans le seul but d'interagir avec du code non managé C / C ++; cela ressemble plus à un langage non géré qui donne l'accès au framework .NET qu'à l'inverse.
Aaronaught

3
@NemanjaTrifunovic: Oui, C ++ / CLI est un exemple de la façon dont cela peut être fait d'une manière totalement inappropriée pour des applications normales . Ce n'est utile que pour l'interopérabilité C / C ++. Non seulement les développeurs normaux ne doivent pas avoir à prendre une décision totalement dépourvue de pertinence, mais si vous essayez de la reformuler, il est trivialement facile de créer accidentellement une erreur de pointeur / référence nulle et / ou une fuite de mémoire. Désolé, mais je me demande si vous avez déjà programmé en Java ou en C #, car je ne pense pas que quiconque voudrait utiliser la sémantique utilisée dans C ++ / CLI.
Aaronaught

2
@Aaronaught: J'ai programmé à la fois en Java (un peu) et en C # (beaucoup) et mon projet actuel est à peu près tout en C #. Croyez-moi, je sais de quoi je parle, et cela n'a rien à voir avec "stack vs. heap" - il s'agit avant tout de veiller à ce que toutes vos ressources soient libérées dès que vous n'en avez plus besoin. Automatiquement. Si ce n'est pas le cas, vous aurez des problèmes.
Nemanja Trifunovic

4
@NemanjaTrifunovic: C'est génial, vraiment génial, mais C # et C ++ / CLI vous demandent d'indiquer explicitement quand vous voulez que cela se produise, ils utilisent simplement une syntaxe différente. Personne ne conteste le point essentiel qui vous préoccupe actuellement ("les ressources sont libérées dès que vous n'en avez pas besoin"), mais vous faites un saut logique gigantesque: "toutes les langues gérées devraient avoir un système automatique, mais uniquement. -sort-de mise au rebut déterministe par pile d’appels ". Ça ne tient pas l'eau.
Aaronaught

5

En C ++, vous utilisez des fonctionnalités de langage de bas niveau plus générales (les destructeurs appelés automatiquement sur des objets basés sur des piles) pour implémenter une version de niveau supérieur (RAII). Cette approche est quelque chose que les gens de C # / Java ne semblent pas être trop friands de. Ils préfèrent concevoir des outils spécifiques de haut niveau pour des besoins spécifiques et les fournir aux programmeurs prêts à l'emploi, intégrés au langage. Le problème avec de tels outils spécifiques est qu’ils sont souvent impossibles à personnaliser (c’est en partie ce qui les rend si faciles à apprendre). Lors de la construction à partir de blocs plus petits, une meilleure solution peut apparaître avec le temps, alors que si vous ne disposez que de constructions de haut niveau et intégrées, cela est moins probable.

Donc oui, je pense (je n'étais pas vraiment là ...) que c'était une décision consciente, dans le but de rendre les langues plus faciles à comprendre, mais à mon avis, c'était une mauvaise décision. Encore une fois, je préfère généralement le C ++ qui donne aux programmeurs une chance de rouler leur propre philosophie, donc je suis un peu partial.


7
La philosophie qui consiste à "donner aux programmeurs la chance de lancer leur propre philosophie" fonctionne parfaitement jusqu'à ce que vous ayez besoin de combiner des bibliothèques écrites par des programmeurs qui ont chacun lancé leurs propres classes de chaînes et pointeurs intelligents.
Dan04

@ dan04 donc les langages gérés qui vous donnent des classes de chaînes prédéfinies, puis vous permettent de les patcher-singe, ce qui est une recette pour un désastre si vous êtes le genre de gars qui ne peut pas faire face à une autre chaîne de caractères propre classe.
Gbjbaanb

-1

Vous avez déjà appelé l'équivalent approximatif de ceci en C # avec la Disposeméthode. Java a également finalize. NOTE: Je réalise que la finalisation de Java est non déterministe et différente de celle Disposeque je viens de signaler. Elles ont toutes deux une méthode de nettoyage des ressources parallèlement au GC.

Si quelque chose C ++ devient plus pénible cependant, car un objet doit être détruit physiquement. Dans les langages de niveau supérieur tels que C # et Java, nous dépendons d'un ramasse-miettes pour le nettoyer lorsqu'il ne fait plus référence à celui-ci. Rien ne garantit que l'objet DBConnection en C ++ ne comporte pas de références ni de pointeurs non autorisés.

Oui, le code C ++ peut être plus intuitif à lire, mais peut être un cauchemar pour déboguer, car les limites et les limitations mises en place par des langages tels que Java excluent certains des bogues les plus difficiles et les plus pénibles, tout en protégeant les autres développeurs contre les erreurs courantes des recrues.

Peut-être s'agit-il de préférences, comme la puissance, le contrôle et la pureté bas niveau du C ++, alors que d’autres, comme moi, préfèrent un langage beaucoup plus explicite.


12
Tout d'abord, la "finalisation" de Java n'est pas déterministe ... ce n'est pas l'équivalent de la "disposition" de C # ou des destructeurs de C ++ ... de plus, C ++ a également un collecteur de déchets si vous utilisez .NET
JoelFan

2
@DeadMG: Le problème, c'est que vous n'êtes peut-être pas idiot, mais que cet autre gars qui vient de quitter l'entreprise (et qui a écrit le code que vous gérez à présent) l'aurait peut-être été.
Kevin

7
Ce mec va écrire du code de merde quoi que vous fassiez. Vous ne pouvez pas prendre un mauvais programmeur et lui faire écrire du bon code. Les pointeurs en suspens sont la moindre de mes préoccupations lorsque je traite avec des idiots. De bonnes normes de codage utilisent des pointeurs intelligents pour la mémoire qui doit être suivie. Par conséquent, une gestion intelligente devrait indiquer clairement comment désallouer et accéder à la mémoire en toute sécurité.
DeadMG

3
Qu'est-ce que DeadMG a dit. Il y a beaucoup de mauvaises choses à propos de C ++. Mais la RAII n'en fait pas partie. En fait, le manque de Java et .NET pour rendre compte correctement de la gestion des ressources (car la mémoire est la seule ressource, n'est-ce pas?) Est l'un de leurs principaux problèmes.
Konrad Rudolph

8
À mon avis, le finalisateur est une conception sage en cas de catastrophe. Comme vous forcez l'utilisation correcte d'un objet du concepteur à l'utilisateur de l'objet (pas en termes de gestion de la mémoire mais de gestion des ressources). En C ++, le concepteur de classe est responsable de la bonne gestion des ressources (effectuée une seule fois). En Java, il appartient à l'utilisateur de la classe d'obtenir une gestion correcte des ressources et doit donc être effectué chaque fois que la classe est utilisée. stackoverflow.com/questions/161177/…
Martin York
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.