Java 8 : complexify all the things!

Des fois, je me dis que l’objet des réunions de chez Oracle, ça doit être quelque chose comme : « augmentation de la complexité ». J’imagine bien un truc du genre : tout le monde fume un bon gros bédo avant d’entamer les discussions sur le premier sujet : « reprendre l’implémentation des fonctions anonymes de Groovy et bien la pourrir pour la rendre à chier ». Et faut avouer que, dans leur domaine, ils sont plutôt bons, ces cons !

Récemment, nos projets au boulot ont commencé à migrer vers Java 8 et j’ai enfin pu mettre les mains dans les lambdas et les opérations map, filter, reduce sur les listes. Ben je m’attendais pas à grand-chose et ils ont quand-même trouvé le moyen de me faire halluciner…

Lambdas Canada Dry (que ça ressemble aux lambda mais qu’en fait, ç’en est pas)

Donc j’ai pu essayer les lambdas et j’ai flairé le truc pas net très vite. Vous voyez, l’une des utilisations des lambdas que je fais souvent, moi, c’est d’isoler un bout de code d’une fonction dans un objet pour augmenter la lisibilité. Par exemple pour parcourir les résultats d’un requête en base de données en Android, on utilise un objet 

Cursor

  et on utilise les fonctions

getColumnIndex(String columnName)

pour obtenir l’index de la colonne à partir de son nom dans la base de données puis getInt(int columnIndex) ,

getString(int columnIndex)

, etc. pour récupérer les données. Bon, ben quand on a un object complexe, ça fait vite chier de taper

cursor.getString(musicCursor.getColumnIndex(columnName))

à tout bout de champ surtout que quand on a des noms de colonnes un peu longs, ça dépasse vite les 120 caractères.

Du coup, pour améliorer la lisibilité, on stocke ce petit bout de code quelque part, dans une lambda pour pouvoir l’appeler plus tard. Petit exemple dans Phœbius, ici :

def musicCursor = getAppContext()
    .contentResolver.query(MUSIC_URI, null, null, null, null)

// Attention, lambda declaration incoming
def getString = { String columnName ->
    return musicCursor.getString(
        musicCursor.getColumnIndex(columnName))
}

def getInt = { String columnName ->
    return musicCursor.getInt(
        musicCursor.getColumnIndex(columnName))
}

def getLong = { String columnName ->
    return musicCursor.getLong(
        musicCursor.getColumnIndex(columnName))
}

if(musicCursor?.moveToFirst())
{
    while(musicCursor.moveToNext())
    {
        this.vSongs << new Song(
            getLong(SONG_ID),
            getString(SONG_TITLE),
            getString(SONG_ARTIST),
            getString(SONG_ALBUM),
            getInt(SONG_NUMBER),
            getInt(SONG_YEAR)
        )
    }
}
musicCursor.close()

Moi, j’aime bien ça, les lambdas. Déjà, je trouve le concept génial de pouvoir stocker un bout de code dans une variable, et puis ensuite, c’est super sexy de pouvoir déclarer des fonctions anonymes en paramètre d’une méthode. Vous voyez pas l’intérêt ? Ok, j’en appelle à la puissance de la démonstration !

Comment qu’on trie une liste en Java 7 ?

List<LaClasseAmericaine> laClasseAmericaineList = new ArrayList<>();
laClasseAmericaineList.sort(new Comparator<LaClasseAmericaine>(){
    @Override
    int compare(LaClasseAmericaine laClasseAmericaine, LaClasseAmericaine t1)
    {
        if(laClasseAmericaine.derniersMots.equals("monde de merde")) return -1;
        else return 1;
    }
});

Alors qu’en Groovy, avec une closure, je peux faire :

List<LaClasseAmericaine> laClasseAmericaineList = new ArrayList<>();
laClasseAmericaineList.sort{ it.derniersMots == "Monde de merde" ? 1 : -1 }

C’est joliiiiiiiii <3

