I. Introduction

Cet article est le septième d'une série consacrée à la bibliothèque Guava :

I-A. Versions des logiciels et bibliothèques utilisés

Pour écrire ce document, j'ai utilisé les versions suivantes :

  • Java JDK 1.6.0_24-b07 ;
  • Eclipse Indigo 3.7 JEE 64b ;
  • Maven 3.0.3 ;
  • JUnit 4.10 ;
  • Guava 14.0.

J'utilise Java 6, car Java 7 n'est pas encore très répandu en entreprise. C'est ce que je vérifie durant mes conférences lorsque je demande qui utilise Java 7 sur ses serveurs de production, mais que très peu de mains se lèvent...

I-B. Mises à jour

xxx : création

II. La bosse des maths

II-A. Les intervalles (range)

Nous allons voir les fonctionnalités avancées offertes par "Range".

Mais, avant d'aller plus loin, rappelons qu'un intervalle est construit à l'aide de deux valeurs, qui constituent les bornes min et max d'un ensemble de valeurs. Un intervalle peut être ouvert (pour exclure une valeur) ou fermé (pour inclure une valeur) à gauche comme à droite. Par exemple, l'intervalle des valeurs entières noté "[3; 6[" est fermé à gauche et ouvert à droite. Il contient donc les valeurs 3, 4 et 5.

La notation des intervalles ouverts diffère en français et en anglais. Ainsi, l'intervalle noté "[3; 6[" en français s'écrira "[3..6)" en anglais. Ce détail aura son importance par la suite.

