Developpez.com

Le Club des Développeurs et IT Pro

Java : le projet Valhalla validé par Oracle

Pour préparer le terrain aux futures évolutions de la plateforme

Le 2014-07-29 14:53:20, par Hinault Romaric, Responsable .NET
Le projet Valhalla pour la plateforme Java prend forme. Annoncé il y a quelques mois par Brian Goetz, responsable du projet, celui-ci a été validé par Oracle.

Le projet Valhalla est une sorte d’incubateur pour le développement des futures fonctionnalités de la plateforme Java. Celui-ci va tenter d’améliorer le langage Java en l’affectant à différents niveaux, comme le typage, le langage, la machine virtuelle ou les bibliothèques. Le projet a pour vocation de définir une sémantique claire au niveau de la machine virtuelle pour permettre un support égal pour le langage Java et les autres langages recourant à la JVM.

La révision majeure dans la description actuelle du projet concerne les génériques. Les versions actuelles de Java ne permettent pas que les types génériques contiennent les types primitifs, et le compilateur Java supprime les détails du type contenu lors de la compilation. Cette approche de typage générique (effacement de type) représente l’un des aspects assez critiqués du système de types de Java.

Le projet Valhalla explorera une nouvelle approche pour la gestion des types génériques, et espère produire une nouvelle forme de typage générique, qui permettra aux développeurs d’appliquer des génériques aux types primitifs. Ainsi, dans les futures versions de Java, des déclarations telles que List<int> pourront être valides en Java.

D’autres modifications importantes sont également au menu comme les types valeurs, qui permettront une optimisation de la représentation mémoire des types tout en maintenant les mêmes performances. Les types valeurs auront pour objectif de combiner certaines propriétés des types primitifs et des types objets, pour donner naissance à un nouveau type, qui sera manipulé par le développeur comme un type primitif.

Les développeurs intéressés par le projet peuvent rejoindre sa liste de diffusion sur le site d’OpenJDK. Brian Goetz a souligné dans l’annonce que le projet est encore à un stade embryonnaire, donc la communauté ne devrait pas s’attendre à trouver une technologie développée au sein de Valhalla dans JDK 9, dont la version finale est prévue pour 2016.

Annonce du projet projet Valhalla

Description du projet Valhalla
  Discussion forum
70 commentaires
  • tomlev
    Rédacteur/Modérateur
    Envoyé par super_navide
    reste plus qu'a pouvoir surcharger les opérateurs comme + - / etc..
    Bah dans ce cas tu peux faire du C#, qui fait tout ce que fait Java, a des types valeur et permet la surcharge d'opérateurs

    (bah oui, je prêche pour ma paroisse, hein )
  • adiGuba
    Expert éminent sénior
    Envoyé par thelvin
    Hmmm. Bon ça a l'air de marcher. Ça va encore démultiplier les classes à charger en mémoire mais ça on n'y aurait pas coupé à un moment ou à un autre.
    C'est atténué par le fait que cela ne concernera que les primitifs/valeurs.
    Les Generics avec des références fonctionneront toujours de la même manière.

    Envoyé par thelvin
    Ça va tout de même commencer à devenir compliqué, la lecture des JavaDoc des classes standard Java : "alors les méthodes remove(Object) et remove(int) elles existent que sur les instances où le E de List<E> est un type référence. Pour les types primitifs ça existe pas, utilisez removeItem(E) et removeIndex(int)"
    Dans l'idée removeItem(E) et removeIndex(int) seront disponible pour tous les types, et les remove(Object)/remove(int) accessible uniquement pour les références pour la rétrocompatibilité.

    Depuis Java 8 la javadoc inclut des onglets pour regrouper les méthodes selon plusieurs critères (instance, static, abstraite, concrète, default...).
    On aura peut-être de nouveaux onglets pour gérer cela...

    Mieux : c'est absent de ce document, mais dans la version précédente ils parlaient carrément de pouvoir faire une implémentation spécifique pour certains types.
    Il serait ainsi possible par exemple d'avoir une implémentation d'ArrayList<boolean> basé sur un BitSet au lieu d'un boolean[]

    Envoyé par super_navide
    Je comprend pas bien ce genre d'évolution qui n'apporte pas grand chose.
    C'est un prérequis à l'intégration des types valeurs.
    Il serait absurde d’insérer des types valeurs dans le langage si on ne peut pas les utiliser avec les Generics.

    Envoyé par super_navide
    Il ferait mieux de faire en sorte de faire évoluer java pour avoir des performance équivalente a du C++.
    Troll ? Ou alors tu es resté bloqué sur la fin des années 90 ?
    Un peu de sérieux voyons...

    Envoyé par super_navide
    La nouvelle fonctionnalité values prévu je sais pas dans quelle version était très intérréssante.
    Ben c'est le même projet : tout ceci est lié.
    Sans specialization l'utilisation des types valeurs risque d'être problématique...

    Envoyé par super_navide
    Sinon une fonctionnalité a embarquer dans JDK serait de pouvoir produire un executable qui ne néssécité pas d'avoir un JRE d'installer sur le PC.
    Cela existe déjà (mais pas dans le JDK en effet).
    Par contre je n'y vois pas grand intérêt...

    Envoyé par super_navide
    Je pense java souffre d'un seul problème ne pas être totalement géré comme python ou C++ pas communauté indépendante.
    Je ne comprend pas trop.
    Même si c'est chapeauté par Oracle l'évolution de Java est entre les mains de plusieurs entreprises/organisations...

    Envoyé par redcurve
    C'est vraiment n'importe quoi cette techno
    Soit tu argumentes, soit c'est du troll pure et le mieux serait d'aller voir ailleurs.
    Ici on préfère les discussions argumentés aux petites phrases toutes faites qui ne veulent rien dire...

    Envoyé par gstratege
    Ça sert à quoi d'avoir des types primitifs ?
    Les types primitifs sont des types de base sans notion d'identité. Ils ne représente qu'un espace mémoire réservé à la valeur qu'ils représente.
    Les types valeurs sont des types primitifs un peu plus évolué, dans le sens où ils peuvent être composé de plusieurs éléments (un peu comme une structure en C).

    Leurs utilités en Java est restreinte, car en les utilisant on "perd" plein de fonctionnalité (pas d'héritage).

    Toutefois cela a deux gros intérêts :
    • Une occupation mémoire fixe, qui permet des allocations en bloc pour les tableaux.
      Un tableau de 10 int occupera toujours la même taille en mémoire quelque soit les valeurs stockées, tandis qu'un tableau de 10 Object occupera plus ou moins de mémoire selon les objets qu'on y met.
    • Elle ne comporte que des données, et aucune informations d'identité propre à l'héritage et la POO.
      Cela permet d'être "partagé" plus facilement avec du code natif, ou des instructions bas-niveau, sans avoir à faire des conversions dans tous les sens.


    C'est important lorsqu'on fait des traitements 3D ou d'autres choses qui peuvent être manipulé directement par le GPU : on génère les données en Java et on les passe tel-quel pour un traitements optimisés (voir même parallélisés).

    Envoyé par tomlev
    En tous cas c'est une très bonne chose qu'ils se penchent enfin sur l'amélioration des génériques; c'est vraiment la feature la plus bancale de Java... Le design actuel partait d'un bon sentiment (garder la compatibilité avec le code déjà compilé existant), mais ça introduit tellement de limitations qu'à mon avis les inconvénients surpassent largement les bénéfices.
    Ils ne vont pas tout changer : les Generics fonctionneront quasiment de la même manière pour les objets.
    Ils vont juste introduire une système de template (un peu à la C#), mais uniquement pour les primitives/valeurs.

    Sinon perso je ne trouve pas que l'implémentation des Generics soit aussi mauvaise que tout le monde le laisse entendre.
    Au contraire je trouve que ses inconvénients sont largement plus décriés qu'ils ne le devraient, et qu'ils y a d'autres avantages qui sont mis sous silence :
    • La compatibilité avec le code déjà compilé comme tu le dis,
    • Mais également l’inter-compatibilité entre du code n'utilisant pas les Generics, et du code les utilisant, au sein d'un même programme sans avoir à faire des conversions.
    • Une covariance/contravariance plus complète, et pas limité à la déclaration du type, et cela dès le début.
    • Une compatibilité avec tous les langages tournant sur une JVM, même si ceux-ci n'ont pas forcément de notion de Generics.


    a++
  • rt15
    Membre éclairé
    Envoyé par wikipedia
    Unlike garbage collection, ARC does not handle reference cycles automatically; it is up to the programmer to break cycles using weak references.
    Traduction = Contrairement au GC, l'ARC ne gère par les cycles de référence automatiquement, c'est au programmeur de casser les cycles avec des références faibles.

    Donc oui, les ingés d'apple travail sur un système pouvant provoquer des fuites mémoires.

    Ils veulent simplifier la vie des devs en passant d'une gestion manuelle de la mémoire à une gestion automatique (Même si pas infaillible).

    Et du même coup, l'ARC est difficilement applicable directement au java où les programmeurs ne font pas (ou très peu) attention aux références circulaires car le GC est supposé s'en occupé ce qu'il fait pas trop mal de nos jours.
    Les programmes existant sont souvent bourrés de références circulaires.
  • professeur shadoko
    Membre chevronné
    Envoyé par super_navide

    reste plus qu'a pouvoir surcharger les opérateurs comme + - / etc..
    la surcharge d'opérateur a été refusée par l'équipe initiale de java pour éviter une confusion à la lecture du code.
    Un des points fondamentaux de la philosophie initiale était: ce qui est compilé est ce qui est explicitement écrit ... malheureusement des évolutions ultérieures ont un peu contourné cette règle
    mais la surcharge d'opérateur rend la lecture des codes C++ incroyablement difficile , et les programmeurs en abusent en violant allégrement la sémantique de l'opérateur.... vade retro satanas ...
    (quoiqu'en Scala il y a un point de vue plus amusant sur la question, voir aussi en Groovy ... mais en Java j'espère que cette demande n'aboutira jamais )
    edit: C# c'est la philosophie Microsoft: vous en voulez? alors en voilà! Peu importe si ça abouti à des formalismes peu en accord avec les règles du génie Logiciel (ou à des incohérences: je pense ici à la vision Microsoft de SQL qui donne un sens à des expressions illégales parce que les utilisateurs font souvent l'erreur ... alors ils donnent un sens à ce qui n'en a pas!)
  • M_Makia
    Membre averti
    C'est une très bonne nouvelle.
    La langage Java accuse d'un retard non négligeable par rapport à d'autres langages (Scala, Python, C# ...).
    Il est temps de dépoussiérer un peu tout ça ^^
  • adiGuba
    Expert éminent sénior
    Envoyé par Gugelhupf 
    Je ne pense pas que les ingénieurs travaillant sur le C++ ou ceux d'Apple mettraient en place ce système s'il y avait autant de fuite de mémoire. C'est d'ailleurs pour éviter les fuites de mémoire que les smart pointers existent en C++.

    Et pourtant c'est bien le cas !!! Les références cycliques ne sont pas détecté.
    Les smart pointers facilitent énormément la gestion des pointeurs, car tu peux les partager sans trop de difficulté (et sans trop te soucier de la libération) et tu n'as pas à les libérer explicitement.

    Mais tout cela à condition qu'il n'y ait pas de référence circulaire.

    Prend le code suivant, ou on affiche un message à la création/destruction de chaque objet :
    Code c++ :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    #include <iostream> 
    #include <memory> 
    using namespace std; 
      
    class MyObject { 
    	private: 
    	const char* name; 
    	std::shared_ptr<MyObject> ptr; 
      
    	public: 
    	MyObject(const char* n) { 
    		name = n; 
    		cout << "create " << name << std::endl; 
    	} 
      
    	~MyObject() { 
    		cout << "delete " << name << std::endl; 
    	} 
      
    	void setOther(std::shared_ptr<MyObject> p) { 
    		ptr = p; 
    	} 
    }; 
      
    int main() { 
    	// p1 et p2 sont alloué dynamiquement  
    	MyObject* p1 = new MyObject("object1"); 
    	MyObject* p2 = new MyObject("object2"); 
      
      
    	// p3 est alloué dynamiquement via un smartpointeur 
    	std::shared_ptr<MyObject> p3(new MyObject("object3")); 
      
    	// p4 et p5 est alloué dynamiquement via un smartpointeur 
    	// et on fait un lien monodirectionnel de p4 vers p5 
    	std::shared_ptr<MyObject> p4(new MyObject("object4")); 
    	std::shared_ptr<MyObject> p5(new MyObject("object5")); 
    	p4->setOther(p5); 
      
    	// p6 et p7 est alloué dynamiquement via un smartpointeur 
    	// et on fait un lien bidirectionnel entre p6 et p7 
    	std::shared_ptr<MyObject> p6(new MyObject("object6")); 
    	std::shared_ptr<MyObject> p7(new MyObject("object7")); 
    	p6->setOther(p7); 
    	p7->setOther(p6); 
      
    	cout << std::endl; 
      
    	delete p1; 
    	return 0; 
    }

    Lorsqu'on l'exécute cela donne ceci (l'ordre des :
    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    create object1 
    create object2 
    create object3 
    create object4 
    create object5 
    create object6 
    create object7 
     
    delete object1 
    delete object4 
    delete object5 
    delete object3
    L'objet pointé par p2 n'est pas libéré car on n'appelle pas explicitement delete.
    Les pointeurs p6 et p7 sont libéré et l'ARC décrémente leurs compteurs respectifs.
    Toutefois ces compteurs indique qu'il y a toujours une référence qui traine (respectivement dans object6 et object7) et du coup ils ne peuvent pas être libéré.
    Bref ils sont toujours référencé pour l'ARC, même si on ne peut plus y accéder.

    Exemple C++ : http://ideone.com/fdVA3X

    En Java on n'a pas de tel problème... par contre on ne peut pas prédire le moment où ces objets seront libéré et on doit donc forcer le GC si on veut le démontrer (mais c'est crade il ne faut pas faire ca dans une application).
    Exemple Java : http://ideone.com/k9WN64

    En fait pour pallier aux références circulaires avec les smart pointeurs, il faut utiliser des "weaks références" : http://ideone.com/V6Mied
    Mais il faut bien prendre en compte que ces "weaks références" peuvent se retrouver à null sans prévenir...

    Donc oui dans des langages comme Java ou C#, le fait de passer d'un GC à l'ARC seraient une grosse regression, et entrainerait la modification d'un bon paquet de code...

    a++
  • Thyxx
    Membre régulier
    Envoyé par super_navide
    On peut dire adieu au C++

    Mais tout à fait...
  • adiGuba
    Expert éminent sénior
    Envoyé par Gugelhupf
    J'ai lu la JEP 193 concernant l'évolution de "volatile" mais je n'ai pas tout à fait saisi l'objectif de ".volatile".
    Non.

    L'idée c'est d'avoir le même mécanisme que les classes AtomicInteger, etc. qui n'ont pas recours à la synchronisation.

    Il faut savoir que les seules opérations atomiques c'est l'affectation d'une valeur :
    Code :
    1
    2
    3
    count = 15;
    // ou 
    int temp = count;


    Mais dès qu'il y a un calcul au milieu on n'est plus atomique, par exemple une incrémentation :
    Code :
    count += 1; // ou count++
    En effet cela se décompose en fait en plusieurs instructions, que l'on pourrait représenter comme cela (en vrai c'est du bytecode, mais pour l'exemple je laisse en java) :
    Code :
    1
    2
    3
    int next = count;
    next++;
    count = next;
    Le problème c'est que si un autre thread modifie "count" entre ces deux opérations on se retrouve avec une valeur incorrect.

    Par exemple si tu as deux threads qui incrément de 1 en même temps tu peux avoir cela :
    Code :
    1
    2
    3
    4
    int count = 0; // etat initial
    
    count += 1; // thread 1
    count += 1; // thread 2
    On pourrait penser que le résultat sera forcément "2", mais ce n'est pas garantie.
    En effet en décomposant on a en fait ceci :
    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    int count = 0; // etat initial
    
    int next1 = count; // thread 1 (count vaut 0)
    next1++; // thread 1
    count = next1; // thread 1
    
    int next2 = count; // thread 2  (count vaut 1)
    next2++;  // thread 2
    count = next2; // thread 2
    Sauf que l'ordonnanceur peut nous jouer des tours, et on obtient alors une exécution qui ressemble à quelque chose comme cela :
    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int count = 0; // etat initial
    
    int next1 = count; // thread 1 (count vaut 0)
    int next2 = count; // thread 2 (count vaut 0)
    
    next1++; // thread 1
    next2++; // thread 2
    
    count = next1; // thread 1
    count = next2; // thread 2
    Du coup next1 et next2 valent tous les deux "1", et count prend finalement la valeur "1" alors qu'on l'a incrémenté deux fois !

    En clair entre le moment ou on fait notre calcul et le moment ou on affecte le résultat dans la variable, la variable a été modifié par un autre thread
    Du coup on devrait avoir un résultat différent, et notre calcul fait "disparaitre" la modif précédente...

    En fait les classes AtomicXXX se base sur le compareAndSet, qui permet d'affecter une variable seulement si sa valeur correspond bien à celle indiqué.
    Du coup l'incrémentation correspond plutôt à ceci :
    Code :
    1
    2
    3
    int old = count;
    int next = old + 1;
    count.volatile.compareAndSet(old, next); // ne marche QUE si la valeur de count == old
    L'idée c'est donc de faire cela dans une boucle, jusqu'à ce que compareAndSet() fonctionne.
    Ainsi si "count" a été modifié par un autre thread entre temps, on reprend le calcul avec la valeur modifié et on réessaye.

    C'est grosso-modo ce que font les méthodes d'AtomicInteger.
    Cela peut paraitre curieux, mais c'est plus efficace que la synchronisation (cela n'a quasiment aucun "coût" lorsque la variable n'est pas modifié entre temps).
    C'est même plus sûr car il n'y a pas forcément besoin d'utiliser synchronized sur toutes les utilisations de la variable...

    L'objectif de la JEP c'est de permettre cela directement sur un attribut primitif, sans pour autant passer par les classes AtomicXXX...

    a++
  • adiGuba
    Expert éminent sénior
    @Gugelhupf : Attention c'est l'ancienne valeur qui est vérifiée, pas celle que l'on a calculé.
    • get() renvoit la valeur actuelle de l'AtomicInteger, que l'on stocke dans une variable locale ("current" dans l'exemple d'incrementAndGet()).
    • On fait notre calcul, que l'on stocke dans une autre variable ("next" dans l'exemple)
    • Enfin compareAndSet() n'affectera la valeur que l'on a calculer ("next" que si la valeur de l'AtomicInteger est toujours égale à "current".
      En clair si la valeur de l'AtomicInteger n'a pas été modifiée depuis la première fois qu'on y a accédé, on affecte notre résultat, sinon on recommence notre calcul car il n'est plus bon...


    Envoyé par thelvin
    Théorique maximum, c'est bel et bien l'infini. Si un autre thread passe son temps à incrémenter le compteur et que le thread en cours est bizarrement aligné par l'ordonnanceur de sorte de ne jamais, jamais faire une boucle entière avant que l'autre ne fasse la sienne, eh ben, il continuera d'essayer pour toujours et restera coincé là.
    Oui.

    Mais cela poserait également problème avec une synchronisation classique, puisqu'on serait bloqué sur l'acquisition du lock.
    Dans ce cas l'utilisation de l'AtomicXXX serait plus gourmande en CPU (boucle active) tandis que le code synchronisé serait en sommeil.
    Mais comme tu le dis il faut avoir une boucle infini qui modifie constamment la variable...

    Dans un cas "normal" même si l'AtomicXXX doit faire quelques tours de boucles il y a des chances que ce soit plus rapide qu'une synchronisation....

    a++
  • tomlev
    Rédacteur/Modérateur
    Envoyé par thelvin
    Ouais enfin bon... Moi, des listes d'objets si grandes que l'empreinte mémoire s'en ressent, j'en ai jamais. J'ai des structures plus complexes, qui contiennent de grandes quantités de données et pèsent sur la mémoire, mais ces données ne sont pas simples au point de pouvoir être remplacées par un système non polymorphe.
    Enfin, on ferait un mix des données qui peuvent en être et des données qui peuvent pas, si vraiment on avait besoin de réduire l'emprunte mémoire au lieu de juste gérer le fait qu'il y a besoin de beaucoup de mémoire. Mais on pourrait très bien le faire avec un décorateur d'accès à un ByteBuffer par exemple. C'est plus compliqué qu'avec des value types, ouais enfin... Pas tant que ça ! Parce que les value types ne font que le dixième du boulot. Les stocker de manière efficace et y accéder efficacement, c'est là la vraie difficulté, maintenant que c'est plus des objets.

    Je la trouve pas franchement omniprésente, cette faiblesse.
    Ce n'est pas seulement une question de mémoire, mais aussi de performance. Si on reprend l'exemple de rt15, sa centaine de points ne sont pas nécessairement contigus en mémoire ; il peuvent même se trouver dans des pages différentes de la mémoire, ce qui peut causer des chargements de page quand on passe d'un objet à l'autre, ce qui dégrade les perfs. Avec un tableau d'objets de type valeur, tu as tous les objets les uns à côté des autre en mémoire, et donc ça va plus vite...

    EDIT: et c'est aussi moins de boulot pour le garbage collector...