Du coup, j’étais content de passer en Java 8. J’allais pouvoir mettre plein de lambdas partout et vivre heureux dans le monde merveillleux des fonctions anonymes ! Et puis j’ai voulu stocker ma première fonction anonyme dans une variable. Alors j’ai commencé à chercher quel objet représentait une lambda. Ça devait forcément être un truc qui s’appelle

Closure

comme en Groovy ou 

Delegate

comme en C♯ ou

Function

comme en C++. Bon, en Groovy y’a pas besoin de typer les closures donc la syntaxe devait ressembler à la déclaration du C++

function <double(double)> f

ou celle du C♯

delegate double f(double);

Ah oui, voilà : c’est Function. Attendez… C’est quoi ce truc bizarre…

R apply(T t)
Applies this function to the given argument.

Putain, c’est quoi, cette arnaque ? Pourquoi y’a qu’un seul argument, là ? Je fais comment pour définir une lambda avec plusieurs arguements ? Ok faut que je trouve la doc, il doit bien y avoir moyen de déclarer une signature de méthode inline… Je veux dire… Ils ont quand-même pas osé…

Functional interfaces. The Runnable interface—like the Callable<T> interface, the Comparator<T> interface, and a whole host of other interfaces already defined within Java—is what Java 8 calls a functional interface: it is an interface that requires exactly one method to be implemented in order to satisfy the requirements of the interface. This is how the syntax achieves its brevity, because there is no ambiguity around which method of the interface the lambda is trying to define.

Une interface… Il… Il faut déclarer une putain d’interface pour pouvoir déclarer une fonction anonyme…

Leonardo-Dicaprio

C’EST QUOI VOTRE PUTAIN DE PROBLÈME AVEC LES INTERFACES, BORDEL !? C’EST QUOI LE PUTAIN D’INTÉRÊT D’AJOUTER LES LAMBDAS AU LANGAGE SI C’EST POUR DEVOIR DÉCLARER DES INTERFACES À LA CON !? VOUS AVIEZ PAS SUFFISAMMENT DE POSSIBILITÉS SYNTAXIQUES POUR LES TYPER ? IL FAUT FORCÉMMENT VENIR M’EMERDER AVEC DES PUTAINS D’INTERFACE DE PARTOUT !? JE HAIS LES INTERFACES !!!!!§§§ ALLLEZ TOUS CREVER EN ENFER !!!§§!!§ GRAAAAAAAAAAAAAAA !!!!!!!!!!

Ok, on se calme…

calmdown

Donc, en Java 8, nous avons le droit à une implémentation foireuse des fonctions anonymes, qui sont, donc, pas anonymes du tout parce qu’elles sont nommées dans une interface. On ne peut donc pas déclarer de lambda sans créer une PUTAIN D’INTERFACE DE MES COUILLES JEVAISTOUSLESCREVERETLESFAIREBRÛLERDANSDELACIDEGROUABLABLAALABLLAAAAAAAAAAAA!!!!

Putain >.<

calmdown

Bref… Sans le côté, fonction jetable, ça limite quand-même sévèrement l’intérêt du truc…

On aurait pu faire autrement ?

Bah ouais, un peu ! Et ça aurait pu être facile, même. Je veux dire : c’est pas pour ce que c’était compliqué de s’inspirer de la syntaxe du C++. Je sais que la communauté Java déteste le C++ qu’elle considère comme un langage dangereux et arriéré, mais quand on en arrive à un tel niveau de dogmatisme, faut commencer à voir un psy ! C’est le Ku Klux Klan de la syntaxe, cette communauté ! Mode Java Power et préservation de la pureté syntaxique !

En fait, j’aurais dû m’en douter que ça ne pouvais qu’être de la merde. Pourquoi ? Parce que Java 8 n’a pas introduit la surcharge des opérateurs. J’explique : finalement, qu’est-ce qu’une fonction anonyme ? Une variable contenant un bout de code, que l’on peut appeler comme une fonction. Autrement dit : c’est un objet. Un objet qui est un objet et qu’on manipule comme un objet. À la différence qu’on peut l’appler comme un fonction. Mais comment telle sorcellerie est possible !?

En fait, c’est très simple, si l’on considère un couple de parenthèses

