La POO par l'utilisation

La POO par l’utilisation #

Pour ce chapitre, les exemples seront exécutés dans le jshell. Utilisez-le également pour apprendre à manipuler vos premiers objets.

Nous allons découvrir plusieurs classes appartenant à la librairie standard. Nous allons volontairement commencer par une classe mal réalisée pour aborder ensuite des classes connues et de meilleures qualité. L’objectif pédagogique est de:

  • comprendre comment utiliser des objets avant de créer nos propres classes
  • réfléchir à ce qui fait qu’une classe est de bonne qualité
Le mot type sera employé de manière générale pour désigner une classe ou une interface.

Utilisation de la (mauvaise) classe Date #

Nous allons commencer par manipuler des dates. Nous allons commencer par la classe Date du package java.util.Date. Cette classe est maintenant dépréciée (deprecated), mais il s’agit d’un très bon exemple d’un très mauvais design de classe.

Danger
La classe java.util.Date est utilisée à des fins pédagogiques uniquement. N’utilisez jamais cette classe. Nous verrons une version plus moderne de celle-ci par la suite.

Le code ci-dessous permet d’instancier (de créer) un objet de la classe Date.

1
2
3
import java.util.Date; // importation nécessaire

Date d = new Date(1981, 4, 1); // les mois commence à 0 ! pas les jours, ni les années

Dans cet exemple, d est une référence de la classe Date qui pointe sur un objet alloué grâce à l’expression new Date(1981, 5, 1). L’objet est donc une date spécifique. La classe est la définition qui regroupe toutes les dates possibles.

En prenant l’analogie de la théorie des ensembles, Date serait un ensemble et l’objet “1 mai 1981” une valeur de cet ensemble.

L’objet référencé par d a été instancié par un constructeur précédé par new. Un constructeur porte le nom de la classe. Dans cet exemple, le constructeur prend en argument 3 ints que sont l’année, le mois et le jour. L’image ci-dessous résume le vocabulaire:

L’image ci-dessous illustre le monde des références (la pile) et le monde des objets (le tas). Gardez toujours cette image en tête, elle vous servira plus tard. Souvenez-vous que la pile est une structure organisée de manière continue. L’accès est très rapide (empiler, dépiler le premier élément). Le tas est un espace mémoire où l’accès est plus lent.

Regardons dans la documentation officielle de Java 14 quelles sont les fonctionnalités offertes et jouons un peu avec.

Fonctionnalités #

Une fonctionnalités est appelée méthode en POO. Il s’agit d’une fonction rattachée à un objet.

Comparaison de dates:

  • before est une méthode qui prend en argument une autre date et retourne true si d est avant l’argument.
1
2
Date d = new Date(1981, 4, 1);
d.before( new Date(1981, 4, 2) ); // ==> true

Extraction d’informations

  • getMonth nous permet de récupérer le numéro du mois
1
2
3
d.getMonth(); // ==> 4 (pour le mois de mai toujours...)
d.getDate() // ==> 1 (et oui! getDate retourne le numéro du jour dans le mois)
d.getDay() // ==> 0 (getDay retourne le numéro du jour dans la semaine, en l'occ. dimanche)

Critiques #

Si nous lisons la documentation officielle, nous remarquons que cette classe offre très peu de fonctionnalités. Il est difficile de connaître quel était le jour de la semaine le 1er mai 1981. Il est impossible de déterminer à quelle date nous nous trouvions 50 jours avant ou encore si l’année 1981 était une année bissextile.

Mais il y a pire. Nous constations qu’il est possible de modifier une date! Pourquoi souhaiterions-nous changer une date ? Changer la date de naissance d’une personne est une chose. Dans un dossier patient, il doit être possible de supprimer une date pour en mettre une nouvelle. Mais une date. A quel moment nous souhaiterions dire “le premier mai 1990… ben finalement c’était un trois mai 1990” ? Tous les événements qui auraient eu lieu le 1er mai 1990 (qui aurait cette référence) verraient leur date reportée au 3 mai !

1
2
3
4
d.setDate(3);
d.getDate(); // ==> 3 (Je réécris l'histoire)
             //       toutes les événements du 1er mai 1981 ont eu lieu
             //       le 3 mai finalement !

Toutes les occurrences qui référencent d ont maintenant un état indésirable.

Continuons:

1
2
3
4
5
6
7
8
Date d = new Date(1981, 2, 28); /* dernier jour du mois de février en 1981
                                   l'année 1981 n'était pas bissextile */
d.getDate(); // => 28
d.getMonth(); // => 2 (jusqu'ici tout va bien)

