Les bonnes et les mauvaises critiques sur Java — Épisode 2 : La syntaxe

La syntaxe est, à mes yeux, le plus gros problème de Java. Le langage n’a pas su évoluer avec le temps au contraire d’autres comme Python. Je sais que je cite souvent les mêmes exemples mais ils sont à mes yeux révélateurs du problème. La syntaxe est lourde à pleurer et il est hallucinant que certains concepts n’ont toujours pas été ajoutés avec le temps à la syntaxe. En voici quelque-uns :

Les chaînes templates et multilignes.

Manipuler des chaînes de caractères est une vraie plaie en Java. Il est impossible de déclarer des chaînes multilignes facilement. Le moindre retour à la ligne oblige à ajouter le caractère spécial

\n

et la moindre insertion de variable oblige à casser la chaîne et la concaténer en utilisant l’opérateur

+

. En Ruby, en Python et en Groovy, ce type de chaîne se déclare avec trois guillemets (en Groovy) :

def val = "lol"
"""Une chaîne
multiligne
avec une variable de valeur ${val}"""

mais la même chose s’écrira comme ça en Java :

String val = "lol";
"Une chaîne\n" +
"multiligne\n" +
"avec une variable de valeur" + val;

C’est atrocement laid et le problème c’est que des opérations de ce type, on en fait constamment en informatique. Ne serait-ce que pour tracer des logs. Ce qui fait qu’une syntaxe spécifique a dû être créée dans certaines bibliothèques. Comme SLF4J pour la gestion des logs, par exemple :

String val = "critique";
log.info("Cette erreur a un niveau de sévérité {}", val);

Et tout en Java est comme ça : le langage est tellement troué qu’on ne peut pas développer un projet décent en entreprise sans faire appel à une biblio courante. La gestion des dates, par exemple, est tellement misérable qu’une biblio a été développée rien que pour ça. Elle est même devenue le standard de facto en Java 7 et a été intégrée à Java 8 !

Les dictionnaires, listes, tableaux et autres itérables

C’est, à mes yeux l’autre monstruosité de Java. Et pourtant, c’est, avec la manipulation de chaînes, l’autre opération la plus courante de l’informatique. En fait, on ne fait même que ça. Même les chaînes de caractères sont en fait des tableaux dans tous les langages courants. Manipuler des données, c’est l’essence même de l’informatique. Et ces données sont presque toujours structurées en piles, files, listes, dictionnaires, bref, en structures séquentielles. Pourtant, leur manipulation est toujours une douleur anale en Java. Pourquoi ? Parce que ces structures se  manipulent encore comme des objets classiques, sans raccourci et sans sucre syntaxique.

Le pire du pire, c’est que les développeurs Java reconnaissent eux-même implicitement que la gestion des itérables en Java est à chier : ils conseillent d’écrire son propre code de management dès qu’on commence à en faire un usage intensif. Par exemple, ces structures utilisent la notation diamant :

LinkedHashMap<String, String> dictionnaire = new LinkedHashMap<String, String>();

et ça, ça peut vite devenir lourd si vous faites des collections d’objets un peu velues. Du coup, il est conseillé d’écrire une classe utilitaire avec des méthodes statiques de construction pour ne pas à avoir à se taper la notation diamant à chaque fois :

LinkedHashMap<String, String> dictionnaire = LinkedHashMaps.newLinkedHashMaps();

C’est d’ailleurs tellement un classique que Google en a même fait une biblio : Guava, qui a commencé comme une simple biblio de méthodes utilitaires pour la gestion des listes en Java. Voici par exemple le code de la méthode qui permet de créer une nouvelle liste dans Guava :

public static <E> ArrayList<E> newArrayList(E... elements) {
  int capacity = computeArrayListCapacity(elements.length);
  ArrayList<E> list = new ArrayList<E>(capacity);
  Collections.addAll(list, elements);
  return list;
}

// Utilisation