Il y a quatre méthodes pour construire les quatre types d'intervalles de base :

  • open, pour ]3; 6[ ;
  • closed, pour [3; 6] ;
  • closedOpen, pour [3; 6[ ;
  • openClosed, pour ]3; 6].
Création d'un intervalle
Sélectionnez

@Test
public void testIntervalleSimple() {
    // Arrange
    final int min = 3;
    final int max = 6;
    final int nb = 5;

    // Act
    final Range<Integer> range = Range.closed(min, max); // ie. [3; 6]

    // Assert
    Assert.assertTrue(range.contains(nb));
}

On peut aussi créer des intervalles à partir d'une seule valeur. Dans ce cas, c'est l'infini négatif ou positif qui sera utilisé pour l'autre borne :

  • greaterThan, pour ]3; +inf[ ;
  • lessThan, pour ]-inf; 3[ ;
  • atLeast, pour [3; +inf[ ;
  • atMost, pour ]-inf; 3] ;
  • all, pour ]-inf; +inf[.
Infini
Sélectionnez

@Test
public void testIntervalleInfini() {
    // Arrange
    final int min = 3;

    final int nb = 12;

    // Act
    final Range<Integer> range = Range.atLeast(min); // ie. [3; +inf[

    // Assert
    Assert.assertTrue(range.contains(nb));
}

On peut aussi préciser en paramètre si on veut un intervalle ouvert ou fermé, ce qui peut être pratique :

Ouvert ou fermé en paramètre
Sélectionnez

@Test
public void testIntervalleSpecifique() {
    // Arrange
    final int min = 3;
    final BoundType gauche = BoundType.CLOSED;
    final int max = 6;
    final BoundType droite = BoundType.CLOSED;
    final int nb = 5;

    // Act
    final Range<Integer> range = Range.range(min, gauche, max, droite); // ie. [3; 6]

    // Assert
    Assert.assertTrue(range.contains(nb));
}

Vous pensez que les intervalles ne s'appliquent qu'à des entiers et vous avez tout faux, car ça peut aussi s'utiliser avec des flottants :

Avec des flottants
Sélectionnez

@Test
public void testIntervalleFlottant() {
    // Arrange
    final double min = 3.2;
    final double max = 6.7;
    final double nb = 5;

    // Act
    final Range<Double> range = Range.closed(min, max); // ie. [3,2; 6,7]

    // Assert
    Assert.assertTrue(range.contains(nb));
}

Maintenant, vous pensez que ça ne s'utilise qu'avec des nombres et vous avez encore tort, car on peut utiliser des strings. Dans ce cas, l'intervalle va simplement permettre de vérifier si un mot est bien alphabétiquement compris entre la borne min et la borne max :

Avec des strings
Sélectionnez

@Test
public void testIntervalleString() {
    // Arrange
    final String min = "idefix";
    final String max = "milou";
    final String prenom = "lassie";

    // Act
    final Range<String> range = Range.closed(min, max); // ie. ["idefix"; "milou"]

    // Assert
    Assert.assertTrue(range.contains(prenom));
}

On a déjà vu, à travers les précédents exemples, qu'on peut demander si une valeur fait partie de l'intervalle. On peut aussi demander si une liste de valeurs en fait partie :

Contient une liste
Sélectionnez

@Test
public void testIntervalleListe() {
    // Arrange
    final int min = 3;
    final int max = 6;
    final List<Integer> liste = Ints.asList(4, 5, 6);

    // Act
    final Range<Integer> range = Range.closed(min, max); // ie. [3; 6]

    // Assert
    Assert.assertTrue(range.containsAll(liste));
}

On peut faire plein de choses sur un intervalle, à commencer par savoir s'il est vide :

Vide ou non vide
Sélectionnez

@Test
public void testIntervalleVide() {
    // Arrange
    final int min = 3;
    final int max = 6;

    // Act
    final Range<Integer> range1 = Range.closed(min, max); // ie. [3; 6]
    final Range<Integer> range2 = Range.closedOpen(min, min); // ie. [4; 4[

    // Assert
    Assert.assertFalse(range1.isEmpty()); // pas vide
    Assert.assertTrue(range2.isEmpty());  // vide
}

Une question encore plus courante est de déterminer si deux intervalles sont inclus l'un dans l'autre :

Inclusion
Sélectionnez

@Test
public void testIntervalleInclusion() {
    // Arrange
    final int min = 3;
    final int max = 6;

    final int min2 = 4;
    final int max2 = 6;

    // Act
    final Range<Integer> grand = Range.closed(min, max); // ie. [3; 6]
    final Range<Integer> petit = Range.closed(min2, max2); // ie. [4; 6]

    // Assert
    Assert.assertTrue(grand.encloses(petit)); 
}

On peut aussi vérifier si deux intervalles se chevauchent :

Chevauchement
Sélectionnez

@Test
public void testIntervalleChevauchement() {
    // Arrange
    final int min = 3;
    final int max = 6;

    final int min2 = 5;
    final int max2 = 9;

    // Act
    final Range<Integer> range1 = Range.closed(min, max); // ie. [3; 6]
    final Range<Integer> range2 = Range.closed(min2, max2); // ie. [5; 9]

    // Assert
    Assert.assertTrue(range1.isConnected(range2));
}

On peut même calculer l'intersection de deux intervalles :

Intersection
Sélectionnez

@Test
public void testIntervalleIntersection() {
    // Arrange
    final int min = 3;
    final int max = 6;

    final int min2 = 5;
    final int max2 = 9;

    final List<Integer> liste = Ints.asList(5, 6);

    // Act
    final Range<Integer> range1 = Range.closed(min, max); // ie. [3; 6]
    final Range<Integer> range2 = Range.closed(min2, max2); // ie. [5; 9]
    final Range<Integer> inter = range1.intersection(range2); // -> [5; 6]

    // Assert
    Assert.assertTrue(inter.containsAll(liste));
}

Si on a besoin d'une union, on utilisera un « RangeSet ».

Si on préfère, on demandera l'intervalle suffisant pour encadrer deux intervalles :

Span
Sélectionnez

@Test
public void testIntervalleSpan() {
    // Arrange
    final int min = 3;
    final int max = 6;

    final int min2 = 8;
    final int max2 = 9;

    final List<Integer> liste = Ints.asList(3, 4, 5, 6, 7, 8, 9);

    // Act
    final Range<Integer> range1 = Range.closed(min, max); // ie. [3; 6]
    final Range<Integer> range2 = Range.closed(min2, max2); // ie. [8; 9]
    final Range<Integer> inter = range1.span(range2); // -> [3; 9]

    // Assert
    Assert.assertTrue(inter.containsAll(liste));
}

Ici, les intervalles [3; 6] et [8; 9] sont disjoints et n'incluent pas le nombre 7.

Enfin, si on a un intervalle dont on veut le contenu, il suffit de préciser le domaine qui nous intéresse :

Domaine
Sélectionnez

@Test
public void testIntervalleAsSet() {
    // Arrange
    final int min = 3;
    final int max = 6;

    final Set<Integer> expected = Sets.newHashSet(3, 4, 5, 6);

    // Act
    final Range<Integer> range = Range.closed(min, max); // ie. [3; 6]
    final Set<Integer> set = range.asSet(DiscreteDomain.integers());

    // Assert
    Assert.assertEquals(expected, set);
}

II-B. Calculs

Dans la vie d'un programme, on doit toujours, à un moment ou un autre, programmer des fonctions mathématiques absentes du JDK. Ce sont généralement des calculs « simples » (sous réserve qu'on considère comme simples des calculs comme ceux des logarithmes), mais qui comportent de nombreux pièges : arrondis, capacité, etc.