d.setDate(29); // Tiens, ça passe
d.getDate(); // => 1 (ok, il l'a corrigé)
d.setDate(-12); // Tiens, ça passe toujours

Conclusions #

Nos dates sont mutables (c’est-à-dire que leur état interne peut changer) et ne devraient pas l’être. La classe offre peu de fonctionnalités intéressantes et nous autorise à réaliser des opérations douteuses.

Conseil
Evitez autant que possible des classes mutables. Il est difficile de leur faire confiance. Il est beaucoup plus facile à comprendre, isoler, tester, corriger ou paralléliser une classe qui ne l’est pas. Privilégiez, si possible, la conception et l’utilisation de classes immutables.

(Réf.: Les principes de privilège)

Conseil
Vos objets doivent toujours se trouver dans un état cohérent et permettre des fonctionnalités permettant des transitions d’un état valide à un autre état valide.

Utilisation de LocalDate #

Java 8 introduit une nouvelle API pour gérer les dates et s’excuse pour le désagrément. Le package java.time contient plusieurs classes pour gérer les dates, les heures, les périodes ou les durées. Concentrons-nous sur LocalDate. Cette dernière est immutable, il ne sera donc pas possible de changer son état. La documentation nous offre beaucoup plus de fonctionnalités.

Remarquons d’abord que cette classe n’offre pas de constructeurs. Un motif proposé à la place est ce que l’on appelle une fabrique : construire l’objet à l’aide d’une méthode statique. Cette technique comporte l’avantage d’avoir un nom plus explicite.

1
2
3
4
import java.time.LocalDate; 
LocalDate d1 = LocalDate.now(); // expressif
LocalDate d2 = LocalDate.of(1981, 5, 1);
LocalDate d3 = LocalDate.parse("1981-05-01");

Les méthodes statiques ne sont pas rattachées à un objet particulier, mais à un type.

Nous remarquons avec l’exemple ci-dessous que Java retourne une exception et nous interdit donc de nous trouver dans un état incohérent.