()

 comme un opérateur. Du coup, une fonction anonyme, c’est un objet auquel on peut appliquer l’opérateur

()

. Mais un opérateur n’est, après-tout, lui-même qu’une fonction ! Par exemple,  prenons l’opérateur binaire

+

appliqué à deux entiers

a

et

b

. Écrire

a + b

pourrait parfaitement s’écrire

a.add(b)

. Hé bien de la même manière, l’opérateur

()

appliqué à l’objet

a

pourrait s’écrire comme

a.call()

C’est comme ça que fonctionne Groovy.

Mais laissez-moi vous présenter Bob. Bob est un spécialiste du langage Java. C’est lui qui sera chargé de vous expliquer pourquoi Java est aussi à chier.

Salut !

Ta gueule. Donc, Bob, dis-nous pourquoi il y a besoin de créer des interfaces à la con pour créer des lambdas ?

Hé bien c’est à cause de la nature fortement typée du langage. Tu vois, il fallait trouver un moyen de déclarer la signature de la méthode. Tu nous as parlé de Groovy, mais Groovy est un langage dynamique. Ce qui fonctionne en Groovy n’aurait pas pu fonctionner en Java.

Et le C++, c’est un langage dynamique, tête de bite ? Ça aurait été trop dur d’ajouter une nouvelle syntaxe pour déclarer des signatures inline ? Ça vous gêne pas, d’habitude ? Ça vous a pas autant emmerdé quand vous avez ajouté les

enum

  à Java 5 ?

Ben, c’est toujours un peu compliqué d’ajouter une nouvelle syntaxe à Java. Tu vois, c’est un peu des gros casse-couilles et ça fait toujours des débats à rallonge pour rajouter une nouvelle fonctionnalité.

Tu métonnes. Mais dis-moi, trou du cul, tu sais comment ils font ça en Groovy ?

Nan. Dis-moi ?

Alors, en Groovy, c’est assez simple. Toute fonction anonyme est représentée par la classe

Closure<V>

. Un

Closure

est un objet qui possède une méthode

call()

et une méthode

call(Object... args)

qui sont la surcharge de l’opérateur

()

respectivement sans arguments et avec arguments. Le type de retour de ces deux méthodes est spécifié par le générique de l’objet. Tu vois, si Java 8 avait ajouté la surcharge des opérateurs à ses objets, les choses auraient été beaucoup plus simples.

Oui mais y’a un problème

Lequel ?

Hé bien, la signature de la méthode

call(Object... args)

indique que c’est un tableau d’

Object

qui est passé en paramètre. Ce signifie que, tu perds le type des paramètres à l’appel.

Et alors, face de cul ? Il suffit de préciser le type à la déclaration :

Function<Integer> f = (String s, Integer i) -> Integer.valueOf(s) + i;

Peut-être, mais si tu veux définir une signature sans déclarer d’objet ?

Facile aussi :

LaFonctionAméricaine extends Function<Void>
{
    @Parameters({String.class, Integer.class})
    public void call(Object... args);
}

Il ne reste plus au compilo qu’à décompacter le tableau et à transtyper comme il faut.

 

Nan mais c’est pas possible, ça.

Pourquoi ?

Parce Java ne devine pas le type des objets à l’exécution.

Ah ouais ? Alors dis-moi, quand je fais ça :

List<Integer> integers = new ArrayList<Integer>();

, mes objets dans ma liste, ils sont de quel type ?

Hmm… C’est compliqué. À quel étape ? À l’exécution ou à la compilation ?

Qu’est-ce qu’on s’en branle ? C’est bien la même chose, non ? C’est pas ce que tu viens de dire ?

Ben… C’est plus compliqué que ça… En fait, à la compilation, c’est une liste d’

Integer

mais, à l’exécution, c’est une liste d’

Object

.

Exactement. On appelle ça le type erasure. C’est un mécanisme qui permettait à Java 5 de rester compatible avec Java 4 qui ne proposait que des listes d’

Object