La bibliothèque Guava travaille principalement avec les trois classes « IntMath », « LongMath » et « BigIntegerMath » dont l'utilisation spécifique dépend (sans surprise) de la grandeur des valeurs. Les méthodes correspondantes sont capables de lever une exception si le résultat du calcul mathématique est faux. Le JDK, quant à lui, ne le fait pas... Prenons un exemple :

Les entiers bouclent
Sélectionnez

@Test
public void testAdditionStandard() {
    // Arrange
    final int nb = Integer.MAX_VALUE; // ie. 2^31 - 1
    final int expected = Integer.MIN_VALUE; // ie. -2^31

    // Act
    final int result = nb + 1;

    // Arrange
    Assert.assertEquals(expected, result);
}

Dans l'exemple précédent, on tombe sur le cas classique des entiers en Java. Quand on arrive à la capacité maximale positive, on boucle simplement sur les négatifs en partant donc de la valeur minimale. Ce qui est gênant, c'est que le JDK ne râle pas et on peut donc facilement passer à côté d'un bug. Au contraire, Guava ne va pas rester silencieux. Prenons le même test :

ArithmeticException
Sélectionnez

@Test(expected = ArithmeticException.class)
    public void testAdditionGuava() {
        // Arrange
        final int nb = Integer.MAX_VALUE; // ie. 2^31 - 1
        final int expected = Integer.MIN_VALUE; // ie. -2^31

        // Act
        final int result = IntMath.checkedAdd(nb, 1); // AE
    }

Cela ne permet pas de réaliser le calcul, mais le programme sait, au moins, qu'il n'a pas la bonne valeur.

On a des opérations fonctionnant sur le même principe pour les « ints » et les « longs » :

  • checkedAdd ;
  • checkedSubtract ;
  • checkedMultiply ;
  • checkedPow.

La bibliothèque peut également réaliser des calculs donnant un résultat flottant (ie. avec une virgule). Dans ce cas, il est prévu de spécifier le type d'arrondis souhaité :

Arrondi
Sélectionnez

@Test
public void testDivisionArrondieEnDessous() {
    // Arrange
    final int a = 5;
    final int b = 2;
    final RoundingMode arrondi = RoundingMode.DOWN;
    final int expected = 2;

    // Act
    final int result = IntMath.divide(a, b, arrondi);

    // Assert
    Assert.assertEquals(expected, result);
}

Les types d'arrondis offerts par Guava sont :

  • DOWN, qui arrondit en dessous (en valeur absolue) : 2.5 donne 2 et -2.5 donne -2 ;
  • UP, qui arrondit au-dessus (en valeur absolue) ;
  • FLOOR, qui arrondit en dessous : 2.5 donne 2 et -2.5 donne -3 ;
  • CEILING, qui arrondit au-dessus ;
  • UNNECESSARY, qui lève une exception (ArithmeticException) si un arrondi est nécessaire ;
  • HALF_DOWN, qui arrondit au demi-inférieur (en valeur absolue) : 2.4 donne 2 et 2.6 donne 3 ;
  • HALF_UP, qui arrondit au demi-supérieur (en valeur absolue) ;
  • HALF_EVEN, qui arrondit au demi le plus proche.

Voici une série d'exemples pour bien comprendre les différences :

Arrondis
Sélectionnez

@Test
public void testDivisionArrondieEnDessous2() {
    // Arrange
    final int a = -5;
    final int b = 2;
    final RoundingMode arrondi = RoundingMode.DOWN;
    final int expected = -2;

    // Act
    final int result = IntMath.divide(a, b, arrondi);

    // Assert
    Assert.assertEquals(expected, result);
}

@Test
public void testDivisionArrondieEnDessous3() {
    // Arrange
    final int a = 7;
    final int b = 3;
    final RoundingMode arrondi = RoundingMode.HALF_DOWN;
    final int expected = 2;

    // Act
    final int result = IntMath.divide(a, b, arrondi);

    // Assert
    Assert.assertEquals(expected, result);
}

