Suite de la présentation de la librairie Java Google Guava après une première introduction publiée ce matin.

Utilisation des méthodes de la classe Objects

Aujourd’hui nous allons parler de Patate. J’ai créé une classe simple pour représenter une Action cotée en bourse. Ma classe Patate a un nom court (un TIC), un prix et une Date d’achat. Je vais faire usage de Google Guava en exagérant un peu le trait, afin de vous montrer ce qu’il est possible de faire.

package org.letouilleur.sample.guava;

import com.google.common.base.Objects;
import org.joda.time.DateTime;

import static com.google.common.base.Preconditions.checkNotNull;


public class Patate {
    private String tic;
    private double price;
    private DateTime buyDate;

    public Patate(String tic, double price, DateTime buyDate) {
        this.tic = checkNotNull(tic);
        this.price = checkNotNull(price);
        this.buyDate = checkNotNull(buyDate);

    }

    public String getTic() {
        return tic;
    }

    public double getPrice() {
        return price;
    }

    public DateTime getBuyDate() {
        return buyDate;
    }

    public int hashCode() {
        return Objects.hashCode(tic, price, buyDate);
    }

    public boolean equals(Object o) {
        if (o == null) {
            return false;
        }
        if (o instanceof Patate) {
            Patate other=(Patate)o;
            return Objects.equal(other.getTic(),getTic()) &&
                    Objects.equal(other.getPrice(), getPrice()) &&
                    Objects.equal(other.getBuyDate(),getBuyDate());
        }
        return false;
    }

    public String toString() {
        return Objects.toStringHelper(this)
                .add("TIC", tic)
                .add("Price", price)
                .add("Created", buyDate)
                .toString();
    }
}

A la ligne 14, la méthode static de Preconditions permet de vérifier si les arguments ne sont pas null avant d’affecter la valeur. C’est un moyen simple d’alléger la lecture du code.

La méthode hashCode ligne 32 s’appuie aussi sur Objects, une class utilitaire bien pratique.

La méthode equals ligne 37 utilise la méthode equals(@Nullable o1, @Nullable o2) toujours dans le but de rendre le code plus lisible.

Enfin notez l’utilisation de l’inner class ToStringHelper.

Je sais qu’un IDE génère ce code technique. Seulement, il ne le génère pas forcément correctement. Ce que vous voyez fera partie de Java 7 ou 8 un jour. Autant s’y habituer maintenant.

Création d’un test JUnit4

Nous allons créer une classe TestPatate afin de s’appuyer sur JUnit 4 pour cet exercice. Je vais avoir besoin d’une liste de patate, que j’appellerai Portfolio. Pour cela, je créé une méthode setup qui sera appelée avant chacun de mes tests unitaires.

package org.letouilleur.sample.guava;

import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import org.joda.time.DateTime;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.util.Comparator;
import java.util.List;

import static junit.framework.Assert.assertEquals;
import static org.junit.Assert.assertFalse;

public class TestPatate {
    private Patate apple;
    private Patate google;
    private Patate yahoo;
    private Patate ebay;
    private List<Patate> portfolio;

   @Before
    public void setupPortfolio() {
        apple = new Patate("AAPL", 100.0, new DateTime().minusDays(5));
        google = new Patate("GOOG", 615.60, new DateTime().minusDays(10));
        yahoo = new Patate("YHOO", 16.192, new DateTime().minusDays(15));
        ebay = new Patate("EBAY", 30.06, new DateTime().minusDays(12));

        portfolio = Lists.newArrayList();

        portfolio.add(apple);
        portfolio.add(google);
        portfolio.add(yahoo);
        portfolio.add(ebay);

    }
...
... // autre code qui viendra plus tard
}

Vous avez vu la ligne 8 ? Suivant le pattern Static Factory, Guava offre un ensemble de builder static qui vous évitent de redire le type déclaré précédemment sur votre variable. Cela devient pratique lorsque vous avez une Map de List de patate par exemple.

Ma première fonction, le willyWoller 2006

Si vous ne connaissez pas les Têtes à Claques, regardez au moins le sketch du Willi Woller, qui parle de Patate.