tout en permettant d’y mettre tout et n’importe quoi sans avoir à transtyper comme un barbare. Et ça fonctionne comment ? Hé bien tout simplement, le compilateur insère des type cast là où il faut pour que les listes continuent de stocker des 

Object

tout en laissant l’utilisateur manipuler d’autres choses. Alors maintenant, dis-moi, imbécile, elle est où, la différence avec ma solution ?

Exactement. Et sinon, t’as pu utiliser map, filter, reduce sur les listes dans Java 8 ?

Une opération de filtrage des listes qui renvoie une liste ? Trop simple…

Une autre nouveauté de Java 8 que j’avais hâte de tester et qui venait avec les lambdas, ce sont les opération de transformation et de filtrage sur les listes. Je vous avait déjà parlé, dans une précédente série d’articles, de la tendance des développeurs Java à multiplier les objets. Ben pour les opération sur les listes, ça a pas loupé, il a fallu qu’ils créent un putain d’objet intermédiaire pour appliquer les opérations : le

Stream

… Bon, dans les faits, c’est pas spécifiquement con, l’idée du

Stream

, c’est de fournir une API qui permette de calculer des opérations en parallèle sur les liste.

Sauf que je devrais pas avoir affaire avec !

L’idée de

map()

, par exemple, c’est de prendre une liste et d’appliquer une opération sur les éléments de cette liste pour en créer une nouvelle. J’ai une liste en entrée, je veux une liste en sortie. C’est simple ! C’est clair !

Alors, oui, mais le développeur de chez Oracle, lui, il emmerde ta clareté…

Petit exemple de filtrage de liste en Java 8 :

List<Integer> integers = new ArrayList<>();
integers = integers.stream().map(integer -> integer + 6)

Voilà, je veux juste ajouter 6 à tous les entiers de ma liste.

Oui mais là, ça va pas compiler.

Putain, t’es encore là, toi ? Oui, je le vois bien que ça va pas compiler. Y’a un problème de typage.

map()

ne renvoie pas une liste, comme il devrait, mais un

Stream

. C’est complètement con ! C’est pas comme ça que ça devrait fonctionner ! Comment je récupère ma liste, moi, maintenant !?

Nan mais c’est simple : il suffit de rajouter

.collect(Collectors.toList())

derrière.

Attends, c’est quoi, ton histoire, là ? Ça fait quoi, ça,

.collect(Collectors.toList())

?

/**
  * Returns a {@code Collector} that accumulates the input elements into a
  * new {@code List}. There are no guarantees on the type, mutability,
  * serializability, or thread-safety of the {@code List} returned; if more
  * control over the returned {@code List} is required, use {@link #toCollection(Supplier)}.
  *
  * @param <T> the type of the input elements
  * @return a {@code Collector} which collects all the input elements into a
  * {@code List}, in encounter order
  */
public static <T>
Collector<T, ?, List<T>> toList() {
    return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
                                (left, right) -> { left.addAll(right); return left; },
                                CH_ID);
}

O___O

Attends, t’es sérieusement, sérieux, là !? Qu’est-ce que c’est que cette bouillasse !? Pourquoi c’est aussi compliqué !? UN LANGAGE DE PROGRAMMATION, ÇA DEVRAIT ÊTRE SIMPLE À UTILISER ! VOUS ÊTES TOUS DES INCAPABLES ! VOUS ALLEZ TOUS BRÛLER EN ENFER ! AAARARRAAGAGAGAAARRARRAGGGAGGGGG !

calmdown

Honnêtement, je comprends pas ce qui traverse la tête des ingénieurs de chez Oracle. Et c’est pas comme si c’était compliqué de faire simple. Prenez Groovy, par exemple. Faire un map sur une liste est une opération extrêment simple :

[12, 16, 42].collect{ it + 6 }

. Pareil pour faire un filter :

[12, 16, 42].findAll{ it == 12 }

.

C’est simple, bordel…

Addendum

On a trouvé, dans les commentaires, que je donnais des exemples à la con. Voici un exemple ablsolument pas trivial tiré du boulot. Il repose sur une

HashMap

de

Range

et de 

BigDecimal