ArrayList osList = Lists.newArrayList("Android", "iPhone", "WindowsMobile",
                                      "Blackberry", "WebOS", "Ubuntu", "Windows7", "Max OS X",
                                      "Linux", "OS/2", "Ubuntu", "Windows7", "Max OS X", "Linux",
                                      "OS/2", "Ubuntu", "Windows7", "Max OS X", "Linux", "OS/2",
                                      "Android", "iPhone", "WindowsMobile");

Voilà… Rien de plus… Juste un putain de raccourci. Vous noterez que la fonction est variadique pour pouvoir initialiser la liste à la création. Sans une méthode comme ça, vous seriez obligés d’initialiser comme ça :

String[] values = new String[] { "Android", "iPhone", "WindowsMobile",
                                 "Blackberry", "WebOS", "Ubuntu", "Windows7", "Max OS X",
                                 "Linux", "OS/2", "Ubuntu", "Windows7", "Max OS X", "Linux",
                                 "OS/2", "Ubuntu", "Windows7", "Max OS X", "Linux", "OS/2",
                                 "Android", "iPhone", "WindowsMobile" };

final ArrayList<String> list = new ArrayList<String>();
for (int i = 0; i < values.length; ++i){ list.add(values[i]); }

Oui, il n’y a que les tableaux que l’on peut déclarer inline… En 2015…

La même chose en Groovy, ça donne ça :

def osList = ["Android", "iPhone", "WindowsMobile",
              "Blackberry", "WebOS", "Ubuntu", "Windows7", "Max OS X",
              "Linux", "OS/2", "Ubuntu", "Windows7", "Max OS X", "Linux",
              "OS/2", "Ubuntu", "Windows7", "Max OS X", "Linux", "OS/2",
              "Android", "iPhone", "WindowsMobile"]

Et ça, c’est encore sans vous parler de traiter ces listes. Et pour le traitement des listes, il y a deux opérations courantes en informatique : map et filter. La première, consiste à parcourir une liste pour lui appliquer une opération. La seconde, permet de réduire la liste selon un critère. Par exemple, si dans la liste du dessus vous souhaitiez construire la sous-liste des éléments qui commencent par un a, en Java, cela s’écrit comme ça :