Java peut faire un peu de Fonctionnel. Je prends le risque de me prendre encore un commentaire de 40 lignes sur la médiocrité de Java et du fonctionnel. Jugeons d’abord sur pièce si vous le voulez bien. Vous allez découvrir qu’il n’est pas nécessaire d’attendre les Closures, les fonctions lambdas ou même la domination de Scala sur le monde.

La première fonction que nous allons voir est la fonction Lists.transform du package Collections de Google Guava.

Elle permet de parcourir une liste d’objet et d’appliquer une Fonction sur chacun des éléments. Une idée d’utilisation ? Je vais créer un portefolio en EURO à partir de mon portfolio en USD en une seule ligne.

Voici le code tout d’abord:

@Test
    public void shouldConvertToEuroThePortfolio() {
        // Convertir en euro
        Function convertToEuro = new Function() {
            @Override
            public Object apply(Object o) {
                if (o == null) {
                    return null;
                }
                Patate toConvert = (Patate) o;
                return new Patate(toConvert.getTic(), toConvert.getPrice() * 0.713012478, toConvert.getBuyDate());
            }
        };

        // Convertir en euro toutes les patates de mon portefeuille
        List<Patate> portfolioEuro = Lists.transform(portfolio, convertToEuro);

        // Verifier la premiere patate
        Patate appleInEuro = portfolioEuro.get(0);
        assertEquals("AAPL", appleInEuro.getTic());
        assertEquals(100 * 0.713012478, appleInEuro.getPrice());

    }

Notez que l’interface Function est utilisée ici comme une classe anonyme. Vous pouvez aussi la déclarer comme static, ou en in-line, cela reste du Java cependant. Ce qui se rapproche le plus d’une fonction lambda est une fonction anonyme en Java.

La fonction transform applique une Function sur chacun des éléments de la liste initiale et retourne une nouvelle liste, l’objet portfolioEuro. Ce n’est donc pas une modification « in-place » comme avec une fonction lambda. Mais cependant vous pouvez vous en servir dès maintenant dans votre code.

Lorsque vous parcourez une collection et que vous appliquez une opération globale, c’est un moyen élégant d’écrire du code orienté fonctionnel.

Filtre moi ce portefolio

Voyons maintenant comment filtrer dynamiquement une liste. Guava propose le principe du Predicate, qui permet ici de retourner true si le prix de la Patate est supérieur à 50.

  @Test
    public void shouldFilterPriceLowerThan50() {
        Predicate with_price_greaterThan_50 = new Predicate() {
            @Override
            public boolean apply(Object o) {
                if (o == null) return false;
                Patate tested = (Patate) o;
                return tested.getPrice() > 50;
            }
        };

        Iterable<Patate> iterablePortfolio = Iterables.filter(portfolio, with_price_greaterThan_50);

        // Iterable est une lazy iteration qui n'est pas instancié tant que
        // l'on appelle pas next()
        // voir 
        for (Patate p : iterablePortfolio) {
            // affichera Apple et Google
            System.out.println(p);
        }
    }

Iterable est un objet lazy, qui décore la liste initiale. C’est en quelques sortes une vue sur la collection originale. En terme de performance donc, cela remplace une itération et un paquet de if. On parle bien de présenter les choses différemment. Je ne veux pas que vous pensiez que Guava fait mieux que ce qu’il est possible de faire. Il le fait de manière plus expressive.

Range moi ces patates

Pour terminer, voyons comment trier notre Portfolio de Patate par nom ou par prix.
Pour cela, je déclare simplement un comparateur Java byTicName.

    @Test
    public void shouldOrderByName() {
        Comparator<Patate> byTicName = new Comparator<Patate>() {
            @Override
            public int compare(Patate patate, Patate patate1) {
                return patate.getTic().compareTo(patate1.getTic());
            }
        };

        List<Patate> orderedByTic = Ordering.from(byTicName).sortedCopy(portfolio);
        assertEquals("AAPL", orderedByTic.get(0).getTic());
        assertEquals("EBAY", orderedByTic.get(1).getTic());
        assertEquals("GOOG", orderedByTic.get(2).getTic());
        assertEquals("YHOO", orderedByTic.get(3).getTic());

  List<Patate> reverseOrderedByTic = Ordering.from(byTicName).reverse().sortedCopy(portfolio);
        assertEquals("YHOO", reverseOrderedByTic.get(0).getTic());
        assertEquals("AAPL", reverseOrderedByTic.get(3).getTic());

    }

Ordering est une classe utilitaire du package com.google.common.collect. Elle permet de trier, de trouver un max ou un min ou d’inverser une liste par exemple. C’est assez pratique et on s’en sert souvent.

Conclusion

J’espère que cette petite introduction vous aura donné envie de tester Google Guava. Il y a de nombreuses fonctions intéressantes à découvrir. Le plus important : utilisez Guava pour améliorer la lisibilité de votre code ou pour réduire le nombre de lignes, particulièrement dans les tests unitaires.

Téléchargez Google Guava sur http://code.google.com/p/guava-libraries/

Voici pour terminer le code complet de mon test unitaire:

package org.letouilleur.sample.guava;

import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import org.joda.time.DateTime;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.util.Comparator;
import java.util.List;

import static junit.framework.Assert.assertEquals;
import static org.junit.Assert.assertFalse;

public class TestPatate {
    private Patate apple;
    private Patate google;
    private Patate yahoo;
    private Patate ebay;
    private List<Patate> portfolio;

    @Before
    public void setupPortfolio() {
        apple = new Patate("AAPL", 100.0, new DateTime().minusDays(5));
        google = new Patate("GOOG", 615.60, new DateTime().minusDays(10));
        yahoo = new Patate("YHOO", 16.192, new DateTime().minusDays(15));
        ebay = new Patate("EBAY", 30.06, new DateTime().minusDays(12));

        portfolio = Lists.newArrayList();

        portfolio.add(apple);
        portfolio.add(google);
        portfolio.add(yahoo);
        portfolio.add(ebay);

    }

    @After
    public void tearDown() {
        portfolio = null;
        apple = null;
        google = null;
        ebay = null;
        yahoo = null;
    }

    @Test(expected = java.lang.NullPointerException.class)
    public void shouldThrownANullPointerExceptionIfTICIsNull() {
        new Patate(null, 10.0, new DateTime());
    }

    @Test(expected = java.lang.NullPointerException.class)
    public void shouldThrownANullPointerExceptionIfPriceIsNull() {
        Double myDouble = null;
        new Patate("test", myDouble, new DateTime());
    }

    @Test(expected = java.lang.NullPointerException.class)
    public void shouldThrownANullPointerExceptionIfBuyDateIsNull() {
        new Patate("test", 10.0, null);
    }

    @Test
    public void shouldBeEquals() {
        DateTime now = new DateTime();
        Patate one = new Patate("test", 10.0, now);
        Patate two = new Patate("test", 10.0, now);
        Patate three = new Patate("test2", 10.0, now);
        Patate four = new Patate("test", 11.0, now);
        Patate five = new Patate("test", 10.0, now.plusDays(1));

        assertEquals(one, two);
        assertFalse(one.equals(three));
        assertFalse(two.equals(three));
        assertFalse(one.equals(four));
        assertFalse(one.equals(five));
    }


    @Test
    public void shouldTestHashCode() {
        DateTime now = new DateTime();
        Patate one = new Patate("test", 10.0, now);
        Patate two = new Patate("test", 10.0, now);
        Patate three = new Patate("test2", 10.0, now);
        Patate four = new Patate("test", 11.0, now);
        Patate five = new Patate("test", 10.0, now.plusDays(1));

        assertEquals(one.hashCode(), two.hashCode());
        assertFalse(one.hashCode() == three.hashCode());
        assertFalse(one.hashCode() == four.hashCode());
        assertFalse(one.hashCode() == five.hashCode());
    }


    @Test
    public void shouldConvertToEuroThePortfolio() {
        // Convertir en euro
        Function convertToEuro = new Function() {
            @Override
            public Object apply(Object o) {
                if (o == null) {
                    return null;
                }
                Patate toConvert = (Patate) o;
                return new Patate(toConvert.getTic(), toConvert.getPrice() * 0.713012478, toConvert.getBuyDate());
            }
        };

        // Convertir en euro toutes les patates de mon portefeuille
        List<Patate> portfolioEuro = Lists.transform(portfolio, convertToEuro);

        // Verifier la premiere patate
        Patate appleInEuro = portfolioEuro.get(0);
        assertEquals("AAPL", appleInEuro.getTic());
        assertEquals(100 * 0.713012478, appleInEuro.getPrice());

    }

    @Test
    public void shouldFilterPriceLowerThan50() {
        Predicate with_price_greaterThan_50 = new Predicate() {
            @Override
            public boolean apply(Object o) {
                if (o == null) return false;
                Patate tested = (Patate) o;
                return tested.getPrice() > 50;
            }
        };

        Iterable<Patate> iterablePortfolio = Iterables.filter(portfolio, with_price_greaterThan_50);

        // Iterable est une lazy iteration qui n'est pas instancié tant que
        // l'on appelle pas next()
        // voir 
        for (Patate p : iterablePortfolio) {
            // affichera Apple et Google
            System.out.println(p);
        }
    }

    @Test
    public void shouldOrderByName() {
        Comparator<Patate> byTicName = new Comparator<Patate>() {
            @Override
            public int compare(Patate patate, Patate patate1) {
                return patate.getTic().compareTo(patate1.getTic());
            }
        };

        List<Patate> orderedByTic = Ordering.from(byTicName).sortedCopy(portfolio);
        assertEquals("AAPL", orderedByTic.get(0).getTic());
        assertEquals("EBAY", orderedByTic.get(1).getTic());
        assertEquals("GOOG", orderedByTic.get(2).getTic());
        assertEquals("YHOO", orderedByTic.get(3).getTic());

        List<Patate> reverseOrderedByTic = Ordering.from(byTicName).reverse().sortedCopy(portfolio);
        assertEquals("YHOO", reverseOrderedByTic.get(0).getTic());
        assertEquals("AAPL", reverseOrderedByTic.get(3).getTic());

    }

}

16 réflexions sur « Google Guava: faire du fonctionnel »

  1. Merci pour cette série d’article je ne connaissais pas du tout cette librairie.
    J’aime bien l’idée de la programmation fonctionnelle même si c’est juste une astuce avec une classe anonyme et un pattern commande 🙂

  2. Ca a l’air pas mal mais pourquoi refaire en permanence ce qui existe déjà ?
    Les commons de jakarta (http://commons.apache.org/collections/ entre autres) permettent déjà de faire tout ça depuis des années. Ok, elles ne supportent pas les generic pour raison de compatibilité mais ça a été porté en Java 5 (http://larvalabs.com/collections/)

    On a donc plusieurs librairies qui font le même boulot : spring, commons de jakarta, google … à croire que chaque framework se doit d’avoir ses librairies techniques.

  3. @Loran les librairies Commons n’apportent pas l’approche fonctionnelle de Guava. Le support des Generic permet en plus d’avoir vraiment du code élégant. Derrière ces librairies on retrouve d’excellents développeurs comme Doug Lea ou Joshua Bloch par exemple qui sont chez Google

  4. Bonjour, merci de parler de cette lib que j’utilise depuis ses débuts il y a deux ans. J’utilisais les common-collections d’Apache, mais l’utilisabilité et la lisibité du code n’ont rien à voir.

    Pour ce qui est du code, juste une remarque en ce qui concerne l’utilisation des prédicats et fontions, pourquoi ne pas avoir utilisé les generics ?

    Function<Patate> convertToEuro = new Function<Patate>() {

    @Override
    public Patate apply(Patate patate) {
    if (patate == null) {
    return null;
    }
    return new Patate(patate.getTic(), patate.getPrice() * 0.713012478, patate.getBuyDate());
    }
    };

    Cela évite une variable local.
    De plus je sais que j’aime bien isoler dans une méthode qui retourne cette fonction. Pour les transformations je l’appelle intoXXX, ici cela donnerait:

    List portfolioEuro = Lists.transform(portfolio,intoEuros());

    [Mise à jour par Nicolas : j’ai remplacé tes < par des &lt; ]

  5. Pour ceux qui utilisent déjà lombok (http://projectlombok.org/) et souhaitent bénéficier d’encore plus d’automatisation il y a http://code.google.com/p/jcurry/

    le code deviendra quelque chose du genre suivant


    @Data
    public class Patate {
    private String tic;
    private double price;
    private DateTime buyDate;
    }

    public class TestPatate {
    @AsFunction
    public static Patate intoEuros(Patate patate) {
    if (patate == null) {
    return null;
    }
    return new Patate(patate.getTic(), patate.getPrice() * 0.713012478, patate.getBuyDate());
    }

    @Test
    public void shouldConvertToEuroThePortfolio() {
    List portfolioEuro = Lists.transform(portfolio,intoEuros());
    ...
    }

    C’est intéressant si vous avez beaucoup de fonctions…

  6. Autorises-tu Guava pour ton entretien développeur ?

    import java.util.List;
    
    import com.google.common.base.Function;
    import com.google.common.base.Joiner;
    import com.google.common.collect.Lists;
    
    public class ListTest {
    
        public static String concatenate(Iterable aTestList) {
            Joiner joiner = Joiner.on(",")
                    .skipNulls();
            return joiner.join(aTestList);
        }
    
        public static List reverseList(final List aTestList) {
            return Lists.reverse(aTestList);
        }
    
        public static List toUppercase(List aTestList) {
            return Lists.transform(aTestList, new Function() {
                @Override
                public String apply(String source) {
                    return source.toUpperCase();
                }
            });
        }
    }
    

    [Modifié par N.Martignole : j’ai ajouté la coloration syntaxique. Bravo pour ta proposition Damien]

  7. @Damien : excellent 🙂 bon là on est tous d’accord, on touche à la version hard-core programmeur… J’ai ajouté ta proposition dans l’article original sur le recrutement.

    Nicolas

  8. Hi!

    Thanks for a nice intro in guava.

    Why do you checkNotNull(price) in Patate constructor?
    You pass double to it, a primitive type double, and it can’t be null.
    So it’s a bit pointless to do that.

    Sorry for English, I’m not sure that my French will be understandable enough 🙂

  9. Intéressant, car la programmation fonctionnelle permet de remplacer avantageusement des boucles ‘for’ plus verbeuses.

    Ce serait encore plus intéressant, si, en Java, une méthode comme la suivante:

    public static Patate intoEuros(Patate patate) {
    if (patate == null) {
    return null;
    }
    return new Patate(patate.getTic(), patate.getPrice() * 0.713012478, patate.getBuyDate());
    }

    pouvait être automatiquement « convertie » (via une forme d’auto-boxing) en

    new Function() {
    public Object apply(Object o) {
    Patate patate = (Patate) o;
    if (patate == null) {
    return null;
    }
    return new Patate(patate.getTic(), patate.getPrice() * 0.713012478, patate.getBuyDate());
    }
    }

    La règle étant simple : si une méthode statique est passée en argument à la place d’une interface qui demande une seule méthode, alors réaliser un auto-boxing de cette méthode statique en un objet qui implémente l’interface si la signature des 2 méthodes est compatible.

    Ainsi, avec un peu plus de sucre syntaxique, la programmation fonctionnelle « passerait mieux » en Java.

    cf. http://www.jroller.com/dmdevito/entry/next_jdk_wishlist_3_a pour plus de détails.

  10. Tu déclares le « price » avec le type primitif double et fait donc un test d’égalité avec la méthode Double.equals au niveau de ce test : assertEquals(100 * 0.713012478, appleInEuro.getPrice());
    Il est préférable pour éviter les surprises d’utiliser la méthode assertEquals(double expected, double actual, double delta) quand tu utilises des doubles.
    Il serait encore mieux de représenter toute valeur monétaire par la classe BigDecimal. cf les 2 posts d’Octo sur le sujet :
    http://blog.octo.com/problemes-courants-imprecision-des-calculs-mathematiques-1ere-partie/
    http://blog.octo.com/problemes-courants-imprecision-des-calculs-mathematiques-2e-partie/

  11. @Pepito en effet tu as raison. Dans la finance on evite BigDecimal en raison du coût de performance que cela entraine. En général on travaille avec l’équivalent d’un double en C

Les commentaires sont fermés.