Tout ce qui pue dans le langage Java

Il vaut mieux le savoir !

a marqué ce sujet comme résolu.

Vous permettez que, pour une fois et malgré le titre du topic, que je tape un peu sur C++ aussi ?

Outre les critiques habituelles et puériles du genre « y'a pas de GC c'est nul », en C++, je trouve que séparer la définition de l'implémentation (.hpp/.cpp) est une fondalementalement très bonne chose. Cependant, quelque chose dans cette séparation m'irrite aussi un peu.

JE sais bien qu'il y a des raisons pratiques et historiques derrière ça, mais je trouve extrêmement dommage que les membres privés aient l'obligation de se trouver dans le .hpp aussi. Pour moi ils ne devraient rien avoir à y faire. L'utilisateur du .hpp n'a pas à savoir ce que j'utilise en privé dans mon implémentation.

J'ai déjà essayé de contourner cette limitation avec un truc ultra crado de ce genre; divisant par 2 voire 3 le temps de compilation en bonus simplement parce que l'énorme fichier unordered_map avec ses gigantesques dépendances n'est inclut que là où il est vraiment nécessaire ; ce qui me fait dire que c'est une bonne solution mais avec un mauvais code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// MyClass.hpp
class MyClass {
private:
char storage[123];
public:
// constructeur, méthodes, destructeur; osef ici
};

// MyClass.cpp
#include<unordered_map>
using namespace std;

MyClass::MyClass () {
new(storage) unordered_map<string,string>(); // Ultra dégueu
}

unordered_map<string,string>& 
MyClass::myMember () {
return (unordered_map<string,string>&)(storage); // Un peu dégueu
}

MyClass::~MyClass {
myMember().~unordered_map<string,string>(); // Ultra dégueu
}

J'ai sûrement dû louper quelque chose dans mon apprentissage du C++ pour réussir à pondre des atrocités pareilles. Mon langage de prédilection reste quand même toujours le java.

Dans la continuité de ceci, il y a bien sûr l'impossibilité de séparer la définition et l'implémentation des fonctions/classes génériques comme on le fait normalement pour le reste; là aussi c'est très dommage.

Quelqu'un pour m'expliquer où j'ai tort ?

+0 -0

Séparer pour cacher absolument est un voeux pieux. C'est bien pour la vitesse de compilation (et même ça c'est pas folichon) et éviter de voir des binaires gonfler si tout était vraiment inline. Mais franchement, à moins de vouloir cacher des choses au client, ce n'est pas très grave.

Si vraiment tu veux cacher des choses au client, il y a d'autres façons de s'y prendre. Tu isoles l'API que tu veux fournir, et caches derrière des déclarations anticipées l'accès aux détails internes. Et en interne, OSEF complètement d'une pure séparation des choses. BTW, regarde aussi l'idiome pimpl (ou encore appelé cat of cheshire). Tu peux aussi jouer avec des interfaces pour forcer des inversion de dépendances – le code client va manipuler des types concrets instanciés via factory, mais dont le code est totalement inaccessible.

Enfin, le modèle de compilation/séparation que l'on hérite du C est effectivement reconnu pour être lourd. Cela fait ainsi quelques années qu'il y a un groupe de réflexion qui travaille sur l'ajout de modules en C++. Il y a d'ailleurs déjà eu une implémentation (pour preuve de concept) dans clang.

Au passage :

Je ne souhaite pas rentrer dans un quelconque troll ou guerre des langages, mais simplement préciser un point : le python (pour le haskell je n'en sais rien du tout) est compilé en bytecode puis exécuté à chaque invocation. Tu peux éventuellement "fixer" le bytecode une bonne fois pour toute : ça te donne un fichier mon_prog.pyc (c pour compiled donc).

Donc, sans parler des différences entre ces 2 langages, mais uniquement en considérant la manière de gérer les bytecodes, Python recompile tout le temps et c'est le même prog qui compile puis exécute, alors qu'en Java tu compile une fois pour toute et ensuite t'as un 2e prog (la JVM bien sûr) qui éxécute.

Il y a une grosse différence qui est ailleurs que ce que tu décrit. Tout d'abord ceci n'est pas lié au langage, mais à CPython, l'implémentation la plus utilisé. La simple existence de Jython va compliqué la généralisation du reste au langage de manière général. Ensuite comme tu le dis, il n'est pas obligatoire de faire re-générer les bytecodes, l'interpréteur peut très bien s'en sortir avec le pyc, a condition que le bytecode n'ai pas changé entre les deux versions de l'interpréteur utilisé pour le générer et pour le lire (ce qui reste assez rare). Continuons ensuite sur le bytecode lui même. Je connais peu le bytecode java mais celui de CPython est ultra haut niveau. Il n'y a presque aucune optimisation de faite. En gros c'est juste une transposition du code en suite de bytecode pour accélérer l'interprétation, mais l'étape de compilation ne fait aucun traitement particulier. En partie dut au fait qu'au vu de la nature de python, il est impossible de faire quoique ce soit. Enfin la dernière grosse différence est dans l'utilisation du bytecode : CPython fais de l'interprétation, la machine virtuelle Java fais de la compilation à la volée.

Bref le fais qu'ils passent par des bytecode n'en fais pas des techno-comparable. A noter cependant que d'autre machines virtuelles python, comme PyPy, font elles de la compilation à la volée.

Si on doit revenir au sujet principal, il faut chercher les immondices du côté de l'autoboxing principalement. Le truc le plus célèbre étant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class TestCache {
    public static void main(String... args)
    {
        Integer a = 127;
        Integer b = 127;
        System.out.println(a == b); // true
        System.out.println(a.equals(b)); // true
        Integer c = 128;
        Integer d = 128;
        System.out.println(c == d); // false !!!
        System.out.println(c.equals(d)); // true
    }
}