ArrayList<String> subList = new ArrayList<String>();
for(String el: osList){
    if(el.startsWith("a"){ subList.add(el); }
}

La même chose en Groovy s’écrit comme ça :

def subList = osList.findAll{ it.startsWith("a") }

Même chose pour appliquer une opération à tous les éléments d’une liste. Par exemple, vous souhaitez mettre une liste de chaînes en majuscules :

ArrayList2<String> subList = new ArrayList<String>();
for(String el: osList){
    if(el.startsWith("a"){ subList2.add(el.toUpperCase()); }
}

En Groovy :

def subList = osList.*toUppercase();

Oui ! Il y a un putain d’opérateur pour distribuer l’appel d’un méthode à tous les éléments d’une collection ! Et c’est loin d’être la seule chose que Groovy sache faire avec les listes.

Par exemple, c’est tout con, mais Groovy n’a même plus besoin d’une seule boucle

for

  :

for(int i = 0; i < 10; i++){ System.out.println("Appel numéro " + i); }

peut s’écrire :

(1..10).each{ println "Appel numéro ${it}" }

L’opérateur

..

sert à générer des séquences. C’est tellement un classique qu’il existe  en Bash ! Ça peut paraître futile, mais des boucles, y’en a toutes les 10 lignes de code. Mais non, Java n’a toujours pas ça en 2015…

Pas de surcharge d’opérateurs

En fait, ce point va un peu avec le précédent, parce que les collections, c’est typiquement le domaine dans lequel on souhaiterait avoir des opérateurs. En Java, la classe

String

est la seule classe qui bénéficie de l’accès à l’opérateur

+

. C’est l’austérité la plus complète. Pourtant, avoir des opérateurs pour faire des fusions ou des fissions de listes serait tellement pratique. Encore une fois, Java se perd ici en verbosité :

ArrayList<Integer> list1 = Lists.newArrayList(1, 2, 3, 4);
ArrayList<Integer> list2 = Lists.newArrayList(5, 6, 7, 8);
ArrayList<Integer> list1 = new ArrayList(list1).addAll(list2);

Alors qu’en Groovy cela s’écrit en une ligne :

def list = [1, 2, 3, 4] + [5, 6, 7, 8]

De même qu’ajouter un élément à une liste serait tellement plus facile à écrire en utilisant l’opérateur

<<

plutôt que la méthode

.add()

. Mais ça, c’est sans vous parler des représentations des réels.

Par exemple, en Java, il existe la classe

BigDecimal

, qui est une représentation exacte des chiffres à virgule destinée à faire des calculs sans introduire d’erreurs de précision comme ça serait le cas avec un

float

. Au travail, je bosse énormément avec ça. C’est une représentation de nombre et on s’attendrait à ce que les opérateurs mathématiques soient accessibles pour cette classe. Mais non, il faudra utiliser les verbeux

.add()

,

.multiply()

,

.substract()

,

.divide()

. Notez que les développeurs n’ont même pas pris le soin de réduire le nom de leurs méthodes à trois petites lettres :

.add()

,

.mul()

,

.sub()

,

.div()

… C’est vrai, la lisibilité du code, qu’est-ce qu’on s’en branle !?

Ça paraît con, dit comme ça, mais pourtant, voici comment calculer la moyenne d’une liste de

BigDecimal

en Java :

List bigDecimalList = ...
BigDecimal moy = BigDecimal.ZERO;
for(BigDecimal it : bigDecimalList){
    moy = moy.add(it);
}
moy = moy.divide(bigDecimalList.size());

En Groovy, ça s’écrit :

def moy = BigDecimal.ZERO
def bigDecimalList = ...
bigDecimalList.each{ moy += it }
moy /= bigDecimalList.size()

Ce qui est tout de même plus intuitif et facile à lire. En fait, ça peut même être plus simple :

def bigDecimalList = ...
def moy = bigDecimalList.sum() / bigDecimalList.size()

Sémantiquement, c’est même la définition exacte d’une moyenne : la somme des éléments d’une liste divisée par le nombre d’éléments… En pratique, la méthode

sum()

utilise l’opérateur

+

pour calculer la somme des éléments de la liste. C’est possible parce que tout objet en Groovy, possède un opérateur

+

.

Pas getters et de setters natifs

Probablement le défaut le plus étonnant de Java étant donné que sa communauté a été parmis les premières à promouvoir l’utilisation intensive des accesseurs pour les variables. C’est tellement un standard que les accesseurs sont compris nativement par un grand nombre de framework et de bibliothèques Java. Par exemple, le langage de templating Thymeleaf comprend nativement la notation. Écrire

"${user.name}"

dans un template sera automatiquement résolu comme un appel à la méthode

getName()

de l’objet

user

. Pourtant, Java n’intègre toujours pas ce raccourci…

En Groovy, déclarer un accesseur sur une variable permet de le résoudre à l’extérieur de l’objet comme la variable elle-même. Par exemple si je déclare un objet :

class LaClassAmericaine{
  private def uneVariable = "Monde de merde"

  public String getUneVariable(){ return uneVariable }
  public void setUneVariable(String val){ uneVariable = val }
}

Je pourrais utiliser la variable à l’extérieur de l’objet comme si elle était publique car les accesseurs sont résolus comme des raccourcis vers la variable elle-même :

def laClasse = new LaClassAmericaine()

laClasse.uneVariable = """Tiens regarde les anglais ont débarqué.
On va être obligés de passer par derrière.
Tu sais par ce tunnel tout sombre qui sent pas très bon."""

println(laClasse.uneVariable)

Mais pas en Java…

J’avoue que de ce point de vue, je préfère la notation de C♯ qui permet de déclarer des getters et des setters en mode raccourci :

class LaClassAmericaine{
  public string uneVariable{ get; protected set; }

  // En version longue :
  private string _deuxVariables;
  public string deuxVariables
  {
      protected set { this._deuxVariables = value; }
      get { return this._deuxVariables; }
  }
}

Le truc bien dans cette notation, c’est que déclarer des getters et des setters ne produit pas forcément une variable. Si vous n’utilisez pas la notation courte, aucune variable supplémentaire n’est créée. Ça n’est qu’une notation pour déclarer des méthodes. Mais ça permet de dissimuler des méthodes derrière des variables, ce qui est pratique pour présenter une même variable de plusieurs manières différentes. Par exemple un ratio, qui sera présenté comme un ratio avec son propre getter, mais sera présenté comme un pourcentage avec un autre getter sans déclarer de variable supplémentaire.

Des classes, toujours plus de classes

La bibliothèque de Java est très complète, c’est vrai et quand je suis passé du C au Java, ça m’a changé la vie. Mais le problème, c’est probablement qu’elle est trop complète. Elle est en fait bourrée de classes intermédiaires qui n’ont aucune utilité pratique dans la majorité des cas. Les ingénieurs d’Oracle semblent en fait positivement incapables de fournir des abstractions simples à utiliser pour leurs concepts. Exemple pour lire un fichier en Java 7 (ça a changé en Java 8) :

File file = new File("path/to/file");
FileInputStream fis = new FileInputStream(file);

//Construct BufferedReader from InputStreamReader
BufferedReader br = new BufferedReader(new InputStreamReader(fis));

String line = null;
while ((line = br.readLine()) != null) {
  System.out.println(line);
}

br.close();

Pourquoi ai-je besoin de déclarer moi-même un

FileInputStream

, un

BufferedReader

et un

InputStreamReader

!?

Non seulement ça, mais en plus, il existe une seconde solution :

File file = new File("path/to/file");
FileInputStream fis = new FileInputStream(file);

//Construct BufferedReader from FileReader
BufferedReader br = new BufferedReader(new FileReader(fis));

String line = null;
while ((line = br.readLine()) != null) {
  System.out.println(line);
}

br.close();

On peut utiliser un

FileReader

à la place du

InputStreamReader

. Mais pourquoi, alors, ai-je les deux !?

Les développeurs de Java sont les Jésus-Christ de l’informatique : ils pratiquent la multiplication des classes avec assiduité. Vous pourrez par exemple retrouver à côté de la classe

File

, la classe

Files

, une collection de méthodes statiques utilitaires pour les fichiers, la classe

Arrays

, à côté de la classe

Array

, la classe

Collections

à côté de l’interface

Collection

, la classe

Date

et la classe

Calendar

(qui fait la même chose…), etc.

Simplifier la bibliothèque standard semble vraiment être mission impossible. Par exemple, pour formater une chaîne de caractères en Python, on fait comme ça :

"%s est le nom d'un %s mignon tout plein" % "Chat", "félin"

C’est propre et simple,

%

est un opérateur de la classe

str

. Par contre, en Java, vous ne pourrez pas faire :

"%s est le nom d'un %s mignon tout plein".format("Chat", "félin");

Nan, il vous faudra passer par une méthode statique de la classe String :

String.format("%s est le nom d'un %s mignon tout plein", "Chat", "félin");

Pour quelle foutue raison !?

 

À suivre…

Déjà 4 avis pertinents dans Les bonnes et les mauvaises critiques sur Java — Épisode 2 : La syntaxe

  • Ramzus
    Attention, il est tout à fait possible d’insérer des variables dans une chaine de caractères en java sans avoir à utiliser l’opérateur de concaténation.
    On peut ainsi remplacer en java l’exemple que tu cites par :
    String val = « lol »;
    String.format(« Une chaînen multilignen avec une variable de valeur %s », val);
    Ce qui donne, si on ajoute un System.out.print() pour afficher le résultat dans la console :
    Une chaîne
    multiligne
    avec une variable de valeur lol
  • Ramzus
    Ah oui effectivement ^^
    C’est con de pas en parler dès le début, sur le coup, j’ai cru que tu ne connaissais pas cette particularité du langage :)
    Mes excuses !

Les commentaires sont fermés.