BT

Diffuser les Connaissances et l'Innovation dans le Développement Logiciel d'Entreprise

Contribuez

Sujets

Sélectionner votre région

Accueil InfoQ Articles De Groovy à Java 8

De Groovy à Java 8

Favoris

Les développeurs Groovy auront de l'avance pour adopter les concepts et les nouvelles fonctionnalités offertes par Java 8. Beaucoup d'améliorations apportées dans la prochaine version de Java sont des fonctionnalités que Groovy supporte depuis des années. Depuis des nouvelles syntaxes pour les styles de programmation fonctionnelle, en passant par les lambdas, les flux de collection, et les références de méthode comme objets de première classe, les développeurs Groovy seront avantagés pour l'écriture de code Java dans le futur. Cet article se concentrera sur les parties communes entre Groovy et Java 8, et démontrera comment des concepts Groovy standards sont traduits en Java 8.

Nous commencerons par parler des styles de programmation fonctionnelle, de la manière dont nous utilisons celle-ci en Groovy et de comment les structures de Java 8 proposent un meilleur style de programmation fonctionnelle.

Les closures sont peut-être le meilleur exemple de programmation fonctionelle en Groovy. Dans le détail, une closure en Groovy est uniquement l'implémentation d'une interface fonctionnelle. Une telle interface est un interface qui ne possède qu'une seule méthode à implémenter. Par défaut, les closures Groovy sont une implémentation de l'interface fonctionnelle Callable, qui implémentent la méthode "call".

def closure = {
       "called"
}

assert closure instanceof java.util.concurrent.Callable
assert closure() == "called"

Nous pouvons faire en sorte que Groovy implémente d'autres interfaces fonctionnelles en transtypant une closure.

public interface Function {
         def apply();


def closure = {
        "applied"
} as Function

assert closure instanceof Function
assert closure.apply() == "applied"

Les closures et la programmation fonctionnelle se transposent bien en Java 8. Les interfaces fonctionnelles sont très importantes pour la prochaines version de Java car Java 8 offre des implémentations implicites des interfaces fonctionnelles avec l'introduction des fonctions Lambda.

Les fonctions Lambda peuvent être assimilées à, et utilisées de la même manière que, les closures en Groovy. Implémenter une interface Callable en Java 8 offre la même simplicité que les closures en Groovy.

Callable callable = () -> "called";
assert callable.call() == "called";

Il est important de noter que les fonctions lambda d'une seule ligne en Java 8 offre une instruction de retour implicite, un concept commun avec Groovy.

Dans le futur, Groovy proposera également une implémentation implicite des Méthodes Uniques Abstraites pour les closures, de manière similaire à celles de Java 8. Cette fonctionnalité donne la possibilité aux closures d'utiliser les propriétés et les méthodes d'instance sans devoir dériver entièrement une sous-classe concrète.

abstract class WebFlowScope {
       private static final Map scopeMap = [:]

       abstract def getAttribute(def name);

       public def put(key, val) {
               scopeMap[key] = val
               getAttribute(key)
       } 

       protected Map getScope() {
              scopeMap
       }


WebFlowScope closure = { name ->
       "edited_${scope[name]}"
}

assert closure instanceof WebFlowScope
assert closure.put("attribute", "val") == "edited_val"

En Java 8, les interfaces fonctionnelles dotées de méthodes par défaut offrent une approximation proche du même concept. Les méthodes d'interface par défaut sont un nouveau concept en Java. Elles ont été conçues pour permettre l'améliorations des APIs principales sans violer les contrats d'implémentation construites avec des versions précédentes de Java.

Les fonctions Lambda auront également accès aux méthodes par défaut de l'interface avec lesquelles elles sont liées. Cela implique que des APIs robustes peuvent être directement construites dans une interface, pour procurer des fonctionnalités aux développeurs d'application sans changer la nature du type ni le contrat dans lequel ce type peut être utilisé.

public interface WebFlowScope {
        static final Map scopeMap = new HashMap();

        Object getAttribute(Object key);

        default public Object put(Object key, Object val) {
                scopeMap.put(key, val);
                return getAttribute(key);
        }

        default Map getScope() {
                return scopeMap;
        }
}

static final WebFlowScope scope = (Object key) -> "edited_" + scope.getScope().get(key);
assert scope.put("attribute", "val") == "val";

Les méthodes d'interface par défaut en Java 8 peuvent également nous aider à implémenter des fonctionnalités de Groovy comme la mémoisation et le trampolining. La mémoisation peut être implémentée simplement en créant une interface fonctionnelle avec une méthode d'interface par défaut pour calculer un résultat de manière déterministe ou le récupérer depuis le cache.

public interface MemoizedFunction<T, R> {
  static final Map cache = new HashMap();