Et récemment, je suis tombé là-dessus dans le même genre de saletés incompréhensibles.

Pour le reste, "l'écosystème" Java a des avantages principalement sa JVM. Parmi les langages qui tentent de pallier les manques de Java (et qui adresse pas mal des critiques que vous formulez ici même), il existe Groovy qui est vraiment vraiment (j'insiste beaucoup mais c'est extrêmement dommage) un joli langage qui cherche à boucher les trous de Java.

En étant notamment moins verbeux, en incluant les lambdas depuis déjà tout plein de versions. C'est un régal pour faire de l'asynchrone (handlers de handlers de handlers de handlers, …) en écrivant du code élégant. La surcharge d'opérateur est là, et elle est pratique. L'héritage multiple est là, et mieux conçu (coucou Java 8 et les interfaces à implémentation par défaut) avec la présence de Traits.

Et sa courbe d'apprentissage est vraiment lisse à souhait, puisque du code Java sera à 100% interprétable / compilable en Groovy. Ca permet de démarrer tout doucement et de piger pas à pas, fonctionnalité après fonctionnalité les intérêts de ce langage. Le jour où tu commences à utiliser la méthode curry de la classe Closure à bon escient par exemple, t'as un vrai sentiment de satisfaction.

Oracle cite souvent Scala dans ses présentations de "langages alternatifs" car c'est un vrai langage alternatif. Il explore un autre paradigme que Java. Groovy et Java sont presques concurrents. Et bon nombre des dernières évolutions de Java sont issues de Groovy (lambda, interfaces avec implémentation par défaut, …). Ce qui fait qu'Oracle le cite malheureusement beaucoup moins et lui fait "moins de pub". Pourtant, le jour où vous avez un petit script à écrire qui nécessite l'emploi d'une bibliothèque Java, pensez-y.

+4 -0

J'ai sûrement dû louper quelque chose dans mon apprentissage du C++ pour réussir à pondre des atrocités pareilles. Mon langage de prédilection reste quand même toujours le java.

Dans la continuité de ceci, il y a bien sûr l'impossibilité de séparer la définition et l'implémentation des fonctions/classes génériques comme on le fait normalement pour le reste; là aussi c'est très dommage.

Quelqu'un pour m'expliquer où j'ai tort ?

QuentinC

Pour le premier probleme => utilise le pattern Pimpl (private implementation). C'est un classique pour avoir une ABI plus stable

Pour le probleme de separation des templates, tu peux le faire aussi. C'est juste une separation differente (au lieu d'avoir .h/.cpp, tu vas avoir deux .h ou des .hpp/impl ou autre)

+0 -0

Si on doit revenir au sujet principal, il faut chercher les immondices du côté de l'autoboxing principalement. Le truc le plus célèbre étant […]

C'est quoi l'explication en fait ? Pour les valeurs entre -128 et +127, Java appelle en interne Integer.valueOf et du coup on récupère des instances mises en cache, tandis que pour le reste il utilise systématiquement le constructeur de Integer ?

Dans le même genre, on a aussi qu'est-ce que y sachant que ce code lance une NPE ?

int x = y;

Dans le présent contexte, facile; mais la première fois que ça nous arrive dans un vrai code, on le cherche un sacré moment…

+0 -0

Pour les valeurs entre -128 et +127, Java appelle en interne Integer.valueOf et du coup on récupère des instances mises en cache, tandis que pour le reste il utilise systématiquement le constructeur de Integer ?

C'est exactement ça. Le plus drôle, c'est que ce comportement n'existe que depuis Java 5 et je crois que la taille du cache est spécifique à ta JVM (HotSpot c'est -128 à 127, les autres je sais pas). Donc le comportement n'est pas forcément reproductible.

Pour palier ça, les JVM IBM <= Java 4 avaient des classes et méthodes du type Integerr.neww() (pas de faute de frappe) qui géraient ce cache avant l'heure…

Très honnêtement en ce qui concerne la séparation interface/implémentation en C++, ça mériterait un mémoire. Il y a des situations tout à fait insolubles sauf peut-être à grand renfort de copié-collés de macros (bienvenue dans les API C et leurs typedef magiques impossibles à forwarder !), et d'autres facilement solubles grâce à un pimpl (mais une indirection n'est pas toujours abordable en termes de perfs).

Pour moi la principale raison qui me pousse à sortir ce genre de solutions nucléaires, c'est quand je dois à tout prix éviter une dépendance de bibliothèque et la confiner dans une unité de compilation (ou un ensemble réduit de). Mais j'y regarde à deux fois, et c'est en dernier recours vu le temps que ça prend.

+0 -0

Dans le même genre, on a aussi qu'est-ce que y sachant que ce code lance une NPE ?

QuentinC

Oui pour le cache des Integer, SpaceFox l'a bien expliqué.

Pour ton exemple, oui… C'est sans doute le second truc le plus célèbre de l'autoboxing.

Tu te trimballes un Boolean un peu partout, et puis pouf quelqu'un qui se sert de ton code écrit boolean, ou pire, invoque une fonction qui prend un boolean en paramètre avec ta variable. NPE potentiel, le compilateur ne t'indique aucune "faute". Et c'est l'appel à la méthode qui provoque le NPE, très pénible à débugger.

+0 -0

De base, ne pas laisser le choix à l'utilisateur de travailler avec une valeur, une référence ou un pointeur, ça pue, non ?

Richou D. Degenne

Si les données sont non-mutables par choix de design, non. Le reste du temps, oui. Finalement, ici, on a des passages par valeur … tout le temps (les références sont passées par valeur, de même que les types natifs), et c'est plus là le truc qui fait chier : on ne peut pas passer les éléments natifs par référence :) .

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte