IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Voir le flux RSS

adiGuba::Blog

C'est bien beau de taper sur Java

Noter ce billet
par , 15/04/2016 à 12h26 (936 Affichages)
...mais des fois çà devient du grand n'importe quoi !

Ce matin mon flux twitter m'a fait remonter un billet qui m'a fait bondir : Java 8 : complexify all the things! (ne vous fiez pas au titre en anglais, c'est bien du français dans le texte).

Java 8 a apporté beaucoup de changement, et j'étais curieux d'en lire plus sur cet avis.
Malheureusement, je suis tombé sur un article à charge sans aucun recul... proche de la mauvaise fois !

Bon au moins y'a de l'humour et quelques GIF amusant...


Ils auraient pu faire autrement ?
(oui, mais heureusement ils ne l'ont pas fait)

Le billet débute par un flots de larmes concernant les interfaces fonctionnelles !
Les lambdas de Java 8 ne serait pas de vrai lambda parce qu'on doit les affecter dans une interface fonctionnelle.

Les autres langages n'ont pas fait comme cela, donc Java est forcément dans l'erreur.
Pourtant cela n'aurait rien apporté de plus, bien au contraire.

Dans les langages objets, il y avait jusqu'à présent 2 grandes solutions pour implémenter des lambdas/closures :
  • Soit on les fait hérités d'un type précis avec du sucre syntaxique pour marquer/typer l'appel de méthode (comme par exemple Closure<T> de Groovy)
  • Soit on intégrait dans le langage un nouveau concept : le type-function (le delegate de C#) qui définit le typeage de la lambda via un type déclarant une méthode unique.


Tout cela pour quoi : pour représenter un objet contenant une seule et unique méthode.
Bref un nouveau concept pour représenter quelque chose qui existe depuis bien longtemps, et éventuellement gagner quelques caractères dans la définition du type en virant deux trois trucs :

Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
delegate double f(double);
Code java : Sélectionner tout - Visualiser dans une fenêtre à part
public interface F { double f(double arg); }
Oui en Java on doit nommer la méthode et ses arguments... Oui c'est vraiment trop affreux !



Après oui c'est "cool" les type-fonctions car on peut faire du sucre syntaxique sur l'opérateur (), et au lieu d'écrire hello.method() on a une surcharge d'opérateur qui nous donne une forme raccourcie hello().
Ouais... c'est génial !!!! Mais çà sert à quoi franchement ? (à part l’ambiguïté sur le type d'appel qu'on effectue)



Car c'est aussi masqué le plus gros problème de ces approches, c'est que cela rajoute une distinction forte entre les lambdas/closures et les objets classiques.
On défini donc un nouveau type qu'il faudra utiliser afin de profiter des lambdas/closures...


Lorsqu'on crée une méthode en C#/Groovy, on se retrouve donc avec 3 possibilités pour chaque argument :
  • On peut utiliser une lambda/closure, pour que ce soit plus simple de passer un "petit bout de code".
  • On peut utiliser une interface, pour que ce soit plus pratique lorsqu'on doit passer du code plus complexe.
  • On peut enfin utiliser une surcharge pour proposer les deux... avec deux types distincts.


C'est pour cela par exemple qu'en C# il y a deux types distincts pour représenter un comparateur (l'interface IComparer<T> et le delegate Comparison<T>) et que la classe List<T> propose deux version de la méthode Sort() :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
void Sort(Comparison<T> comparison)
void Sort(IComparer<T> comparer)


En Java il n'y a rien de tel. Le fait d'utiliser une "interface fonctionnelle" ne change pas les règles de typeage.
Lorsqu'on écrit une méthode, on se contente dans ce cas là de choisir l'interface la plus adaptée.
Et si c'est une interface fonctionnelle on pourra l'utiliser avec une instance "classique" ou une lambda selon le cas.
Ainsi par exemple l'interface List<E> définie une seule et unique méthode sort(Comparator<? super E> c), que l'on peut utiliser de diverses manières :
Code java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
    List<String> list = ...
 
    // Avec une expression lambda :
    list.sort( (a,b) -> (a.compareToIgnoreCase(b)) );
 
    // Avec une référence de méthode :
    list.sort( String::compareToIgnoreCase );
 
    // Avec une instance de Comparator<String> comme Collator :
    Collator collator = Collator.getInstance(Locale.FRANCE);
    collator.setStrength(Collator.PRIMARY);
    list.sort(collator);

Au passage l'article présente un exemple en Groovy sur le trie, mais curieusement sans montrer l'équivalent Java 8 pour faire la même chose :
Code groovy : Sélectionner tout - Visualiser dans une fenêtre à part
laClasseAmericaineList.sort{ it.derniersMots == "Monde de merde" ? 1 : -1 }
Code java : Sélectionner tout - Visualiser dans une fenêtre à part
laClasseAmericaineList.sort( (a,b) -> a.derniersMots.equals("Monde de merde") ? 1 : -1 );




Bref grâce à cette notion d'interface fonctionnelle, les lambdas sont utilisables avec tout un tas d'API existante, et cela même si elles n'ont pas été conçus pour cela à la base et sans avoir à adapter le code.
Par exemple il est possible d'utiliser des expressions lambdas avec l'antique AWT :
Code java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
    Panel advancedPanel = new Panel();
    advancedPanel.setVisible(false);
 
    Checkbox checkbox = new Checkbox("Show advanced panel");
    checkbox.addItemListener( event -> advancedPanel.setVisible(checkbox.getState()));

Ainsi on peut d'or et déjà utiliser les lambdas avec des milliers d'API existante, qu'elle ait été développé ou pas pour les lambdas, et mêmes si ces dernières ont été compilés avec une vielles version de Java...



Non franchement je ne vois aucune raison d'implémenter un type-function à-la delegate de C#.
Car c'est cela qui aurait "augmenter la complexité" du langage... pour pas grand chose.
Mais par contre cela aurait limité fortement l'usage des lambdas...


Quand à Groovy, il devient compatible avec les lambdas de Java 8 dans ses dernières versions...




L'API Stream
(Du filtrage pour tous - pas seulement les listes)

Avant toute chose une petite remarque : pour "ajouter 6 à tous les entiers de la liste", il suffit d'écrire ceci :
Code java : Sélectionner tout - Visualiser dans une fenêtre à part
integers.replaceAll(integer -> integer + 6);



Ensuite les reproches à l'encontre de l'API de Stream se base sur un contrat de départ erroné : l'idée du Stream, ce n'est pas "de fournir une API qui permette de calculer des opérations en parallèle sur les listes".

Non les Stream c'est bien plus large que cela, puisque cela permet de parcourir un flux de données quelconque (et qui n'est pas forcément une collection) de manière simple et efficace.

Les critiques fusent sur la méthode map()... qui devrait soit disant renvoyer une liste, mais c'est juste une aberration car cela empêcherait d'utiliser efficacement des trucs comme cela :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
    List<Product> products = ...

    // On récupère les emails des différents fabricants 
    List<String> manufacturerEmails = products.stream()
            .map(Product::getManufaturer)
            .distinct()
            .map(Manufacturer::getEmail)
            .filter(Objects::nonNull)
            .collect(toList());
En effet si chaque méthode map/distinct/filter retournait une liste, cela signifierait que chacune de ces méthodes devrait parcourir les données pour générer la nouvelle liste.
On se retrouverait donc avec 4 parcours des données pour 4 création de listes...

C'est juste inefficace au possible, surtout si la quantités de données est importante !

A l'inverse avec l'API de Stream chaque méthode intermédiaire (map/distinct/filter) se contente de retourner un Stream qui prend en compte ce nouveau critère sans effectuer aucun traitement.
Le parcourt des données et l'exécution des diverses opérations n'est effectué qu'une seule fois lors de l'appel de l'opération terminale collect() en optant pour toutes les optimisations possibles...


Par exemple il existe déjà des implémentations de Stream basé sur JDBC comme Speedment, qui transforme de manière transparente l'utilisation de Stream en une requête SQL vers le SGBD.



Alors certes il reste qu'il aurait été souhaitable de pouvoir écrire directement toList() à la place de collect(toList())...




Quand aux complaintes concernant le collect()/findAll(), elle ne concerne pas l'API de Stream mais l'API de Collections.
En fait tu voudrais des méthodes permettant de retourner une nouvelle liste, comme en Groovy.
Il n'y a rien de tel en standard car l'API de Collections de Java a opté pour l'approche inverse : ses méthodes modifient l'instance courante (et ne crées pas de nouvelle instance).

Donc collect{ it + 6 } peut être remplacé par replaceAll( it -> it+6 ) tout comme findAll{ it == 12 } peut trouver un équivalent avec removeIf(it -> it != 12) (sachant que dans les deux cas en Java cela s'applique à l'instance courante au lieu d'en créer une nouvelle).


Mais au pire ce genre de méthode peut s'écrire en deux-trois lignes avec l'API de Stream...



Bref, je suis d'accord sur le fait qu'il y aurait beaucoup de chose à reprocher à Java, mais là c'est vraiment du n'importe quoi...



a++

Envoyer le billet « C'est bien beau de taper sur Java » dans le blog Viadeo Envoyer le billet « C'est bien beau de taper sur Java » dans le blog Twitter Envoyer le billet « C'est bien beau de taper sur Java » dans le blog Google Envoyer le billet « C'est bien beau de taper sur Java » dans le blog Facebook Envoyer le billet « C'est bien beau de taper sur Java » dans le blog Digg Envoyer le billet « C'est bien beau de taper sur Java » dans le blog Delicious Envoyer le billet « C'est bien beau de taper sur Java » dans le blog MySpace Envoyer le billet « C'est bien beau de taper sur Java » dans le blog Yahoo

Catégories
Java

Commentaires