23 janvier 2014

Les détails sont dans l'épaisseur du trait

Olivier Croisier a publié un article sur ses expérimentations avec Java 8, plus précisément l'introduction des default methods dans les interfaces. Cette nouveauté dans la syntaxe Java a été introduite initialement pour permettre à l'API Collections d'évoluer sans nécessiter à tout un écosystème de librairie de produire de nouvelles versions mises à jour pour Java 8, autrement dit d'attendre 2020 pour que ce soit utilisable concrètement.

Comme l'a très bien analysé Olivier, ces default methods apportent à Java le concept de trait très utilisé en Scala. La tournure fonctionnelle que Java 8 va permettre de faire prendre au langage est un pas majeur, bien plus important amha que l'introduction des génériques en Java 5.

Pour rebondir sur le problème du diamant exposé par Olivier, mon interrogation sur les conflits de méthodes introduites pas les default methods se pose un peu différemment :

nous avons donc une classe Clazz qui implémente deux interfaces Foo et Bar.

1 public interface Foo {
2     public default void helloWorld() {
3         System.out.println("Hello World");}
4 }

1 public interface Bar {
2 }


1 public class Clazz implements Foo, Bar {
2 
3     public static void main(String args[]) throws Exception {
4         new Main().helloWorld();
5     }
6 }

Disons que la première déclare une default method helloworld(). Si Bar définit lui aussi la même default method le compilateur détecte le conflit. Supposons cependant que dans un premier temps Bar n'a pas de telle méthode et que nous compilons notre projet. Nous ajoutons alors a Bar la default method conflictuelle, et re-compilons la classe Bar (et elle seule).

Question : est-ce que notre programme continue de fonctionner comme avant - en gros, est-ce que le bytecode produit fait directement référence à la méthode Foo.helloworld(). Ce qui veut dire que nous ne pouvons plus le re-compiler depuis les sources. Une bonne raison de configurer votre intégration continue avec un "mvn clean install" :)

Je vous laisse essayer par vous même pour avoir la réponse :-D

Java 8 apporte des changements profonds, et expérimenter est la seule façon concrète de comprendre leur impact. Voir ce que fait Yan sur https://github.com/ybonnel/Jdk8Experiments par exemple, je vous encourage à en faire autant.

Je suis en ce moment (exactement en ce moment, dans le TGV Rennes-Paris, connecté en tethering) en train de lire le livre "Functional Programming for Java Developers". Si le livre n'est pas tout juste sorti des presses et ne parle donc qu'à peine de Java 8, et avec une syntaxe obsolète, les concepts fondamentaux sont bien présents et très utiles.


J'entend beaucoup de monde me dire qu'ils n'utiliseront pas Java 8 avant une décennie, bloqués par les contraintes client d'un runtime websphere 6. C'est évidement une réalité, et clairement vous n'aurez pas de traits dans vos applications. Ca n'empêche pas de commence à appréhender les concepts fonctionnels, qui sont un excellent complément à l'approche objet.

En dehors de l'approche fonctionnel que va (mieux) permettre Java 8, ces changements annoncent aussi des bouleversements dans l'écosystème des librairies et frameworks. Exemple type, de nombres frameworks définissent des classes abstraites, que votre code doit étendre. Un jeu d'interface serait plus propre, mais alors impossible de faire évoluer les API. Jenkins par exemple comporte une quantité phénoménale de classes abstraites pour cette raison.

Autre approche si on aime pas les classes abstraites, faire à la façon d'Eclipse, en définissant des interfaces HelloWorld1, HelloWorld2, HelloWorld3, et en testant au runtime quelle interface est implémentée, bref une solution pire que le mal lui même.

Bref, tous les frameworks pre-java 8 - en dehors d'Eclipse - se basent sur des classes abstraites, et pourrissent nos arbres d'héritage. Une opportunité apparaît donc avec Java 8 pour une nouvelle génération de frameworks, basés sur des traits. Le même genre de changement qui a laissé commons-collections sur le carreau avec l'arrivée des génériques et une mise à niveau jamais aboutie, qui a laissé la place à Guava et quelques autres.

Donc, oui, votre client a du Java 5 en production. Oui, c'est une version "End of Life" mais il s'en fout, et vous n'y pouvez rien. Et pourtant, si vous ne voulez pas passer pour un développeur avec des faux airs de Cobol dans quelques années, il est temps pour vous de regarder ce qu'apporte Java 8, d'expérimenter, de découvrir les bénéfices et les concepts de la programmation fonctionnelle, et de pleurer en revenant à votre code sans lambdas ;)

6 commentaires:

Unknown a dit…

Ce serait pas :
new Clazz().helloWorld();
au lieu de
new Main().helloWorld();

Java 8, c'est le bon moment pour s'y mettre, les articles abondent :)

yan bonnel a dit…

Encore pire : si on ajoute la default method à Bar mais avec un type de retour différent.

Michel DAVID a dit…

Ouais c'est quand même un peu tordu ton cas ; tu casses une interface, donc normalement tu changes de version et du coup les projets qui en dépendent doivent migrer et donc recompiler. C'est pareil avant java 8 quand tu ajoutes une méthode à une interface (sauf qu'en java 8 la méthode peut déjà être "héritée" d'une autre interface mais bon...).

yan bonnel a dit…

C'est pas si tordu que ça. En tant que développeur d'une API, ajoute une méthode à une interface. Je décide de mettre une implémentation par défaut pour pas casser la compile de mes utilisateurs. Ben en fait ça peux non seulement la casser mais en plus ne pas être resolvable (si même signature mais retour différent que l'interface d'une autre librairie)

yan bonnel a dit…

Voici le résultat si on ajoute la method à Bar, et qu'on ne recompile que Bar :
Exception in thread "main" java.lang.IncompatibleClassChangeError: Conflicting default methods: Foo.helloWorld Bar.hello
World
at Test.helloWorld(Test.java)
at Test.main(Test.java:21)

Nicolas De Loof a dit…

@Yan ah ben si tu donnes la réponse les gens ne vont pas essayer :)

Bon, ce qui est dérangeant c'est que du coup les frameworks qui veulent faire évoluer leur API sans casser la compatibilité risquent finalement de casser la compatibilité à cause d'autres frameworks.

De bonnes séances de test/debug en perspective