. Il s’agit de rechercher le réel qui est associée à l’ensemble de valeurs entières qui contient un entier passé en paramètre. En Java, ça donne ça :

public BigDecimal getValue(Integer intValue){
    Optional<Map.Entry<Range<Integer>, BigDecimal>> e = map.entrySet()
            .stream().filter(entry -> entry.getKey().contains(intValue)).findFirst();
    return e.isPresent() ? e.get().getValue() : BigDecimal.ONE;
}

En Groovy, ça donne ça :

public BigDecimal getValue(Integer intValue){
    map.find {it.key.contains(intValue)}?.value ?: BigDecimal.ZERO
}

Voilà, voilà… Question lisibilité, c’est autre chose, quand-même…

Déjà 20 avis pertinents dans Java 8 : complexify all the things!

  • Ça fait un moment que j’ai lâché Java, mais ça ne me surprend pas du tout. Pour l’anecdote, ce qui m’a dégoûté de Java, c’est d’avoir dû un jour utiliser un truc qui ressemblait à « XMLDocument document = DocumentBuilderFactory.getInstance().newDocumentBuilder().createDocument() ». Ce machin avait été conçu comme un singleton-fabrique de fabrique de documents XML par un développeur complètement drogué aux design patterns.
  • Cloug
    Hello

    Très chouette comme article.

    En fait, j’ai eu envie de me remettre à Java… (à mon avis je suis maso :-))

    Sur le fond, je crois que les gars de Java ont des impératifs… et des choix de stratégie.

    Sinon, je trouverai chouette, vu ton niveau de compréhension du langage, de proposer une analyse de bout de code ou d une bonne pratique de programmation. Car en fait, il est rare de trouver en français des articles pointus en Java ou en Groovy.

    J’ai en tête le genre d’article que l’on trouve sur Python sur http://sametmax.com/ (je parle évidemment des articles sur le code et non le cul :-))

    J’attends avec impatience ton prochain article ou coup de gueule documenté.

    Merci

    Cloug

  • Cascador
    Tant que tu n’as pas ta carte Jdh non ;p

    Ouais c’est fait mais comme je suis une pourriture, j’ai ajouté le tag humour hé hé hé.

    Tcho !

  • adiGuba
    J’ai pourtant précisé dans mon message en quoi c’était mieux comme cela…

    – Pas de type-function complètement inutile et qui fait doublon.
    – Compatibilité maximum avec tous les codes existant sans avoir à modifier ou recompiler quoi que ce soit.
    – Et cela en proposant exactement les mêmes fonctionnalités.

    S’ils avaient opté pour des delegates comme C#, cela aurait été limité à quelques API conçus spécifiquement pour cela.
    Le tout sans rien apporté de plus…

    Tu sembles avoir avoir un problème avec le fait de déclarer une interface…
    Pourtant le fait de déclarer un delegate cela revient exactement au même, mais implique souvent de dupliquer le type avec une interface…

    Sinon tu affirmes désormais que ce ne sont pas des lambdas : pourquoi donc ???
    Parce que cela me semble juste une affirmation lancé en l’air sans rien derrière…

    Quand à mon exemple sur les Streams, il me semble plus pertinent qu’ajouter « +6 » à tous les éléments…
    L’intérêt des Streams ce n’est pas de modifier une collection, c’est surtout d’écrire des traitements divers qui peuvent se paralléliser facilement.
    Tout comme la source de données, le résultat n’est pas forcément une liste ou une collection, donc oui il faut le préciser.

    Enfin tu dis que tu en a chier pour faire un « map », mais je t’ai bien montré que cela pouvait se faire en une ligne avec replaceAll().

    Désolé si tu sembles mal prendre la critique…

    a++

  • adiGuba
    Heu… Où j’ai dit que les lambdas était inutile ???

    Ce que tu n’arrives pas à comprendre, c’est que les interfaces fonctionnelles et les delegates c’est la même chose : un type avec une seule méthode permettant de décrire le prototype de la fonction-lambda.
    Donc du coup ton affirmation « ce ne sont pas des lambdas » pourrait s’appliquer aussi à C#… et ca reste tout autant stupide !
    Le fait d’utiliser des delegates à la place des interfaces fonctionnelles n’aurait rien apporté de plus, si ce n’est une incompatibilité avec l’immense majorité des APIs existante qui utilisaient déjà cela de facto.

    Et maintenant tu arrives même à reprocher l’existence d’un package avec des fonctions.
    C’est comme si tu critiquais l’existence de plusieurs delegates dans l’API standard de C#

    Et désolé si tu n’arrives pas à comprendre que l’API de Stream ce n’est pas simplement TA méthode map() qui retourne une liste, mais quelque chose de bien plus complet et puissant que cela basé sur l’abstraction du flux (design pattern MapReduce pour plus de détail).
    Alors oui du coup ce n’est pas un simple appel qui fait exactement ce que TOI tu veux… mais cela permet de le faire en 3 lignes de codes, et bien plus encore.

    Enfin non je ne suis pas « prêt à toute la mauvaise foi possible pour défendre mon langage ».
    Ca me fait marrer de te voir dire cela, surtout quand je relis ton « joliiiiiiiii » exemple du sort() en Groovy… et que tu omets complètement d’indiquer le code équivalent en Java.
    Du coup : ne serais-tu pas « prêt à toute la mauvaise foi possible pour conspuer un langage » ???

    Et que dire de ta comparaison en bois entre delegate double f(double) et le type Function !
    L’interface Function de Java devrait plutôt être comparé au delegate Func de C#.

    Ce qui nous donne coté C# :

    public delegate TResult Func(T arg);

    Et en Java :

    public interface Function {
    	R apply(T t);
    }

    Et quand à ton exemple minimaliste :

    delegate double f(double arg);

    Tu aurais dû le comparer avec une interface équivalente :

    interface F {
    	double f(double arg);
    }

    Mais problème ca colle pas avec ton propos, car on voit bien que les deux syntaxes sont très proche.
    La seule différence c’est qu’en Java on donne un nom explicite à la fonction…

    En venant ici le « java fan » pensait trouver des critiques constructives… Il n’en est rien.
    Je n’y ai même pas trouvé mention des deux plus gros reproches que je fais à Java 8 concernant (indirectement) les lambdas :
    – l’absence de type-value qui oblige à démultiplier les prototypes de fonctions.
    – la notion même de checked-exception qui devient encore plus lourdingue avec les lambdas.

    a++

  • adiGuba
    Args bien sûr le typeage Generics a été zappé du code que j’ai posté.
    Il faut lire ceci (en espérant que cela marche) :

    Ce qui nous donne coté C# :

    public delegate TResult Func<in T, out TResult>(T arg);

    Et en Java :

    public interface Function<T, R> {
    	R apply(T t);
    }
  • adiGuba
    Pas d’exemple C# ?
    Curieusement ça tombe mal parce qu’on se retrouve dans le même cas que Java : il faut que le delegate soit déclaré avant de l’utiliser.

    Tu n’arrêtes pas d’opposer les lambdas de Java à celle de C#, alors qu’ils fonctionnent sur le même principe.
    Donc pour être cohérent tu aurais dû opposer Java/C# à Groovy/C++.

    Maintenant perso je ne vous rien d’hallucinant à ce que des langages fortement typés comme Java ou C# t’impose de typer fortement tes lambdas.

    a++

  • adiGuba
    Désolé mais relis-toi : tu opposes les lambdas de Java à celle de Groovy, C++ ET C#.
    Et je ne t’ai jamais reproché la comparaison avec C++ nulle part. D’ailleurs j’ai uniquement basé mes exemples sur le C#…

    Mais il y a un gros problème dans ton résonnement puisque le fonctionnement des lambdas de C# est similaire à celui de Java, mais là cela ne semble pas te poser de problème :

    Java : interface fonctionnelle obligatoire, et namespace en contenant plein => Mon dieu c’est une catastrophe !!
    C# : type delegate obligatoire, et namespace en contenant plein => OK

    a++

Les commentaires sont fermés.