1
2
3
4
5
6
7
jshell> LocalDate bad = LocalDate.of(1981, 2, 29); // Voyons voyons
|  Exception java.time.DateTimeException: Invalid 
|        date 'February 29' as '1981' is not a leap year
|        at LocalDate.create (LocalDate.java:458)
|        at LocalDate.of (LocalDate.java:272)  
|        at (#55:1) // Ah! Il me retourne une exception : impossible 
|                   // d'obtenir un objet dans un état incohérent

Regardons maintenant un extrait des méthodes d’instances (méthode rattachée à un objet instancié cette fois-ci).

1
2
3
4
5
LocalDate date = LocalDate.parse("1981-05-01");
date.getDayOfMonth(); // ==> 1
date.getDayOfWeek(); // ==> FRIDAY  (sympa ça)
date.isBefore( LocalDate.now() ); // ==> true
date.isLeapYear() // ==> false (leap year signifie année bissextile)

Même si un objet est immutable, il est possible d’obtenir un nouvel état à partir d’un état initial:

1
2
3
date.plusDays(10);
// ==> 1981-05-11 
date // => vaut toujours 1981-05-01

L’objet est immutable mais la référence peut changer :

1
2
date = date.plusDays(10);
date // => vaut maintenant 1981-05-11

Etant donné que les méthodes retournent le nouvel état plutôt que de le modifier, il est possible de chaîner les appels:

1
2
LocalDate.parse("1980-02-29").plusYears(1).minusYears(1); 
// ==> 1980-02-28 !! (l'algèbre des dates reste difficile sur notre calendrier)

Pour rendre une référence constante, il est nécessaire de précéder la déclaration par le mot-clé final :

1
2
final LocalDate date = LocalDate.parse("1981-05-01");
date = date.plusDays(10);

Quiz #

Réalisez une expression permettant, à partir d’un date, de vérifier si l’année dans laquelle elle se trouve est bissextile.

  • Vous avez droit aux méthodes d’instances suivantes: withDayOfMonth, withMonth, minusDays, getDayOfMouth. Consultez la documentation officielle.

Le cas du String #

La classe String est également une classe immutable qui est couramment utilisée pour manipuler des chaînes de caractères. La création se fait simplement, sans constructeur ni méthode grâce à la méthode de conversion implicite dite d’autoboxing.

1
2
3
4
5
6
// String s = new String("Hello"); INUTILE ! et non performant...
String s = "Hello"; // autoboxing
s = s + "Joel!"; // concatenation
// s ==> HelloJoel!
s = s.replace("oJ", "o J");
// s ==> Hello Joel! 

Nous remarquons que l’objet est immutable mais que la référence s n’est pas constante. Il est donc possible de faire pointer s sur une nouvelle version du String.

1
2
String str = "abc";
str = str + str; 

Ces deux instructions sont illustrées ci-dessous:

Exemples de méthodes d’instance #

String test = "Abcdef";
test.contains("bc"); // retourne true 
test.contains("ab"); // retourne false
test.concat("ghi");  // retourne Abcdefghi mais ne modifie 
                     // pas l'instance test
test.toUpperCase();  // retourne "ABCDEF"
test.split("c");     // retourne { "Ab", "def" }

Exemples de méthodes de classe #

String.join(" - ", "This", "course", "is", "awesome");
// ou
String.join(" - ", new String[]{"This","course","is","awesome"});
				// retourne "This - course - is - awesome"

String.valueOf(22); // retourne "22"

Quiz #

Ecrivez, à l’aide du split et du join, une expression qui retourne une nouvelle chaîne de caractères dont tous les ; sont remplacés par des ,. Par ex: "33;44;55" doit retourner "33,44,55".

Attention

L’égalité == se fait par valeur pour les types primitifs (les énumérations et singletons) mais par référence pour les objets. Utilisez toujours la méthode equals pour ces derniers.

Comparaison d’objet #

Il est important d’utiliser la méthode equals pour comparer la structure d’un objet.

String s = "bonjour";
s.toUpperCase() == s.toUpperCase(); // ==> false (les réfs sont différentes)
s.toUpperCase().equals(s.toUpperCase()); // ==> true

Quiz #

Ecrivez une expression qui permet de préciser si une chaine de caractères est en majuscule.

Le cas du StringBuilder #

Cette classe est la version mutable pour la manipulation d’une chaîne de caractères. Les objets immutables ont beaucoup de bénéfices au détriment de l’efficacité. Le Builder est un patron de conception populaire qui permet de manipuler un objet mutable, permettant éventuellement qu’il soit incohérent, avant de le construire pour le transformer dans sa version immutable.

1
2
3
4
5
6
StringBuilder s = new StringBuilder("Hello");
s.append("Joel!");
// s ==> HelloJoel!
s.insert(5, " ");
// s ==> Hello Joel! 
String finalWord = s.toString(); // construction finale en un String

Le cas de List #

Le type List de manipuler des listes dont le nombre d’éléments peut varier contrairement aux tableaux statiques. Ce type est réellement une interface et non une classe. Une interface est un contrat que doit avoir une classe qui la respecte. Lors de la création d’une liste, il est donc important de préciser quelle implémentation est utilisée. Les listes sont génériques : elles prennent en paramètre le type d’éléments qui s’y trouve. La logique d’une liste est la même, quel que soit ce paramètre. Par exemple: retirer un élément, ajouter un élément, compter le nombre d’éléments,… sont des opérations qui sont identiques pour une liste de Strings, une liste d’entiers ou une liste de Schtroumpfs.

Quelques précisions:

  • Le paramètre peut être n’importe quel type non-primitif (une classe ou une interface)
    • List<int> interdit
    • List<Integer> ok
  • ArrayList et LinkedList sont des classes qui peuvent être utilisées

Exemple d’instanciation et ajout de trois éléments:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// List<String> greetings = new ArrayList<String>();
/* Il n'est pas nécessaire de préciser le paramètre de ArrayList, 
   le compilateur sait qu'il s'agit du même que List */
final List<String> greetings = new ArrayList<>();
greetings.add("Hello");
greetings.add("Bonjour");
greetings.add("Hi!");

// Parcours conseillé d'une liste
for (String greeting: greetings) {
  System.out.println(greeting);
}

// Ou
greetings.forEach( greeting -> System.out.println(greeting) );

// Parcours déconseillé no 1
for (int i = 0; i < greetings.size(); i += 1) {
  System.out.println( greetings.get(i) )
}

// Parcours déconseillé no 2
int i = 0;
while ( i < greetings.size() ) {
  System.out.println( greetings.get(i) )
  i += 1;
}

Observez ci-dessus, la référence greetings est une constante (mot-clé final), mais l’objet est quant à lui mutable. Il est impossible d’attribuer greetings à une autre liste mais il est possible de modifier l’état de l’objet pointé.

Il est possible de créer une liste avec ses valeurs en une ligne. L’exemple ci-dessous montre comment créer une liste en spécifiant directement les valeurs. L’exemple utilise également l’inférence du type avec le mot-clé var. Notez que ces constructions retournent des listes immutables. Il est donc impossible d’ajouter ou de supprimer un élément. Vous remarquerez que les débordements ne sont pas autorisés. Il est impossible d’accéder à un indice qui n’existe pas, contrairement au C.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// A partir d'une méthode statique de la classe utilitaires Arrays
var greetings1 = Arrays.asList("Hello", "Bonjour", "Hi!");
// A partir de la méthode statique de List
var greetings2 = List.of("Hello", "Bonjour", "Hi!");

greetings1.add("Hola"); // KO, les objets retournés sont immutables!
|  Exception java.lang.UnsupportedOperationException
|        at AbstractList.add (AbstractList.java:153)
|        at AbstractList.add (AbstractList.java:111)
|        at (#9:1)

// Les indices d'une liste débutent à 0
greetings1.get(2) // ==> "Hi!"
greetings1.get(3) // Exception ! Pas de débordement possible
|  Exception java.lang.ArrayIndexOutOfBoundsException: 
|      Index 3 out of bounds for length 3
|        at Arrays$ArrayList.get (Arrays.java:4164)
|        at (#10:1)

Les tableaux statiques #

L’étude de cas des tableaux statiques est de moindre intérêt pour la POO. Ils offrent peu de fonctionnalités et comportent plusieurs défauts. Privilégiez d’autres collections comme les Lists, Sets, … Par contre, ils restent incontournables. Profitons de ce chapitre pour parcourir quelques exemples d’utilisation.

Un tableau statique possède une taille fixe. Il est possible de modifier son contenu, mais il est impossible de supprimer ou d’ajouter des éléments.

La syntaxe utilise la notation [] pour la déclaration et la modification.

Voici deux manières possibles de créer un tableau statique. En précisant les valeurs ou en déterminant la taille.

type[] tab1 = {a, b, c};
type[] tab2 = new type[TAILLE];

Attention, s’il s’agit d’un tableau d’objet, la valeur null le compose. S’il s’agit de types primitifs, leur valeur “neutre” sera utilisée. Le mot-clé null permet d’indiquer qu’une référence ne pointe sur aucun objet.

Danger
Le concept de la valeur arbitraire null pour signifier une absence de valeur a été inventé en 1964 par Tony Hoare. En 2009, il l’appelle son “erreur à plusieurs milliards” (The Billion Dollar Mistake) tant il a généré de bugs dans l’industrie. Son utilisation est fortement déconseillée.

Conseil
Veillez à ne jamais réaliser une API (classe, composant, module…) qui pourrait retourner une référence null ou l’accepter en argument. Si vous l’employez, faites en sorte de ne jamais l’exposer. Isolez son utilisation et réduisez sa visibilité. Traitez ce null comme s’il s’agissait d’un virus : avec beaucoup de précautions.

Remarques

void et null ne sont pas des types

  • void signifie qu’une fonction ne retourne pas de valeur, il s’agit généralement d’un effet de bord.
  • null signifie qu’un objet n’est pas instancié

Exemples d’instanciation de tableaux

int[] is;  // is = null
int[] empty = {}; // tableau vide
int[] fibonacci = {1, 1, 2, 3, 5, 8, 13};

Exemple d’instanciation de tableaux avec taille arbitraire

float[] fs = new float[4]  // fs = {0.0, 0.0, 0.0, 0.0}
String[] fs2 = new String[4] // fs2 = {null, null, null, null]

Plusieurs dimensions possibles:

double[][] matrix = new double[2][3]; // deux lignes, trois colonnes

Copie et référencement:

double[][] matrix2 = matrix; // /!\ matrix et matrix2 pointent 
                             // sur la même instance
double[][] matrix3 = matrix.clone() // copie le tableau

Un tableau expose uniquement l’attribut length pour connaître le nombre d’éléments. Il ne comporte aucune méthode.

int[] is = new int[12];
is.length; // ==> 12

Plusieurs méthodes statiques de la classe utilitaire Arrays permettent de manipuler un tableau.

int[] is = {4,7,8,9,2,9};
Arrays.sort(is); // trie le tableau, is = {2,4,7,8,9,9};
Arrays.fill(is, 22) // is = {22,22,22,22,22,22};
Arrays.equals(is, new int[]{4,7,8,9,2,9}); // true

Absence de valeurs #

Conseil
Pour représenter l’absence d’articles dans un sac de commissions, retournez un sac de commissions vide !

De la même façon:

  • Pour représenter l’absence de valeurs dans une liste, retournez une liste vide et non une référence null (par ex: List.of() ou Collections.emptyList())
  • Pour représenter l’absence de valeurs dans un tableau statique, retournez un tableau vide (par ex: {})











comments powered by Disqus