web-dev-qa-db-fra.com

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

En tant que programmeur C # de longue date, j'ai récemment appris à en savoir plus sur les avantages de L'acquisition de ressources est l'initialisation (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 qu'il n'est pas nécessaire de penser à enfermer l'utilisation de ressources comme DbConnection dans un bloc using en C++! Cela semble être un avantage majeur du C++. Ceci est encore plus convaincant lorsque vous considérez 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 IDisposable comme tel:

class Foo : IDisposable {
    DbConnection dbConn;

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

et pire encore, chaque utilisateur de Foo devra se rappeler de placer Foo dans un bloc using, 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 apprécié ce qu'ils abandonnaient lorsqu'ils ont abandonné la pile dans faveur du tas, abandonnant ainsi RAII?

(De même, Stroustrup a-t-il pleinement apprécié l'importance de RAII?)

84
JoelFan

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

(De même, Stroustrup a-t-il pleinement apprécié l'importance de RAII?)

Je suis 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 de la suppression des génériques et de la surcharge des opérateurs, mais n'a jamais mentionné les destructeurs déterministes et le 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 êtes vraiment dedans, vous pouvez la trouver parmi ses interviews ici: http://www.stroustrup.com/interviews.html

38

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

Premièrement, l'idée d'une sémantique différente pour un objet selon que son empilement ou son empilement est certainement contraire à l'objectif de conception unificateur des deux langages, qui était de soulager les programmeurs de ces problèmes.

Deuxièmement, même si vous reconnaissez qu'il y a des avantages, la comptabilité tient compte d'importantes complexités de mise en œuvre et d'inefficacités. 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 de type 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 qui entrent et reviennent dans la mémoire gérée).

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

En C #, vous avez le mot clé using, qui est arrivé assez tard dans la conception de ce qui est devenu C # 1.0. L'ensemble IDisposable est assez misérable, et on se demande s'il serait plus élégant d'avoir using de travailler avec la syntaxe du destructeur C++ ~ marquer les classes auxquelles le modèle de plaque chauffante IDisposable pourrait être appliqué automatiquement?

60
Larry OBrien

Gardez à l'esprit que Java a été développé en 1991-1995 lorsque C++ était un langage très différent. Exceptions (ce qui rendait RAII nécessaire ) et les modèles (qui facilitaient l'implémentation des pointeurs intelligents) étaient des fonctionnalités "nouvelles". La plupart des programmeurs C++ venaient de 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. Cependant, c'était une décision délibérée pour Java de préférer la sémantique de référence au lieu de 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 le langage beaucoup plus simple.

  • Il n'est pas nécessaire de faire une distinction syntaxique entre Foo et Foo* Ou entre foo.bar Et foo->bar.
  • Il n'y a pas besoin d'affectation surchargée, lorsque toute l'affectation ne consiste qu'à copier un pointeur.
  • Il n'y a pas besoin de constructeurs de copie. (Il y a occasionnellement un besoin pour une fonction de copie explicite comme clone(). De nombreux objets n'ont tout simplement pas besoin d'être copiés. Pour par exemple, les immutables ne le font pas.)
  • Il n'est pas nécessaire de déclarer private les constructeurs de copie et operator= Pour rendre une classe non copiable. Si vous ne voulez pas que les objets d'une classe soient copiés, vous n'écrivez simplement pas de fonction pour la copier.
  • Il n'y a pas besoin de fonctions swap. (Sauf si vous écrivez une routine de tri.)
  • Il n'y a pas 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 avez à peu près une gestion automatique de la mémoire.

Java a choisi d'utiliser un garbage collector non déterministe.

Le 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, ajouté GC de traçage pour gérer les ordures cycliques où les recomptages échouent.

Mais le recomptage est horriblement inefficace. Beaucoup de cycles CPU ont mis à jour les comptages. Pire encore dans un environnement multi-thread (comme le type Java a été conçu pour) où ces mises à jour doivent être synchronisées. Il vaut mieux utiliser le null garbage collector jusqu'à vous devez passer à un autre.

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

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

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

42
dan04

Java7 a introduit quelque chose de similaire au C # using : l'instruction try-with-resources

une instruction try qui déclare une ou plusieurs ressources. Une ressource est un objet qui doit être fermé une fois le programme terminé. L'instruction try- 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 ...

Donc je suppose qu'ils n'ont pas consciemment choisi de ne pas implémenter RAII ou qu'ils ont changé d'avis entre-temps.

20
Patrick

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

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

Cependant, cela convient à la plupart d'entre nous, car il n'y a presque jamais besoin de finalisation déterministe, sauf lors de l'interaction avec des ressources natives (C++)!


Pourquoi Java n'a-t-il pas d'objets basés sur la pile?

(Autre que les primitives ..)

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

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

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

return myObject;
  • La référence à myObject est retournée. Peu importe que la variable soit locale, membre ou globale; et il n'y a aucun objet basé sur la pile ou cas de pointeur à s'inquiéter.

Ce qui précède montre pourquoi les objets basés sur la pile sont une cause très courante d'erreurs de programmation en C++. Pour cette raison, les concepteurs Java Java les ont supprimés; et sans eux, il est inutile d'utiliser RAII en Java.

Votre description des trous de using est 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.

17
DeadMG

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

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

maintenant, ils ont ajouté des finaliseurs à la spécification de langue et Java a vu une certaine adoption.

Bien sûr, plus tard, tout le monde a été invité à ne pas mettre de finaliseurs sur leurs objets, car cela ralentissait considérablement le GC. (car il devait non seulement verrouiller le tas mais déplacer les objets à finaliser vers une zone temporaire, car ces méthodes ne pouvaient pas être appelées car le GC avait interrompu l'application. Au lieu de cela, elles seraient appelées immédiatement avant la prochaine Cycle GC) (et pire encore, parfois le finaliseur ne serait jamais appelé du tout lorsque l'application était en cours de fermeture. Imaginez ne pas avoir le descripteur de fichier 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 garçons MS 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 des applications, puis nous ont dit à quel point le GC était incroyable et comment toutes nos anciennes applications étaient ordures et n'a jamais fonctionné à cause de toutes les références circulaires. Puis ils ont cédé à la pression et nous ont dit qu'ils avaient ajouté ce modèle IDispose à la spécification que nous pourrions utiliser. À ce stade, je pensais que c'était à peu près de retour à la gestion manuelle de la mémoire dans les applications C #.

Bien sûr, les garçons MS ont découvert plus tard que tout ce qu'ils nous avaient dit était ... eh bien, ils ont fait IDispose un peu plus qu'une simple interface standard, et ont ensuite ajouté la déclaration using. W00t! Ils ont réalisé que la finalisation déterministe était quelque chose qui manquait à 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 avoir une sémantique de style d'utilisation automatiquement placée sur chaque bloc de portée depuis le début? Probablement de l'efficacité, mais j'aime à penser qu'ils ne l'ont tout simplement pas réalisé. Tout comme ils ont finalement réalisé que vous avez encore besoin de pointeurs intelligents dans .NET (google SafeHandle), ils pensaient que le GC résoudrait vraiment tous les problèmes. Ils ont oublié qu'un objet est plus qu'une simple mémoire et que GC est principalement conçu pour gérer la gestion de la mémoire. ils ont été pris dans l'idée que le GC gérerait cela, et ont oublié que vous y mettez d'autres choses, un objet n'est pas seulement 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 le manque d'une méthode de finalisation dans l'original Java avait un peu plus à faire - que les objets que vous avez créés étaient tous de la mémoire, et si vous vouliez supprimer autre chose ( comme un descripteur de base de données ou un socket ou autre) alors vous étiez censé le faire manuellement.

Rappelez-vous Java a été conçu pour les environnements embarqués où les gens étaient habitués à écrire du code C avec beaucoup d'allocations manuelles, donc ne pas avoir de libre automatique n'était pas vraiment un problème - ils ne l'ont jamais fait auparavant, donc pourquoi en auriez-vous besoin en Java? Le problème n'avait rien à voir avec les threads, ou avec la pile/le tas, il était probablement juste là pour rendre l'allocation de mémoire (et donc la désallocation) un peu plus facile. Enfin, l'instruction est probablement un meilleur endroit pour gérer les ressources non-mémoire.

À mon humble avis, 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.

14
gbjbaanb

Bruce Eckel, auteur de "Thinking in Java" et "Thinking in C++" et membre du C++ Standards Committee, est d'avis que, dans de nombreux domaines (pas seulement RAII), Gosling et le Java équipe n'a pas fait ses devoirs.

... Pour comprendre comment le langage peut être à la fois désagréable et compliqué, et bien conçu en même temps, vous devez garder à l'esprit la décision de conception principale à laquelle tout en C++ dépendait: la compatibilité avec C. Stroustrup a décidé - et correctement , il semblerait - que le moyen d'amener les masses de programmeurs C à se déplacer vers les objets était de rendre le déplacement transparent: pour leur permettre de compiler leur code C inchangé sous C++. C'était une énorme contrainte, et a toujours été la plus grande force du C++ ... et son fléau. C'est ce qui a fait le succès de C++, et aussi complexe qu'il soit.

Cela a également trompé les concepteurs Java Java qui ne comprenaient pas assez bien le C++. Par exemple, ils pensaient que la surcharge des opérateurs était trop difficile à utiliser pour les programmeurs. Ce qui est fondamentalement vrai en C++, car C++ a à la fois l'allocation de pile et l'allocation de tas et vous devez surcharger vos opérateurs pour gérer toutes les situations et ne pas provoquer de fuites de mémoire. Difficile en effet. Java, cependant, a un mécanisme d'allocation de stockage unique et un garbage collector, ce qui rend la surcharge de l'opérateur triviale - comme cela a été montré en C # (mais avait déjà été montré en Python, qui était antérieur à Java). Mais pendant de nombreuses années, la ligne en partie de l'équipe Java était "La surcharge de l'opérateur est trop compliquée". Ceci et bien d'autres les décisions où quelqu'un n'a clairement pas fait ses devoirs est la raison pour laquelle j'ai la réputation de dédaigner bon nombre des choix faits par Gosling et l'équipe Java.

Il existe de nombreux autres exemples. Les primitives "devaient être incluses 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 hotspot de rendre les choses plus efficaces de manière transparente, comme elles le feraient finalement avoir). Oh, et le fait que vous ne pouvez pas utiliser directement le processeur à virgule flottante pour calculer les fonctions transcendantales (cela se fait à la place dans le logiciel). J'ai écrit sur des questions comme celle-ci autant que je peux le supporter, et la réponse que j'entends a toujours été une réponse tautologique à l'effet que "c'est la manière Java Java."

Quand j'ai écrit sur la façon dont les génériques étaient mal conçus, j'ai obtenu la même réponse, ainsi que "nous devons être rétrocompatibles avec les (mauvaises) décisions antérieures prises en Java". Dernièrement, de plus en plus de gens ont acquis suffisamment d'expérience avec les génériques pour voir qu'ils sont vraiment très 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 - quelque chose qui serait utile mais qui ne mettra pas autant de brèche dans une conception qui est paralysée par des contraintes auto-imposées.

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

11
Gnawme

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

Vous ne pouvez pas passer d'objets alloués par pile à d'autres threads.

Arrêtez-vous et pensez-y. Continuez à penser ... Maintenant, C++ n'avait pas de threads quand tout le monde était si passionné par RAII. Même Erlang (des tas séparés par fil) devient capricieux lorsque vous passez trop d'objets autour. C++ n'a obtenu un modèle de mémoire qu'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 (presque) le premier jour pour plusieurs threads.

J'ai toujours mon ancienne copie de "The C++ Programming Language" où Stroustrup m'assure que je n'aurai pas besoin de threads.

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

10
Tim Williscroft

En C++, vous utilisez des fonctionnalités de langage de niveau inférieur plus générales (destructeurs appelés automatiquement sur des objets basés sur la pile) pour implémenter un niveau supérieur (RAII), et cette approche est quelque chose que C #/Java les gens ne semblent pas trop aimer. 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 dans le langage. Le problème avec ces outils spécifiques est que ils sont souvent impossibles à personnaliser (en partie c'est ce qui les rend si faciles à apprendre). Lors de la construction à partir de blocs plus petits, une meilleure solution peut arriver avec le temps, alors que si vous niquement avez un niveau élevé, constructions intégrées, cela est moins probable.

Alors oui, je pense (je n'étais pas vraiment là ...) que c'était une décision concise, dans le but de rendre les langues plus faciles à comprendre, mais à mon avis, c'était une mauvaise décision. Là encore, je préfère généralement la philosophie C++ donner aux programmeurs une chance de rouler leur propre, donc je suis un peu biaisé.

5
imre