@Test
public void testDivisionArrondieEnDessous4() {
    // Arrange
    final int a = 8;
    final int b = 3;
    final RoundingMode arrondi = RoundingMode.HALF_DOWN;
    final int expected = 3;

    // Act
    final int result = IntMath.divide(a, b, arrondi);

    // Assert
    Assert.assertEquals(expected, result);
}

Les méthodes de calcul qui nécessiteront des arrondis seront :

  • divide ;
  • log2 (logarithme) ;
  • log10 (logarithme en base 10) ;
  • sqrt (racine carrée).

En plus de ces méthodes, Guava permet de calculer quelques opérations qui sont des souvenirs d'école :

  • gcd, qui calcule le Plus Grand Diviseur Commun (PGCD en français) ;
  • mod, qui calcule le modulo (positif) ;
  • pow, qui calcule une puissance (attention ça croit très vite) ;
  • isPowerOfTwo, qui dit si le paramètre est une puissance de 2 ;
  • factorial, qui calcule la fonction « factoriel » (retourne MAX_VALUE en cas de dépassement de capacité, sachant que le factoriel croit plus vite que la puissance) ;
  • binomial(a, b), qui renvoie le coefficient binomial, c'est-à-dire l'élément à la ligne « a » et à la position « b » dans un triangle de Pascal.
PGCD et Pascal
Sélectionnez

@Test
public void testPgcd() {
    // Arrange
    final int a = 12; // 3 x 4
    final int b = 8;  // 2 x 4
    final int expected = 4;

    // Act
    final int pgcd = IntMath.gcd(a, b);

    // Assert
    Assert.assertEquals(expected, pgcd);
}

@Test
public void testPascal() {
    // Arrange
    final int a = 5;
    final int b = 2;
    final int expected = 10;

    // Act
    final int binomial = IntMath.binomial(a, b);

    // Assert
    Assert.assertEquals(expected, binomial);
}

La bibliothèque permet également quelques petites opérations sur les flottants à l'aide de « DoubleMath » :

  • isMathematicalInteger, qui dit si une valeur est entière : 1.0 donne true ;
  • roundToInt, qui arrondit vers un int selon le type d'arrondi spécifié ;
  • roundToLong, qui arrondit vers un long ;
  • roundToBigInteger, qui arrondit vers un BigInteger ;
  • log2, qui calcule le logarithme et arrondit selon le type d'arrondi spécifié.

III. Conclusion

Nous venons de découvrir qu'il ne faut plus avoir peur des maths, enfin quand vous avez Guava sous la main. N'hésitez pas à consulter les autres épisodes de cette série pour découvrir les fonctionnalités fantastiques de la bibliothèque.

Vos retours nous aident à améliorer nos publications. N'hésitez donc pas à commenter cet article sur le forum : Commentez Donner une note à l'article (5)

IV. Remerciements

D'abord j'adresse mes remerciements à l'équipe Guava, chez Google, pour avoir développé une bibliothèque aussi utile et pour la maintenir. Je n'oublie pas tous les contributeurs qui participent notamment sur le forum Guava.

Plus spécifiquement en ce qui concerne cet article, je tiens à remercier l'équipe de Developpez.com et plus particulièrement Bernard Le Roux, Ricky81, Mickael Baron, Yann Caron, Logan, et Cédric Duprez.

V. Annexes

V-A. Liens

Guava : https://code.google.com/p/guava-libraries/

Article "Simplifier le code de vos beans Java à l'aide de Commons Lang, Guava et Lombok" :
http://thierry-leriche-dessirier.developpez.com/tutoriels/java/simplifier-code-guava-lombok/

Blog sur Guava : http://blog.developpez.com/guava/

Article "J2SE 1.5 Tiger" par Lionel Roux :
http://lroux.developpez.com/article/java/tiger/

Article "Présentation de Java SE 7" par F. Martini (adiGuba) :
http://adiguba.developpez.com/tutoriels/java/7/

V-B. Liens personnels

Retrouvez ma page et mes autres articles sur Developpez.com à l'adresse
http://thierry-leriche-dessirier.developpez.com/#page_articlesTutoriels

Suivez-moi sur Twitter : @thierryleriche(https://twitter.com/thierryleriche)@thierryleriche