  R calc(T t);

  public default R apply(T t) {
    if (!cache.containsKey(t)) {
        cache.put(t, calc(t));
    }
    return (R)cache.get(t);
  }
}

static final MemoizedFunction<Integer, Integer> fib = (Integer n) -> {
  if (n == 0 || n == 1) return n;
  return fib.apply(n - 1)+fib.apply(n-2);
};
assert fib.apply(20) == 6765;

De la même manière nous pouvons utiliser les méthodes d'interface par défaut pour développer une implémentation de Trampoline en Java 8. Le trampolining en Groovy est une stratégie de récursivité qui ne submergera pas la pile d'appel de Java et c'est une fonctionnalité très utile en Groovy lorsqu'un niveau de récursivité profond est nécessaire.

interface TrampolineFunction<T, R> {
   R apply(T...obj);

public default Object trampoline(T...objs) {
  Object result = apply(objs);
  if (!(result instanceof TrampolineFunction)) {
     return result;
  } else {
     return this;
  }
 }
}

// Wrap the call in a TrampolineFunction so that we can avoid StackOverflowError
static TrampolineFunction<Integer, Object> fibTrampoline = (Integer...objs) -> {
  Integer n = objs[0];
  Integer a = objs.length >= 2 ? objs[1] : 0;
  Integer b = objs.length >= 3 ? objs[2] : 1;

  if (n == 0) return a;
  else return fibTrampoline.trampoline(n-1, b, a+b);
};

Au delà de ces fonctionnalités basiques des closures, et celles plus avancées comme la Mémoisation et le Trampolining, certaines des fonctionnalités les plus utiles de Groovy concernent les extensions du langage pour l'API de Collections. En Groovy, nous pouvons utiliser ces extensions pour écrire des opérations sur les listes en utilisant la méthode 'each'.

def list = [1, 2, 3, 4]
list.each { item ->
        println item

Java 8 introduit un concept similaire à Groovy pour itérer sur une collection, en rendant disponible une méthode 'forEach' qui remplace la manière traditionnelle d'itérer sur une liste.

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.forEach( (Integer item) -> System.out.println(item); );

En plus de simplifier les itérations de liste, Groovy apporte aux développeurs d'application nombre de raccourcis dans la manipulation des listes. La méthode 'collect', par exemple, est le raccourci pour faire correspondre les éléments de liste avec de nouveaux types ou de nouvelles valeurs, et pour collecter les résultats dans une nouvelle liste.

def list = [1, 2, 3, 4]
def newList = list.collect { n -> n * 5 }
assert newList == [5, 10, 15, 20]

L'implémentation Groovy de 'collect' passe l'objet qui effectue la correspondance en argument de la méthode collect, alors que Java 8 fournit une implémentation légèrement plus verbeuse. Avec l'API Flux, les développeurs peuvent accomplir la même stratégie de correspondance et de collecte en appelant la méthode 'map' du composant 'stream' de la liste, puis en appelant la méthode 'collect' de ce flux retourné de l'étape précédente. L'API Flux donne aux développeurs la possibilité d'enchaîner de manière fluide plusieurs opérations sur la liste.

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
List<Integer> newList = list.stream().map((Integer n) -> n * 5).collect(Collectors.toList());
assert newList.get(0) == 5 && newList.get(1) == 10 && newList.get(2) == 15 && newList.get(3) == 20;

Groovy donne également aux développeurs des raccourice pour filtrer les listes en utilisant la méthode 'findAll'.

def emails = ['danielpwoods@gmail.com', 'nemnesic@gmail.com', 
'daniel.woods@objectpartners.com', 'nemnesic@nemnesic.com']
def gmails = emails.findAll { it.endsWith('@gmail.com') }
assert gmails = ['danielpwoods@gmail.com', 'nemnesic@gmail.com']

De la même manière, les développeurs Java 8 peuvent filtrer une liste avec l'API Flux.

List<String> emails = new ArrayList<>();
emails.add("danielpwoods@gmail.com");
emails.add("nemnesic@gmail.com");
emails.add("daniel.woods@objectpartners.com");
emails.add("nemnesic@nemnesic.com");
List<String> gmails = emails.stream().filter( (String email) -> email.endsWith("@gmail.com") ).collect(Collectors.toList());
assert gmails.get(0) == "danielpwoods@gmail.com" && gmails.get(1) == "nemnesic@gmail.com";

L'extension Groovy de l'API Collections rend aisé le tri de listes en ajoutant aux Collections une méthode 'sort'. Cette méthode accepte une closure qui sera transtypée en comparateur pendant le tri de la liste si une logique de tri spécifique est nécessaire. De plus, si seul l'inversion de l'ordre de la liste est requis, le méthode 'reverse' peut être appelée et l'ordre inversé.

def list = [2, 3, 4, 1]
assert list.sort() == [1, 2, 3, 4]
assert list.sort { a, b -> a-b <=> b } == [1, 4, 3, 2]
assert list.reverse() == [2, 3, 4, 1]

En utilisant encore l'API Flux de Java 8, nous pouvons trier une liste en utilisant la méthode 'sorted' et récupérer ces résultats avec le Collector 'toList'. La méthode 'sorted' prend en option un argument fonctionnel (comme une fonction Lambda) comme comparateur, de sorte que la logique de tri spécifique et l'interversion des éléments de la liste sont faciles à réaliser.

List<Integer> list = new ArrayList<>();
list.add(2);
list.add(3);
list.add(4);
list.add(1);

list = list.stream().sorted().collect(Collectors.toList());
assert list.get(0) == 1 && list.get(3) == 4;
list = list.stream().sorted((Integer a, Integer b) -> Integer.valueOf(a-
b).compareTo(b)).collect(Collectors.toList());
assert list.get(0) == 1 && list.get(1) == 4 && list.get(2) == 3 && list.get(3) == 2;
list = list.stream().sorted((Integer a, Integer b) -> b.compareTo(a)).collect(Collectors.toList());
assert list.get(0) == 2 && list.get(3) == 1;

En utilisant des APIs fluides, comme le flux de liste, tenter de gérer tout le traitement dans une closure ou une fonction Lambda peut rapidement devenir inmaintenable. Cela peut faire sens, dans ces cas, de déléguer le traitement à une méthode qui est conçue spécialement pour cette unité de travail.

En Groovy, nous sommes capable d'accomplir cela en passant les références de méthode aux fonctions. Une fois la méthode référencé avec l'opérateur '.&', elle est contrainte en closure et peut être passée en argument à une autre méthode. Cela permet de la flexibilité dans l'implémentation de manière inhérente, car le code de traitement peut être introduit depuis des sources externes. Les développeurs peuvent maintenant organiser leurs méthodes de traitement logiquement, et parvenir à une architecture applicative plus maintenable et pérenne.

def modifier(String item) {
  "edited_${item}"
}

def list = ['item1', 'item2', 'item3']
assert list.collect(this.&modifier) == ['edited_item1', 'edited_item2', 'edited_item3']

En Java 8, les développeurs pourront atteindre la même flexibilité en utilisant l'opérateur '::' pour récupérer la référence d'une méthode.

List<String> strings = new ArrayList<>();
strings.add("item1");
strings.add("item2");
strings.add("item3");

strings = strings.stream().map(Helpers::modifier).collect(Collectors.toList());
assert "edited_item1".equals(strings.get(0));
assert "edited_item2".equals(strings.get(1));
assert "edited_item3".equals(strings.get(2));

Les références de méthode peuvent être passées en argument à toute méthode qui nécessite une interface fonctionnelle. La référence de méthode prendra à son tour la forme d'une interface fonctionnelle, et peut être traitée comme telle.

public interface MyFunctionalInterface {
  boolean apply();
}
void caller(MyFunctionalInterface functionalInterface) {
  assert functionalInterface.apply();
}
boolean myTrueMethod() {
  return true;
}
caller(Streaming::myTrueMethod);

Dans Java 8, les développeurs de bibliothèque peuvent opérer des changements aux contrats d'interface sans que les consommateurs n'aient à modifier leur manière de s'interfacer avec la bibliothèque.

Le portage sans accrocs des concepts et des styles de programmation de Groovy vers Java 8 est un pont important entre les deux langages. Groovy a été adopté très fortement dans l'écosystème de la JVM pour sa flexibilité intrinsèque et ses améliorations des APIs Java. Comme beaucoup de celles-ci ont pris place en Java 8, cela signifie que les similitudes entre les deux langages commencent à l'emporter sur leurs différences, ce que cet article tente de souligner. C'est pour ces raisons que les développeurs Groovy expérimentés auront une courbe d'apprentissage plus rapide pour apprendre et s'adapter aux nouvelles APIs, aux fonctionnalités et aux concepts qui vont être introduits dans l'écosystème Java avec Java 8.

A propos de l'Auteur

Daniel Woods est Consultant Senior au sein d'Object Partners, Inc. Il est spécialiste des Architecture Applicatives en Groovy et Grails tout en conservant un intérêt marqué pour Java et d'autres langages basés sur la JVM. C'est un contributeur OpenSource et il sera conférencier pour Gr8Conf et SpringOne 2GX cette année. Daniel peut être contacté par email à danielpwoods@gmail.comou ou via Twitter @danveloper.

Evaluer cet article

Pertinence
Style

Contenu Éducatif

BT