Tutoriel pour faire des mathématiques avec Guava

Thierry Leriche-Dessirier

Guava est une bibliothèque de chez Google, proposant de nombreux outils pour améliorer les codes des programmes Java. Elle permet, entre autres, de manipuler les collections, de jouer efficacement avec les immutables, d'éviter la gestion des beans nuls, de s'essayer à la programmation fonctionnelle, de cacher les objets, de les simplifier, et bien d'autres choses...

Dans ce septième article sur Guava, vous allez vous réconcilier avec les mathématiques, enfin... Commentez

- Titre : Tutoriel pour faire des mathématiques avec Guava

- Auteur : Thierry Leriche-Dessirier

- Parution : 21 juillet 2013

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 Thierry Leriche-Dessirier. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

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

- I-B. Mises à jour

- II. La bosse des maths

- II-A. Les intervalles (range)

- II-B. Calculs

- III. Conclusion

- IV. Remerciements

- V. Annexes

- V-A. Liens

- V-B. Liens personnels

I - Introduction

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

- introduction et installation ;

- collections ;

- programmation fonctionnelle ;

- utilitaires ;

- cache et concurrence ;

- tout pour vos Strings et primitifs ;

- un peu de math ;

- hash et I/O.

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

@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

@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

@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

@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

@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

@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

@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

@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

@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

@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

@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

@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

@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

@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

@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

@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 coefficientbinomial, c'est-à-dire l'élément à la ligne « a » et à la position « b » dans un triangle de Pascal.

PGCD et Pascal

@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

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" :
https://thierry-leriche-dessirier.developpez.com/tutoriels/java/simplifier-code-guava-lombok/

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

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

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

V-B - Liens personnels

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

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