1. Langage Java

1.1. Le langage Java

1.2. Évolutions du langage

  • Depuis 2006, le code source de Java est libre et open source et l’implémentation de référence se nomme OpenJDK

  • Depuis 2019 et suite à un changement de licence et de modèle de support opéré par Oracle, plusieurs distributions du JDK sont apparues (AdoptOpenJDK, Amazon Corretto, GraalVM, Zulu OpenJDK)

  • Depuis 2019, une nouvelle version de Java SE est publiée tous les six mois. Une version tous les trois ans est une version à support étendue (Long Term Support ou LTS) : Java SE 11 en sept. 2018 (LTS), Java SE 12 en mars 2019, Java SE 13 en septembre 2019, …​

1.3. JRE et JDK

  • Le Java Runtime Environment (JRE) fournit la machine virtuelle Java, les bibliothèques et d’autres composants nécessaires pour l'exécution de programmes Java

  • Le Java Development Kit (JDK) fournit le JRE ainsi qu’un ensemble d’outils pour le développement d’applications

1.4. Compilation et interprétation

  • Le langage Java est à la fois interprété et compilé

  • Un fichier source (.java) est compilé en un langage intermédiaire appelé bytecode (.class)

  • Ce bytecode est ensuite interprété par la machine virtuelle Java

java compil exec

1.4.1. Compilation en ligne de commande (JDK)

Le compilateur javac génère le bytecode en ligne de commande.
$ javac <options> <fichiers source>
Quelques options importantes
--class-path|-classpath|-cp

fixe le chemin de recherche des classes compilées (Classpath)

-d

fixe le répertoire de destination pour les classes compilées

-encoding

précise l’encodage des fichiers sources ("UTF-8", …​)

-g|-g:none

gère les informations pour le débogage

--source|-source

précise la version des fichiers sources (8, …​, 11, …​)

-sourcepath

fixe le chemin de recherche des sources

--target|-target

précise la version de la VM cible (8, …​, 11, …​)

Compilation séparant les sources des fichiers compilés
$ javac -sourcepath src \ (1)
        -source 11 \ (2)
        -d classes \ (3)
        -classpath classes \ (4)
        -g \ (5)
        src/MonApplication.java (6)
1 les sources se trouvent dans le répertoire src
2 les fichiers sources sont conformes à la version 11 de Java
3 les fichiers compilés .class doivent être placés dans le répertoire classes
4 le réperoire classes est ajouté au Classpath
5 les informations de débogage sont intégrées
6 le programme principal se trouve dans src/MonApplication.java

1.4.2. Exécution en ligne de commande (JRE)

java permet d’exécuter le programme
$ java [-options] class [args...]
$ java [-options] -jar jarfile [args...]
Quelques options importantes
class

le nom de la classe contenant le programme principal (le .class doit pouvoir être trouvé dans le CLASSPATH)

--class-path|-classpath|-cp

fixe le chemin de recherche des classes compilées

-jar

exécute un programme encapsulé dans un fichier jar

Exécution}
$ java -cp classes \ (1)
       MonApplication (2)
1 ajoute le répertoire classes au_CLASSPATH_
2 l’exécution débutera par la méthode main de la classe MonApplication

1.5. Classpath

  • Le Classpath précise la liste des bibliothèques ou des classes compilées utilisées par l’environnement Java

  • Le compilateur et la machine virtuelle ont besoin d’avoir accès aux classes compilées

  • Il peut être défini en ligne de commande ou par la variable d’environnement CLASSPATH

1.6. Les indispensables pour développer en Java

1.7. Références

1.7.2. Bibliographie (apprentissage)

1.7.3. Bibliographie (Perfectionnement)

2. Notions de base

2.1. Syntaxe

  • Java possède une syntaxe proche du C

    • se retrouve à tous les niveaux (commentaires, types, opérateurs, …​)

    • chaque instruction se termine par un ;

    • Java différencie majuscules et minuscules

  • Commentaires

    /* …​ */

    le texte entre /* et */ est ignoré

    // …​

    le texte jusqu’à la fin de la ligne est ignoré

2.2. Types primitifs

Un type primitif est un type de base du langage, i.e. non défini par l’utilisateur.

En Java, les valeurs de ces types ne sont pas des objets.
boolean

true ou false

byte

entier signé sur 8 bits (-128 à 127)

short

entier signé sur 16 bits (-32768 à 32767)

int

entier signé sur 32 bits (-231 à 231-1)

long

entier signé sur 64 bits (-263 à 263-1)

float

nombre en virgule flottante simple précision (32 bits IEEE 754)

double

nombre en virgule flottante double précision (64 bits IEEE 754)

char

caractère Unicode sur 16 bits de \u0000 à \uffff

2.3. Littéraux

Un littéral est la représentation dans le code source d’une valeur d’un type.

Entiers

123 de type int, 123L de type long, 0x123F en hexadécimal, 0b101 en binaire (Java 7)

Flottants

1.23E-4 de type double, 1.23E-4F de type float

Booléens

true ou false

Caractères

'a', '\t' ou '\u0000'

Chaînes

"texte"

Même si un littéral existe, le type chaîne de caractères n’est pas un type primitif : les chaînes de caractères sont des instances de la classe java.lang.String.
Null

null (valeur des références non initialisées)

Remarques
  • Il est possible d’inclure le caractère _ dans les littéraux numériques pour en améliorer la lisibilité (Java 7)

  • Une chaîne de caractères sur plusieurs lignes peut être représentée par un bloc de texte (Java 15).

    System.out.println("""
        This is the first line
        This is the second line
        This is the third line
        """);

2.4. Variables

  • Une variable permet d’associer un nom (identifiant) à une valeur.

  • En Java, la valeur peut être directement la valeur d’un type primitif ou une référence.

Exemples de déclarations et initialisations de variables pour des types primitifs
byte aByte = 12;            // Un entier sur 8 bits
short aShort = 130;         // Un entier sur 16 bits
int anInteger = -153456;    // Un entier sur 32 bits

// Remarquer le L pour le litteral de type long
// (sinon erreur a la compilation: entier trop grand)
long aLong = 987654321234L; // Un entier sur 64 bits

// Remarquer le F pour le litteral de type float
// (sinon erreur a la compilation: perte de precision)
float aFloat = 1.3F;        // Un reel simple precision
double aDouble = -1.5E-4;   // Un reel double precision

char aChar = 'S';           // Un caractere
boolean aBoolean = true;    // Un booleen

// La constante est introduite par le mot-cle final
final int zero = 0;       // Une constante
  • Lors de sa déclaration et si elle est initialisée, le type d’une variable locale peut être remplacé par le mot-clé var (Java 10). Le type de la variable est alors inféré depuis le contexte.

2.5. Références

  • Les variables de type tableau, énumération, objet ou interface sont en fait des références

  • La valeur d’une telle variable est une référence vers (l’adresse de) une donnée

  • Dans d’autres langages, une référence est appelée pointeur ou adresse mémoire

  • En Java, la différence réside dans le fait qu’on ne manipule pas directement l’adresse mémoire : le nom de la variable est utilisé à la place

    • pas d’arithmétique des pointeurs en Java

    • les références assurent une meilleure sécurité (moins d’erreurs de programmation)

  • L’association (l’affectation) d’une donnée à une variable lie l’identificateur et la donnée

java reference
Figure 1. Une référence contient un pointeur vers un objet

2.6. Gestion de la mémoire dans la JVM

  • Les variables locales (types primitifs et références vers des objets du tas) sont créées sur la pile (stack)

  • Lors de la création d’un objet, la mémoire est allouée dans une zone mémoire appelée le tas (heap)

  • La libération de la mémoire est automatique et gérée par le ramasse-miette (garbage collector)

    • le GC s’exécute lorque certaines conditions sont réunies

  • Certains paramètres de la JVM permettent de contrôler le GC et les zones mémoires (-mx|-Xmx, -XX:+UseParallelGC, …​)

2.7. Tableaux

  • Un tableau est une structure de données regroupant plusieurs valeurs de même type

  • La taille d’un tableau est déterminée lors de sa création (à l’exécution) et ne varie pas par la suite

  • Un tableau peut contenir des références

    • tableau d’objets ou tableau de tableaux

    • permet de simuler des tableaux à plusieurs dimensions

2.7.1. Déclaration et création de tableaux

  • La déclaration d’une variable de type tableau se fait en ajoutant [] au type des éléments

    int[] unTableau;
    une telle déclaration n’alloue pas de mémoire mais juste une référence sur la pile
  • La création du tableau se fait en utilisant l’opérateur new suivi du type des éléments du tableau et de sa taille entre []

    new int[10];
  • La référence retournée par new peut être liée à une variable

    int[] unTableau = new int[10];
  • Il est possible de créer et d’initialiser un tableau en une seule étape

    int[] unTableau = { 1, 5, 10 };

2.7.2. Manipulation de tableaux

  • L’accès aux éléments d’un tableau se fait en utilisant le nom du tableau suivi de l’indice entre [] (exemple: unTableau[2])

  • La taille d’un tableau peut être obtenue en utilisant la propriété length (exemple: unTableau.length)

  • La méthode de classe arraycopy de System permet de copier efficacement un tableau

Exemples de manipulations de tableaux
int[] arrayOfFiveZeros = { 0, 0, 0, 0, 0};
int[] anArray = new int[5];
assertArrayEquals(arrayOfFiveZeros, anArray);
assertNotSame(arrayOfFiveZeros, anArray); (1)

int[] theSameArray = anArray;
assertSame(anArray, theSameArray); (2)

theSameArray[0] = 12;
assertEquals(12, anArray[0]);
assertEquals(12, theSameArray[0]); (3)

int[] anotherArray = new int[5];
assertArrayEquals(arrayOfFiveZeros, anotherArray);
assertNotSame(arrayOfFiveZeros, anotherArray); (4)

theSameArray = anotherArray;
assertSame(anotherArray, theSameArray);
assertNotSame(anArray, theSameArray); (5)

theSameArray[0] = 21;
assertEquals(12, anArray[0]);
assertEquals(21, theSameArray[0]);
assertEquals(21, anotherArray[0]); (6)
1 les tableaux arrayOfFiveZeros et anArray contiennent les mêmes éléments mais ne sont pas identiques, i.e. ils ne référencent pas le même objet
2 les tableaux anArray et theSameArray sont identiques, i.e. ils référencent le même objet
3 comme anArray et theSameArray sont identiques, la modification est visible par l’intermédiaire des deux références
4 même cas que 1
5 theSameArray référence maintenant anotherArray donc theSameArray et anotherArray sont identiques mais theSameArray et anArray ne le sont plus
6 comme theSameArray et anotherArray sont identiques, la modification est visible par l’intermédiaire des deux références mais anArray n’a pas été modifié

2.8. Opérateurs

Un opérateur est une suite de symboles réalisant une opération spécifique.

Opérateurs par ordre de priorité
  • expr++ expr-- (incrémentation/décrémentation post-fixée)

  • ++expr --expr (incrémentation/décrémentation pré-fixée) +expr (+ unaire) -expr (- unaire) ~ (complément bit à bit) ! (négation)

  • * (multiplication) / (division) % (reste)

  • + (addition/concaténation de chaînes) - (soustraction)

  • << >> >>> (décalage à gauche/à droite/à droite non signé)

  • < > >= (comparaison) instanceof (comparaison à un type)

  • == != (égalité/inégalité)

  • & (ET bit à bit)

  • ^ (OU exclusif bit à bit)

  • | (OU bit à bit)

  • && (ET logique)

  • || (OU logique)

  • ? : (conditionnel ternaire)

  • = += -= *= /= %= &= ^= |= <⇐ >>= >>>= (affectation)

2.9. Expressions, instructions et blocs

  • Une expression est une construction conforme à la syntaxe du langage, formée de variables, d’opérateurs et d’appels de méthode qui est évaluée en une valeur unique.

  • Une instruction est une unité d’exécution. En Java, chaque instruction est suivie d’un ;.

  • Un bloc est un groupe d’instructions entre accolades et peut être utilisé à tout endroit où une instruction peut l’être.

Expression switch (Java 14)
int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> { (1)
        yield 6; (2)
    }
    case TUESDAY -> {
        yield 7;
    }
    case THURSDAY, SATURDAY -> {
        yield 8;
    }
    case WEDNESDAY -> {
        yield 9;
    }
    default -> {
        throw new IllegalStateException("Invalid day: " + day);
    }
};
1 case suivi de
2 yield renvoie le résultat

2.10. Structures de contrôle

2.10.1. Structure conditionnelle

Instruction if
if (aVariable == 0) { (1)
  (2)
  assert aVariable == 0;
} else {
  (3)
  assert aVariable != 0;
}
1 L’évaluation de la condition (entre parenthèse) doit produire un booléen.
2 Ces instructions sont évaluées si la condition est vraie.
3 Ces instructions sont évaluées si la condition est fausse.
Instruction switch
switch (aVariable) { (1)
  case 1:
    (2)
    assert aVariable == 1;
    break;
  case 2:
    (3)
    assert aVariable == 2;
    break;
  default:
    (4)
    assert aVariable != 1 && aVariable != 2;
}
1 L’expression entre parenthèses doit être d’un type primitif byte, short, char, int, de type énuméré, de type String ou d’un type wrapper (Character, Byte, Short, Integer).
2 Instructions exécutées si la valeur de l’expression est 1.
3 Instructions exécutées si la valeur de l’expression est 2.
4 Instructions exécutées dans les autres cas.
Instruction switch "améliorée" (Java 14)
switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> numLetters = 6; (1)
    case TUESDAY                -> numLetters = 7;
    case THURSDAY, SATURDAY     -> numLetters = 8;
    case WEDNESDAY              -> numLetters = 9;
    default -> throw new IllegalStateException("Invalid day: " + day);
};
1 case est suivi de et non plus de :, break n’est plus nécessaire.

2.10.2. Boucles

Instruction for
var numbers = List.of(1, 2, 3, 4);
for (var number: numbers) { (1)
  (2)
}

for (int i = 1; i < 11; i++) { (3)
  (4)
}
1 Forme spécifique pour itérer sur les éléments d’une collection ou d’un tableau (référence de type Iterable)
2 La référence number prendra successivement les valeurs de chaque élément de la collection
3 Trois parties séparées par ; :
  • initialisation (exécutée une fois avant le début de la boucle),

  • tant que la condition est vraie, la boucle s’exécute (évalué en début de boucle),

  • instruction de fin de boucle (exécutée à la fin de chaque itération)

4 i a pour valeurs successives 1, 2, …​, 10
Instructions while et do while
var count = 1;
while (count < 11) { (1)
  (2)
  count++;
}
assert count == 11;
do {
  (3)
  count++;
} while (count < 11); (4)
assert count == 12;
1 Tant que la condition est vraie, la boucle s’exécute (test en début de boucle).
2 count a pour valeurs successives 1, 2, …​, 10
3 count a pour valeur 11
4 La boucle s’exécute tant que la condition est vraie (test en fin de boucle).

2.10.3. Instructions de branchement

break

(avec ou sans label) saute à la fin de la boucle

continue

(avec ou sans label) saute à l’itération suivante de la boucle

return

termine une méthode en retournant éventuellement un résultat

3. Objets, types, interfaces et classes

3.1. Objet

  • Un objet est formé de deux composants indissociables

    • son état, i.e. les valeurs prises par des variables le décrivant (propriétés)

    • son comportement, i.e. les opérations qui lui sont applicables

  • Un objet est une instance d’une classe et peut avoir plusieurs types, i.e. supporter plusieurs interfaces

3.1.1. Création d’objets

  • Un objet est créé à partir d’une classe en utilisant le mot-clé new

  • L’invocation de new

    • provoque la réservation de mémoire pour l’objet,

    • invoque le constructeur qui initialise l’objet, et

    • retourne une référence sur l’objet créé

  • Cette référence doit être affectée (liée) à une variable pour permettre l’accès à l’objet

3.1.2. Déclaration et affectation

  • La syntaxe pour la déclaration d’une variable est type nom

    la déclaration ne crée pas d’objet mais uniquement une référence. La variable est donc invalide tant qu’elle n’est pas liée à un objet (null reference). Une tentative d’accès à une telle référence lancera une exception de type NullPointerException (cf. What is a NullPointerException, and how do I fix it?)
  • Une affectation variable = objet va lier la variable variable à l’objet objet

  • Il est possible de lier la variable lors de sa déclaration (initialisation)

    Si possible, toujours initialiser une variable lors de sa déclaration
Exemple de déclarations et d’instanciation de chaînes de caractères
String s1 = new String();
assertEquals("", s1);

char[] abc = {'a', 'b', 'c'};
String s2 = new String(abc);
assertEquals("abc", s2);
assertNotSame("abc", s2);

String s3 = "xyz";
assertEquals("xyz", s3);

String s4 = s2;
assertEquals(s2, s4);
assertSame(s2, s4);

3.1.3. Accès aux attributs et aux méthodes

  • L’accès aux attributs d’un objet est possible

    • simplement avec le nom de l’attribut dans la classe où il est défini

    • en qualifiant/préfixant avec une référence sur l’objet

  • L’invocation d’une méthode utilise la même syntaxe que pour les attributs suivie de la liste des paramètres

  • L’accès dépend du niveau de contrôle d’accès utilisé lors de la déclaration

Exemple de manipulation de chaînes de caractères
assertEquals(3, s2.length());
assertEquals("b", s2.substring(1, 2));

3.1.4. Destruction

  • Quand un objet n’est plus utilisé, il doit être retiré de la mémoire

  • La destruction des objets en Java est automatique

    • l’environnement d’exécution de Java supprime les objets lorsqu’il détermine qu’ils ne sont plus utilisés

    • un objet est éligible pour la destruction quand plus aucune référence n’est liée à lui

  • Ce processus de suppression s’appelle garbage collector (GC)

  • Avant de détruire l’objet, la méthode protected void finalize() de l’objet est invoquée

    • utilisée pour restituer les ressources allouées par l’objet

    • finalize est membre de la classe Object

    • super.finalize() doit être appelé à la fin de la méthode

La redéfinition de finalize est fortement déconseillée et son usage a même été déprécié dans Java 9 (cf. Avoid finalizers, Effective Java, Joshua Bloch).

3.1.5. Les chaînes de caractères en Java

  • Java fournit trois classes pour les chaînes de caractères :

    • String dédiée aux chaînes de caractères immuables, i.e. dont la valeur ne change pas,

    • StringBuffer pour les chaînes de caractères pouvant être modifiées (contexte mono-thread), et

    • StringBuilder pour les chaînes de caractères pouvant être modifiées (contexte multi-threads).

Création d’une chaîne (String)
  • Une instance de String représente une chaîne au format UTF-16

  • Une chaîne est souvent créée à partir d’un littéral (une suite de caractères entre guillemets)

    • quand Java rencontre un littéral de type chaîne, il crée un objet de type String dont la valeur est le littéral

  • Une chaîne peut aussi être créée en utilisant l’un des constructeurs de String

Quelques accesseurs de String
length()

taille de la chaîne,

charAt(int)

caractère à l’indice spécifié,

substring(int, int)

extraction d’une sous-chaîne,

indexOf(…​), lastIndexOf(…​)

recherche dans la chaîne

  • Un littéral chaîne peut être utilisé à tout endroit où un objet String peut l’être

    • on peut invoquer des méthodes de String sur un littéral chaîne

  • L’opérateur + permet de concaténer des objets de type String

    • c’est le seul opérateur surchargé pour un objet en Java

  • Une chaîne peut être utilisée avec l’instruction switch

Les classes StringBuilder et StringBuffer
  • Les instances disposent à peu prés des mêmes accesseurs que String

  • Quelques mutateurs :

    append(…​)

    ajout de caractères

    delete(…​)

    suppression de caractères

    insert(…​)

    insertion de caractères

  • StringBuilder est optimisée pour un environnement mono-thread

  • StringBuffer est à utiliser dans un contexte multi-threads

Utilisation de String et de StringBuilder
String source = "abcde";
int sourceLength = source.length();
StringBuilder destination = new StringBuilder(sourceLength);

for (int i = (sourceLength - 1); i >= 0; --i) {
  destination.append(source.charAt(i));
}
assertEquals("edcba", destination.toString());

3.2. Type

  • Un type (de donnée) spécifie :

    • l’ensemble des valeurs possibles pour cette donnée (définition en extension),

    • l’ensemble des opérations applicables à cette donnée (définition en intention).

  • Un type spécifie l'interface par laquelle une donnée peut être manipulée

deplacable
Figure 3. Exemple : un type Comparable

3.3. Interface

  • Une interface regroupe des signatures d’opérations et des déclarations de constantes

    Une interface permet donc de définir un type
  • La définition d’une interface comporte une déclaration et un corps

    interface UneInterface extends UneSecondeInterface, UneAutreInterface {
      String uneChaine = "abcde";
      double unDouble = 123.456;
      void uneMethode(int unEntier, String uneChaine);
    }
  • Toutes les méthodes de l’interface sont implicitement public et abstract

  • Toutes les constantes de l’interface sont implicitement public, static, et final

Exemple : définir l'ordre naturel des objets (interface Comparable)
/**
 * This interface imposes a total ordering on the objects
 * of each class that implements it. ...
 */
interface Comparable<T> {
  /**
   * Compares this object with the specified object for order. ...
   */
  int compareTo(T o);
}

3.3.1. Compléments sur les interfaces

  • À l’origine, un interface pouvait contenir uniquement des méthodes abstraites et des constantes

  • Les possibilités ont été étendues au fil des versions successives :

    • les méthodes privées sont supportées (Java 9)

3.3.2. Objet et interface

  • Un objet peut être manipulé par une référence sur une interface si sa classe implémente cette dernière

    la référence étant alors du type de l’interface, seules les méthodes de l’interface sont accessibles
    Exemple : manipuler une chaîne de caractères par le type Comparable
    // La classe String implémente Comparable<String>
    Comparable<String> uneChaine = "abcd";
    if (uneChaine.compareTo("defg") > 0) System.out.println("abcd > defg");
    else if (uneChaine.compareTo("defg") == 0) System.out.println("abcd == defg");
    else System.out.println("abcd < defg");

3.4. Classes

3.4.1. Définition d’une classe

La définition d’une classe comporte deux parties: la déclaration et le corps de la classe

/**
 * Un bref commentaire.
 * Un commentaire plus détaillé...
 * @version juin 2020
 * @author Prénom NOM
 */
class NomClasse { // Déclaration de la classe
  // Corps de la classe
}
  • La déclaration précise au compilateur un certain nombre d’informations sur la classe (son nom, …​)

  • Le corps de la classe contient les attributs et les méthodes (les membres) de la classe

3.4.2. Déclarer des attributs

  • La déclaration d’un attribut spécifie son nom et son type

    /** Description de l'attribut. */
    Type nom;
    /** Description de l'attribut. */
    final Type nom;
  • L’initialisation des attributs peut se faire lors de la déclaration, dans le constructeur, ou dans un bloc d’initialisation d’instance (instance initialization block)

    Si la classe possède au moins un constructeur, il est préférable d’y regrouper les initialisations afin de ne pas les disperser dans le fichier source.
  • final est optionnel et permet de déclarer un attribut qui ne pourra être affecté qu’une unique fois

Pseudo-attribut this
  • Chaque classe possède un attribut privé particulier nommé this qui référence l’objet courant

  • Cet attribut est maintenu par le système et ne peut pas être modifié par le programmeur

  • this n’est accessible que dans le corps de la classe

  • Il est utilisé pour

    • passer l’objet courant en paramètre d’une méthode (unObjet.uneMethode(this))

    • lever certaines ambiguïtés à propos des membres (this.centre = centre)

    • invoquer un autre constructeur dans un constructeur (this(centre, 1))

3.4.3. Définir des méthodes

  • La définition d’une méthode comporte deux parties : la déclaration et le corps (l'implémentation) de la méthode

    /**
     * Brêve description de la méthode.
     * Une description plus longue...
     * @param param1 description du paramêtre
     * @param ...
     * @return description de la valeur de retour
     */
    TypeRetour nomMethode(listeDeParametres) { // Déclaration
      // Corps de la méthode
    }
  • TypeRetour est le type de la valeur retournée ou void si aucune valeur n’est retournée

  • Dans le corps de la méthode, on utilise l’opérateur return pour renvoyer une valeur

  • Un constructeur a le même nom que sa classe et ne possède pas de type de retour

Paramètres de méthode
  • listeDeParamètres est une liste de déclarations de variables séparées par des virgules

    • un paramètre peut être vu comme une variable locale à la méthode

    • final peut préfixer la déclaration si le paramètre ne doit pas être modifié

  • Le passage de paramètres se fait par valeur

    • la valeur d’un paramètre d’un type primitif modifié dans la méthode ne le sera pas à l’extérieur

    • la valeur d’une référence modifié dans la méthode ne le sera pas à l’extérieur

    • dans la méthode, les appels de méthode sur un paramètre de type référence seront appliqués sur l’objet original (comme avec un pointeur en C)

3.4.4. Contrôle d’accès aux membres

  • Le contrôle de l’accès aux membres permet de contrôler l’interface d’une classe

  • Le niveau d’accès est précisé en ajoutant un mot-clé devant la déclaration du membre (attribut ou méthode)

  • Il peut prendre l’une des valeurs private, public, protected ou être absent

    Niveau Classe Module Sous-classe Extérieur

    private

    X

    aucun

    X

    X

    protected

    X

    X

    X

    public

    X

    X

    X

    X

  • La restriction d’accès s’applique au niveau de la classe et non pas de l’objet

En général, afin de maintenir l’encapsulation, les attributs sont déclarés avec private et les méthodes avec le niveau le plus restrictif possible.

3.4.5. Un exemple de classe pour des nombres complexes

Cet exemple est volontairement simplifié et sera étoffé par la suite. Une classe complexe plus complète peut être trouvée dans la bibliothèque Apache Commons Math.
Définition de Complex
package fr.uvsq.refcardjava.classes;

/**
 * La classe <code>Complex</code> représente un nombre complexe.
 *
 * @author hal
 * @version 2020
 */
public class Complex {
  /** La partie réelle du nombre. */
  private final double real;
  /** La partie imaginaire du nombre. */
  private final double imaginary;

  /**
   * Construit un complexe à partir d'une partie réelle et imaginaire.
   * @param real      la partie réelle
   * @param imaginary la partie imaginaire
   */
  public Complex(double real, double imaginary) {
    this.real = real;
    this.imaginary = imaginary;
  }

  /**
   * Construit un complexe uniquement à partir d'une partie réelle.
   * La partie imaginaire sera égale à zéro.
   *
   * @param real      la partie réelle
   */
  public Complex(double real) {
    this(real, 0.0);
  }

  /**
   * Retourne la partie réelle.
   * @return la partie réelle
   */
  public double getReal() {
    return real;
  }

  /**
   * Retourne la partie imaginaire.
   * @return la partie imaginaire
   */
  public double getImaginary() {
    return imaginary;
  }

  /**
   * Retourne un nouveau nombre complexe, somme du nombre courant et du paramètre.
   *
   * @param rhs le nombre complexe en partie droite de l'opération
   * @return la somme de l'objet courant et de rhs
   */
  public Complex add(Complex rhs) {
    if (rhs == null) throw new IllegalArgumentException("Parameter can not be null.");
    return new Complex(real + rhs.real, imaginary + rhs.imaginary);
  }
}
Utilisation de la classe Complex
package fr.uvsq.refcardjava.classes;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class ComplexTest {
  @Test
  public void shouldCreateAComplexNumber() {
    var c = new Complex(1.0, 2.0); // 1 + 2i
    assertEquals(1.0, c.getReal());
    assertEquals(2.0, c.getImaginary());
  }

  @Test
  public void shouldCreateAComplexNumberFromARealNumber() {
    var c = new Complex(1.0); // 1 + 0i
    assertEquals(1.0, c.getReal());
    assertEquals(0.0, c.getImaginary());
  }

  @Test
  public void shouldAddTwoComplex() {
    var c1 = new Complex(1.0, 2.0); // 1 + 2i
    var c2 = new Complex(2.0, 3.0); // 2 + 3i
    var result = c1.add(c2);
    assertEquals(3.0, result.getReal());
    assertEquals(5.0, result.getImaginary());
  }

  @Test
  public void shouldAddAComplexWithNull() {
    var c1 = new Complex(1.0, 2.0); // 1 + 2i
    Exception exception = assertThrows(IllegalArgumentException.class, () -> c1.add(null));
    assertEquals("Parameter can not be null.", exception.getMessage());
  }
}

3.4.6. Membre de classe

  • Un membre de classe est un attribut ou une méthode partagé par toutes les instances de la classe

  • Il se déclare avec le mot-clé static

    L’accès à un membre de classe se fait de préférence par l’intermédiaire de la classe et non d’une référence sur un objet
  • Pour un attribut de classe, le système alloue un espace mémoire pour un attribut par classe (et non pas un attribut par instance)

  • Un attribut de classe est souvent utilisé pour définir une constante (static final)

    public static final double E = 2.718281828459045d;
    public static final double PI = 3.141592653589793d;
  • L’initialisation d’un attribut de classe peut se faire directement ou en utilisant un bloc d’initialisation statique

    • c’est un bloc de code Java classique commençant par le mot-clé static et placé dans le corps de la classe

Une méthode de classe ne peut pas accéder aux attributs d’instance (pas d’accès à this)

3.4.7. Exemple : compter les instances de nombres complexes

Définition de ComplexWithCounter
package fr.uvsq.refcardjava.classes;

/**
 * La classe <code>ComplexWithCounter</code> est un exemple d'usage de membre de classe.
 *
 * @author hal
 * @version 2020
 */
public class ComplexWithCounter {
  /** Le nomdre de complexe */
  private static long complexCounter = 0L;
  /** La partie réelle du nombre. */
  private final double real;
  /** La partie imaginaire du nombre. */
  private final double imaginary;

  /**
   * Construit un complexe à partir d'une partie réelle et imaginaire.
   * @param real      la partie réelle
   * @param imaginary la partie imaginaire
   */
  public ComplexWithCounter(double real, double imaginary) {
    this.real = real;
    this.imaginary = imaginary;
    complexCounter++;
  }

  /**
   * Retourne le nombre de complexes
   * @return le nombre d'instances
   */
  public static long getComplexCounter() {
    return complexCounter;
  }

  public static void resetComplexCounter() {
    complexCounter = 0;
  }

  /**
   * Décrémente le compteur quand l'objet est détruit.
   */
  @Override
  protected void finalize() throws Throwable {
    --complexCounter;
    super.finalize();
  }

  /**
   * Retourne la partie réelle.
   * @return la partie réelle
   */
  public double getReal() {
    return real;
  }

  /**
   * Retourne la partie imaginaire.
   * @return la partie imaginaire
   */
  public double getImaginary() {
    return imaginary;
  }
}
Utilisation de la classe ComplexWithCounter
package fr.uvsq.refcardjava.classes;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

class ComplexWithCounterTest {
  @BeforeEach
  public void setup() {
    ComplexWithCounter.resetComplexCounter();
  }

  @Test
  public void shouldHaveNoInstances() {
    assertEquals(0, ComplexWithCounter.getComplexCounter());
  }

  @Test
  public void shouldHaveThreeInstances() {
    var c1 = new ComplexWithCounter(0.0, 0.0);
    var c2 = new ComplexWithCounter(1.0, 1.0);
    var c3 = new ComplexWithCounter(2.0, 2.0);

    assertEquals(3, ComplexWithCounter.getComplexCounter());
  }

  @Test
  public void shouldHaveAlsoThreeInstances() {
    var c1 = new ComplexWithCounter(0.0, 0.0);
    var c2 = new ComplexWithCounter(1.0, 1.0);
    var c3 = new ComplexWithCounter(2.0, 2.0);
    var c4 = c3;

    assertEquals(3, ComplexWithCounter.getComplexCounter());
  }

  @Disabled
  @Test
  public void shouldHaveTwoInstances() throws InterruptedException {
    var c1 = new ComplexWithCounter(0.0, 0.0);
    var c2 = new ComplexWithCounter(1.0, 1.0);
    var c3 = new ComplexWithCounter(2.0, 2.0);

    assertEquals(3, ComplexWithCounter.getComplexCounter());

    c1 = null;
    System.gc();
    Thread.sleep(1000);

    assertEquals(2, ComplexWithCounter.getComplexCounter());
  }
}
Si cet exemple illustre en effet la notion de membre de classe, il ne fonctionne pas en pratique pour compter le nombre d’instances d’une classe (cf. How to Count Number of Instances of a Class et en particulier la réponse utilisant PhantomReference). En effet, il n’existe pas en Java de moyens simples pour forcer la destruction d’objet et donc l’appel de finalize. De plus, la redéfinition de cette dernière est fortement déconseillée et son usage a même été déprécié dans Java 9 (cf. Avoid finalizers, Effective Java, Joshua Bloch).

3.4.8. Le programme principal en Java : la méthode main

  • Le point d’entrée d’une application Java est une méthode de classe nommée main

  • Lors de l’exécution, l’interpréteur Java est invoqué avec le nom d’une classe qui doit implémenter une méthode main

  • La déclaration de la méthode main est :

    class Application {
      public static void main(String[] args) {
    
      }
    }
  • Le paramètre de main est un tableau de chaînes de caractères contenant les arguments de ligne de commande passés lors de l’appel du programme

On limite en général le code se trouvant dans le main au strict minimum: création d’un objet application et invocation d’une méthode. En effet, en programmation objet, un programme est composé d’un ensemble d’objets qui interagissent et non pas de méthodes de classe s’appelant les unes les autres. Cette dernière approche s’apparente plus à de la programmation procédurale.

3.5. Énumération

  • Un type énuméré permet de contraindre l’ensemble des instances possibles pour un type donné

  • En Java, le type énuméré enum est en fait une classe dont les instances sont connues et déclarées lors de la compilation

  • Les instances d’un type énuméré sont des objets et peuvent donc être utilisées partout où un objet peut l’être

  • Un type énuméré étant une classe, il peut contenir des méthodes et des attributs

  • De plus, le compilateur ajoute automatiquement certaines méthodes

    • values() retourne un tableau contenant les constantes dans l’ordre de leur déclaration

    • un type énuméré hérite implicitement de la classe Enum

Plus d’information dans le tutoriel The Java Tutorials - Enum Types ou dans la section 8.9. Enum Types de la spécification du langage.
Exemple : un singleton pour le programme principal
package fr.uvsq.refcardjava.classes;

/**
 * La classe <code>Application</code> contient la méthode <code>main</code> du programme.
 * Cette implémentation s'appuie sur le design pattern Singleton.
 *
 * @author hal
 * @version 2020
 */
public enum Application {
  APPLICATION;

  /**
   * Méthode principale du programme.
   * @param args les arguments de ligne de commande
   */
  public void run(String[] args) {
    // ...
  }

  /**
   * Point d'entrée du programme.
   * @param args les arguments de ligne de commande
   */
  public static void main(String[] args) {
    APPLICATION.run(args);
  }
}

Cette approche possède plusieurs avantages :

  1. la méthode main est utilisée à minima ce qui favorise une approche objet

  2. le singleton est accessible dans tout le programme (Application.APPLICATION)

  3. c’est un bon endroit pour conserver les informations partagées (configuration, …​)

Une surexploitation de cette classe conduit à l’anti-pattern God object.

3.6. Généricité

  • La généricité permet de paramétrer une classe par un ou plusieurs paramètres formels (généralement des types)

  • La généricité permet de définir une famille de classes, chaque classe étant instanciée lors du passage des paramètres effectifs

  • Cette notion est orthogonale au paradigme objet : on parle de programmation générique

  • En Java, les paramètres formels de type sont placés entre “<” et “>”

  • Un paramètre effectif est obligatoirement une classe (pas un type primitif)

  • Le mécanisme implémentant la généricité en Java se nomme Type erasure

    • Ce mécanisme supprime toute trace de la généricité dans le bytecode ⇒ il n’existe pas d’information concernant la généricité à l’exécution

  • Il peut être souhaitable de limiter les paramètres possibles, la généricité est alors contrainte

3.6.1. Classe générique

  • Les paramètres formels de type sont placés entre “<” et “>” juste après le nom de la classe

    class Complex<T> {
      //...
    }
  • Le paramètre de type peut ensuite être utilisé comme tout autre type dans la définition de la classe

    • sans contrainte précisée sur le type, aucune méthode ne peut être appelée sur une instance de ce type (impossible de vérifier statiquement)

  • La déclaration d’une variable de ce type nécessite de passer le type effectif à la classe paramétrée

    Complex<Float> c = //...
  • La création d’une instance ne nécessite pas de répèter le type effectif (diamond notation)

    Complex<Float> c = new Complex<>(/* ... */);
Définition de la classe générique Complex
package fr.uvsq.refcardjava.classes;

/**
 * La classe <code>Complex</code> représente un nombre complexe générique dont le type de la partie entière et imaginaire est paramétré.
 *
 * @author hal
 * @version 2020
 */
public class GenericComplex<T> {
  /** La partie réelle du nombre. */
  private final T real;
  /** La partie imaginaire du nombre. */
  private final T imaginary;

  /**
   * Construit un complexe à partir d'une partie réelle et imaginaire.
   * @param real      la partie réelle
   * @param imaginary la partie imaginaire
   */
  public GenericComplex(T real, T imaginary) {
    this.real = real;
    this.imaginary = imaginary;
  }

  /**
   * Retourne la partie réelle.
   * @return la partie réelle
   */
  public T getReal() {
    return real;
  }

  /**
   * Retourne la partie imaginaire.
   * @return la partie imaginaire
   */
  public T getImaginary() {
    return imaginary;
  }
}
Utilisation de la classe générique Complex
package fr.uvsq.refcardjava.classes;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class GenericComplexTest {
  @Test
  public void shouldCreateAComplexFromFloat() {
    GenericComplex<Float> c = new GenericComplex<>(1.0F, 2.0F);
    assertTrue(c.getReal() instanceof Float);
    assertTrue(c.getImaginary() instanceof Float);
  }

  @Test
  public void shouldCreateAComplexFromDouble() {
    GenericComplex<Double> c = new GenericComplex<>(1.0, 2.0);
    assertTrue(c.getReal() instanceof Double);
    assertTrue(c.getImaginary() instanceof Double);
  }
}

3.6.2. Méthode générique en Java

  • Une méthode générique possède un type formel entre “<” et “>” placé en début de déclaration

    public static <T> T max(T o1, T o2) // ...
  • La portée de ce paramètre est alors restreinte à la méthode

  • L’invocation de la méthode peut préciser le type effectif ou se baser sur l'inférence de type

    // Type effectif explicite
    Integer i = uneClasse.<Integer>max(i1, i2);
    
    // Type effectif déterminé par inférence de type
    Integer i = uneClasse.max(i1, i2);

3.6.3. Généricité contrainte en Java

  • Il est possible d’imposer que le paramètre de type formel soit un sous-type d’un autre type avec le mot-clé extends

    public static <T extends Number> T max(T o1, T o2) // ...
  • Le mot-clé super permet d’imposer que le paramètre de type formel soit un super-type d’un autre type (exemple : <T super Number>)

  • Il peut être nécessaire d’utiliser le caractère joker ? si le type effectif n’est pas connu

    // Une boite qui peut contenir des nombres
    Boite<? extends Number> b = //...
    
    // Un boite qui peut contenir n'importe quoi
    Boite<?> b = //...

4. Héritage

4.1. Sous-typage

  • Un type T1 est un sous-type d’un type T2 si l’interface de T1 contient l’interface de T2.

    • T1 possède une interface plus riche que celle de T2, i.e. au moins toutes les opérations de T2

  • De façon duale, un type T1 est un sous-type d’un type T2 si l’ensemble des instances de T2 inclut l’ensemble des instances de T1.

    • l’extension du super-type T2 contient l’extension du sous-type T1, i.e. tout objet de type T1 est aussi instance de T2

  • En Java, les interfaces peuvent être organisées en hiérarchies

    • un lien entre interfaces est une relation de sous-typage

    • pour cela, on utilise le mot-clé extends dans la déclaration

    • Une interface peut avoir plusieurs super-interfaces

  • Une interface est formellement équivalente à une classe abstraite ne possédant que des méthodes abstraites

Exemple : Collection<E> et List<E>
collection et list
public interface Iterable<T> {
  // ...
}

public interface Collection<E> extends Iterable<E> {
  //...
}

public interface List<E> extends Collection<E> {
  //...
}

4.1.1. Classe et interface

  • Pour déclarer une classe qui implémente une ou plusieurs interfaces, on ajoute implements ListeInterfaces dans sa déclaration (après la clause extends si elle existe)

  • La classe doit alors implémenter toutes les méthodes de l’interface ou être déclarée abstraite

Exemple : définir l'ordre naturel pour une classe
class String implements Comparable<String> {
  // ...
  @Override
  public int compareTo(String o) {
    // Code pour la comparaison
  }
  // ...
}

4.2. Héritage entre classes

  • L'héritage permet de définir l’implémentation d’une classe à partir de l’implémentation d’une autre

    • lors de la définition d’une nouvelle classe, seul ce qui change par rapport à une classe existante est précisé

      rectangle et rectangle plein
      Figure 4. Exemple : rectangle et rectangle plein
  • En Java, on spécifie qu’une classe est une sous-classe d’une autre en utilisant extends dans la déclaration

    class Rectangle2DPlein extends Rectangle2D {
    
    }
  • Une classe ne peut avoir qu’une seule super-classe (pas d’héritage multiple)

  • Si extends n’est pas précisé, la classe hérite de la classe Object

    Une classe Java a une et une seule super-classe
  • Une classe déclarée final ne peut plus être spécialisée

4.3. Héritage et membres

  • Une classe C hérite de sa super-classe S les attributs et méthodes qu’elle possède

    • tous les attributs de S font partie de l’état des instances de C

    • les méthodes publiques de S font partie de l’interface publique de C

  • Les attributs et méthodes d’une super-classe ne sont pas forcément accessibles

    • les attributs privés de S ne sont pas accessibles dans C

    • les méthodes non privées de S sont accessibles dans C

    • les constructeurs de S sont utilisables dans les constructeurs de C mais ne font pas partie de l’interface publique de C

  • Une classe peut masquer un membre de sa super-classe si elle possède un membre de même nom (ou de même signature)

    • Le mot clé super permet d’accéder aux membres masqués d’une super-classe

Exemple : définition de la classe Rectangle2DPlein
package fr.uvsq.refcardjava.inheritance;


import java.awt.*;
import java.awt.geom.Point2D;

/**
 * Un rectangle plein en deux dimensions.
 *
 * @author Stéphane Lopes
 * @version nov. 2008
 */
class Rectangle2DPlein extends Rectangle2D { (1)
    /**
     * La couleur de remplissage
     */
    private final Color couleur; (2)

    /**
     * Initialise le rectangle plein.
     *
     * @param supGauche Le coin supérieur gauche.
     * @param infDroit  Le coin inférieur droit.
     * @param couleur   La couleur de remplissage.
     */
    public Rectangle2DPlein(Point2D.Double supGauche,
                            Point2D.Double infDroit,
                            Color couleur) {
        super(supGauche, infDroit); (3)
        assert couleur != null;
        this.couleur = couleur;
    }

    /**
     * Renvoie la couleur.
     *
     * @return la couleur.
     */
    public Color getCouleur() {
        return couleur;
    } (4)

    // tag::rect-plein-tostring[]
    /**
     * Retourn une chaîne représentant l'objet.
     *
     * @return la chaîne.
     */
    @Override
    public String toString() {
        return String.format("%s, couleur : %s", super.toString(), couleur);
    }
    // end::rect-plein-tostring[]
}
1 extends exprime l’héritage
2 seuls les attributs supplémentaires sont déclarés dans la sous-classe
3 dans le constructeur, super permet d’appeler le constructeur de la super-classe
4 seules les méthodes supplémentaires sont définies dans la sous-classe
Utilisation de la classe Rectangle2DPlein
Rectangle2DPlein rp = new Rectangle2DPlein(new Point2D.Double(1.0, 2.0),
  new Point2D.Double(3.0, 0.0),
  Color.RED);
assertEquals(Color.RED, rp.getCouleur());

// Déclaration d'un rectangle et liaison avec un rectangle plein
Rectangle2D r = new Rectangle2DPlein(new Point2D.Double(1.0, 2.0), (1)
  new Point2D.Double(2.0, 1.0),
  Color.YELLOW);
assertEquals(1, r.getLargeur());
//assertEquals(Color.RED, r.getCouleur()); (2)
1 L’affectation d’une instance de Rectangle2DPlein à une référence sur un Rectangle2D respecte le principe de substitution de Liskov
2 À partir de r1, l’accès à getCouleur est impossible (échoue à la compilation) car getCouleur ne fait pas partie de l’interface de Rectangle2D

4.4. Héritage et sous-typage

  • L’héritage (ou héritage d’implémentation) est un mécanisme technique de réutilisation

  • Le sous-typage (ou héritage d’interface) décrit comment un objet peut être utilisé à la place d’un autre

  • Si Y est une sous-type de X, cela signifie que "Y est une sorte de X" (relation IS-A)

  • Dans un langage de programmation, les deux visions peuvent être représentées de la même façon : le mécanisme d’héritage permet d’implémenter l’un ou l’autre

heritage et interface
Figure 5. Exemple : héritage d’implémentation et d’interface

4.5. Polymorphisme

  • Le polymorphisme est l’aptitude qu’ont des objets à réagir différemment à un même message

  • L’intérêt est de pouvoir gérer une collection d’objets de façon homogène tout en conservant le comportement propre à chaque type d’objet

  • Une méthode commune à une hiérarchie de classe peut avoir plusieurs implémentations dans différentes classes

  • Une sous-classe peut redéfinir une méthode de sa super-classe pour spécialiser son comportement

  • Le choix de la méthode à appeler est retardé jusqu’à l’exécution du programme (liaison dynamique ou retardée)

description des rectangles
Figure 6. Exemple : une description pour les rectangles (méthode toString)

4.5.1. Redéfinition de méthode

  • La redéfinition (overriding) consiste à définir dans une sous-classe, une méthode ayant même signature et même type de retour qu’une méthode de la super-classe

    • La déclaration de la méthode redéfinie est toujours précédée de l’annotation @Override

    • la méthode de la super-classe est alors masquée

    • il est toujours possible d’appeler la méthode redéfinie en utilisant le mot-clé super

  • Le contrôle d’accès peut être relaxé lors de la redéfinition

  • Une méthode déclarée final ne peut pas être redéfinie

  • Une méthode de classe ne peut pas être redéfinie

  • La redéfinition ne permet plus au compilateur de sélectionner la méthode adéquat

  • C’est le type de l’objet (et non pas de la référence) qui permettra de déterminer la méthode à invoquer et ce type ne peut être connu qu’au moment de l’exécution

rectangle et rectangle plein polym
Figure 7. Exemple : une description pour les rectangles (méthode toString)
Exemple : redéfinition de la méthode toString de la classe Rectangle2D
/**
 * Retourne une chaîne représentant l'objet.
 * @return la chaîne.
 */
@Override
public String toString() {
    return String.format("O = %s L = %s, H = %s", orig, getLargeur(), getHauteur());
}
Exemple : redéfinition de la méthode toString de la classe Rectangle2DPlein
/**
 * Retourn une chaîne représentant l'objet.
 *
 * @return la chaîne.
 */
@Override
public String toString() {
    return String.format("%s, couleur : %s", super.toString(), couleur);
}
Exemple : utilisation du polymorphisme
// Création d'un tableau de références sur des Rectangle2D
final int NB_RECTANGLES = 2;
Rectangle2D[] figures = new Rectangle2D[NB_RECTANGLES];

// Un rectangle
figures[0] = new Rectangle2D(new Point2D.Double(0.0, 5.0),
  new Point2D.Double(2.0, 2.0));

// Un rectangle plein
figures[1] = new Rectangle2DPlein(new Point2D.Double(1.0, 3.0),
  new Point2D.Double(3.0, 2.0),
  Color.BLUE);

assertEquals("O = Point2D.Double[0.0, 5.0] L = 2.0, H = 3.0", figures[0].toString());
assertEquals("O = Point2D.Double[1.0, 3.0] L = 2.0, H = 1.0, couleur : java.awt.Color[r=0,g=0,b=255]",
  figures[1].toString());

4.5.2. La classe Object

  • La classe Objet définit et implémente le comportement dont chaque classe Java a besoin

  • C’est la plus générale des classes Java

  • Chaque classe Java hérite directement ou indirectement de Object (tout objet y compris les tableaux implémente les méthodes de Object)

  • Certaines méthodes de Object peuvent être redéfinies pour s’adapter à la sous-classe

    protected Object clone()

    permet de dupliquer un objet

    boolean equals(Object obj)

    permet de tester l’égalité de deux objets et int hashCode() de renvoyer une valeur de hashage

    • Object.equals teste l’identité

    • equals et hashCode doivent être redéfinies ensembles

    protected void finalize()

    représente le destructeur d’un objet

    String toString()

    retourne une chaîne représentant l’objet

    toString est très utile pour le débogage ⇒ toujours la redéfinir
  • Autres méthodes :

    Class getClass()

    retourne un objet de type Class représentant la classe de l’objet

    la classe Class est par exemple utile pour créer des objets dont la classe n’est pas connu à la compilation
  • quelques méthodes pour les threads

Copie d’objets
  • L’opération de copie peut avoir différentes sémantiques

    • copie profonde (deep copy)

    • copie superficielle (shallow copy)

  • La copie peut être obtenue de plusieurs manières

    • par un constructeur de copie

    • par une méthode de classe (méthode de fabrication)

    • par clonage (implémentation de l’interface Cloneable et redéfinition de la méthode Object.clone)

L’usage de clone est déconseillée
Egalité d’objets : la méthode equals
  • La méthode boolean equals(Object o) est destinée à tester l’égalité de deux objets

  • La méthode equals de la classe Object se contente de tester l’égalité des références des objets, i.e. l’identité et se comporte comme l’opérateur ==

    l’opérateur == teste l’identité de ses opérandes, i.e. l’égalité des références
  • Il est donc en général nécessaire de redéfinir equals pour le test d’égalité

Contraintes de equals
  • equals implémente une relation d’équivalence pour des références d’objet non nulles

    • x.equals(x) == true (réflexivité)

    • x.equals(y) == true si et seulement si y.equals(x) == true (symétrie)

    • si x.equals(y) == true et y.equals(z) == true alors x.equals(z) == true (transitivité)

    • x.equals(null) == false

  • Toute classe qui redéfinit equals doit également redéfinir hashCode()

    • si deux objets sont égaux au sens de equals alors hashCode doit produire le même résultat pour les deux objets

Exemple : égalité de rectangles
/**
 * Teste l'égalité de deux rectangles.
 * @param obj le rectangle à comparer.
 * @return true si les objets sont égaux.
 */
@Override
public boolean equals(Object obj) {
    if (obj instanceof Rectangle2D) {
        Rectangle2D r = (Rectangle2D)obj;
        return orig.equals(r.orig) && fin.equals(r.fin);
    }
    return false;
}

/**
 * Retourne une valeur de hashage pour l'objet.
 * @return la valeur de hashage.
 */
@Override
public int hashCode() {
    return orig.hashCode() ^ fin.hashCode();
}
Exemple : contraintes de equals
Rectangle2D r1 = new Rectangle2D(new Point2D.Double(0.0, 5.0),
  new Point2D.Double(2.0, 2.0));
Rectangle2D r2 = new Rectangle2D(new Point2D.Double(0.0, 5.0),
  new Point2D.Double(2.0, 2.0));
Rectangle2D r3 = new Rectangle2D(new Point2D.Double(0.0, 5.0),
  new Point2D.Double(2.0, 2.0));
assertEquals(r1, r1);    // Réflexivité
assertEquals(r1, r2);
assertEquals(r2, r1); // Symétrie
// r1.equals(r2) && r2.equals(r3) =>
assertEquals(r1, r3); // Transitivité
assertNotNull(r1);
assertEquals(r1.hashCode(), r2.hashCode());

4.6. Classe abstraite

  • Une classe abstraite représente un concept abstrait qui ne peux pas être instancié

    • En général, son comportement ne peut pas être intégralement implémenté à cause de son niveau de généralisation

    • Elle sera donc seulement utilisée comme classe de base dans une hiérarchie d’héritage

  • En Java, une classe est spécifiée abstraite en ajoutant le mot-clé abstract dans sa déclaration

    L’instanciation d’une telle classe est alors refusée par le compilateur
  • Une classe abstraite contient généralement des méthodes abstraites, i.e. qui ne possèdent pas d’implémentation

    • Une méthode est déclarée abstraite en utilisant le mot-clé abstract lors de sa déclaration

  • Toute sous-classe non abstraite d’une classe abstraite doit redéfinir les méthodes abstraites de cette classe

  • Une classe possédant des méthodes abstraites est obligatoirement abstraite

hierarchie heritage
Figure 8. Exemple : La hiérarchie d’héritage des figures
Exemple : une classe abstraite (Java)
/**
 * Une figure fermée.
 *
 * @version  jan. 2017
 * @author   Stéphane Lopes
 *
 */
abstract class FigureFermee2D {
    /**
     * Translate la figure.
     * @param dx déplacement en abscisse.
     * @param dy déplacement en ordonnée.
     */
    public abstract void translate(double dx, double dy);
}
Translation dans la classe Rectangle2D
/**
 * Translate le rectangle.
 * @param dx déplacement en abscisse.
 * @param dy déplacement en ordonnées.
 */
@Override
public void translate(double dx, double dy) {
    orig.setLocation(orig.getX() + dx, orig.getY() + dy);
    fin.setLocation(fin.getX() + dx, fin.getY() + dy);
}
Translation dans la classe Cercle2D
/**
 * Translate le cercle.
 *
 * @param dx déplacement en abscisse.
 * @param dy déplacement en ordonnées.
 */
@Override
public void translate(double dx, double dy) {
    centre.setLocation(centre.getX() + dx, centre.getY() + dy);
}
Exemple : utilisation d’une classe abstraite (Java)
// Création du tableau de références
final int NB_FIGURES = 4;
FigureFermee2D[] figure = new FigureFermee2D[NB_FIGURES];

// Création des formes
figure[0] = new Rectangle2D(new Point2D.Double(0.0, 5.0), new Point2D.Double(2.0, 2.0));
figure[1] = new Cercle2D(new Point2D.Double(1.0, 2.0), 3.0);
figure[2] = new Rectangle2D(new Point2D.Double(5.0, 5.0), new Point2D.Double(7.0, 3.0));
figure[3] = new Cercle2D(new Point2D.Double(4.0, 5.0), 2.0);

// Réalise une translation de la figure
for (FigureFermee2D figureFermee2D : figure) {
  figureFermee2D.translate(1.0, 2.0);
}

4.6.1. La classe Number

  • La classe Number est une classe abstraite de la librairie Java

  • Elle définit le comportement commun aux classes pour la gestion des nombres (les conversions)

  • Elle possède plusieurs sous-classes

    • les adaptateurs : Byte, Double, Float, Integer, Long, Short

    • BigDecimal, BigInteger

  • Ces classes offrent une vue "objet" des types primitifs

La plupart des fonctions arithmétiques sont des méthodes de classe de la classe Math.
autoboxing/autounboxing
  • Ce mécanisme permet d’éviter la conversion manuelle entre type primitif et adaptateur

  • C’est simplement une facilité d’écriture (sucre syntaxique)

Integer i = 12; // à la place de Integer i = Integer.valueOf(12);
int n = i; // à la place de int n = i.intValue();
Les adaptateurs

5. Modules et bibliothèques

5.1. Module

  • Un module est l’unité de base de décomposition d’un système

  • Il permet d’organiser logiquement des modèles

  • Un module s’appuie sur la notion d'encapsulation

    • publie une interface, i.e. ce qui est accessible de l’extérieur

    • utilise le principe de masquage de l’information, i.e. ce qui ne fait pas parti de l’interface est dissimulé

  • Un module

    • sert de brique de base pour la construction d’une architecture,

    • représente le bon niveau de granularité pour la réutilisation,

    • est un espace de noms qui permet de gérer les conflits.

  • La conception d’un module devrait conduire à un couplage faible et une forte cohésion

    couplage

    désigne l’importance des liaisons entre les éléments ⇒ doit être réduit

    cohésion

    mesure le recouvrement entre un élément de conception et la tâche logique à accomplir ⇒ doit être élevé, i.e. chaque élément est responsable d’une tâche précise

package diagram
Figure 9. Exemple de modules dans une architecture multi-couches

5.2. Module en Java

  • Un module peut être représenté en Java en s’appuyant sur plusieurs aspects du langage :

    • les packages,

    • les modules depuis Java 9,

    • les classes.

Cette section n’aborde que la notion de packages.

5.3. Définition d’un package

  • Pour créer un package ou y ajouter une classe ou une interface, on place une instruction package au début du fichier source

    package monpackage;
  • Tout ce qui est défini dans le fichier source fait alors partie du package

  • Sans instruction de ce type, les éléments se trouvent dans le package par défaut (non nommé)

  • Les noms des package respectent en général une convention (par exemple, fr.uvsq.monpackage)

  • La librairie Java est organisée de cette façon (java.lang, java.util, java.io, …​)

5.4. Interface d’un package

  • Seul les éléments publics sont accessibles à l’extérieur du package

  • Pour rendre une classe ou une interface publique, on spécifie le mot-clé public dans sa déclaration

    public class MaClasse {
      //...
    }

5.5. Utilisation d’un package

  • Différentes façons d’utiliser les éléments public d’un module

    • utiliser son nom qualifié

      fr.uvsq.monpackage.MaClasse m = new fr.uvsq.monpackage.MaClasse();
    • importer l’élément

      import fr.uvsq.monpackage.MaClasse; // en début de fichier source
    • importer le module complet (déconseillé en général)

      import fr.uvsq.monpackage.*; // en début de fichier source
    • importer les classes imbriquées

      import fr.uvsq.monpackage.MaClasse.*; // en début de fichier source
    • importer les membres de classes

      import static fr.uvsq.monpackage.MaClasse.*; // en début de fichier source
  • Les directives import se placent avant toute définition de classes ou d’interfaces mais après l’instruction package

  • Deux packages sont automatiquement importés : le module par défaut et java.lang

5.6. Package et gestion des sources en Java

  • Dans un fichier source

    • plusieurs éléments (classes, interfaces, …​) peuvent être définies

    • un seul élément peut être public

    • le nom de l’élément public doit être le même que le nom du fichier

  • Parc convention, on se limite de préférence à une classe par fichier source

    • le nom du fichier .java est le même que le nom de l’élément qu’il contient

  • Le nom du répertoire doit refléter le nom du paquetage

    • la classe fr.uvsq.monpackage.MaClasse doit se trouver dans le fichier MaClasse.java du répertoire fr/uvsq/monpackage/

5.7. Package et compilation

  • Lors de la compilation, un fichier .class est créé pour chaque élément source (classe, classe imbriquée, interface, …​)

  • La hiérarchie de répertoires contenant les .class reflète les noms des modules

  • Les répertoires où sont recherchées les classes lors de la compilation ou de l’exécution sont listés dans le class path

  • Par défaut, le répertoire courant et la librairie Java se trouve dans le class path

  • La façon dont le class path est défini dépend de la plateforme

    • en général, on définit une variable d’environnement CLASSPATH

  • Le class path contient des chemins vers des répertoires contenant une arborescence de .class, des fichiers .jar, des fichiers .zip

5.8. Écosystème Java et bibliothèques

  • L’écosystème Java fournit un nombre important de bibliothèques et d’outils de développement

  • Dans un projet de développement logiciel, le choix des bibliothèques à utiliser est une étape importante

    • fonctionnalités, complexité, support de la communauté, licence, …​

  • La plupart des programmes Java font appel à des bibliothèques tierces (third party libraries)

  • Une bibliothèque est organisé en packages

5.9. Utilisation d’un bibliothèque tierce

  1. Récupérer la bibliothèque

    • manuellement (téléchargement du jar)

    • automatiquement (outils de gestion des dépendances comme maven ou gradle)

  2. Inclure la bibliothèque dans le projet

    • le CLASSPATH doit être modifié pour faire référence aux archives (jar en général) de la bibliothèque

  3. Consulter l’interface de la bibliothèque

    • toute bibliothèque Java est distribuée avec sa documentation au format javadoc

  4. Importer les modules nécessaires dans les fichiers sources

    • l’utilisation d’une classe de la bibliothèque nécessite d’importer le package Java adéquat

5.10. Exemple : utilisation de la bibliothèque Apache Commons Math

  1. Télécharger et décompresser le fichier commons-math3-3.6.1-bin.tar.gz

    $ wget -c https://downloads.apache.org//commons/math/binaries/commons-math3-3.6.1-bin.tar.gz -O - | tar -xz
    $ ls commons-math3-3.6.1
    ~/commons-math3-3.6.1 $ ls
    commons-math3-3.6.1.jar               commons-math3-3.6.1-tools.jar
    commons-math3-3.6.1-javadoc.jar       docs/
    commons-math3-3.6.1-sources.jar       LICENSE.txt
    commons-math3-3.6.1-tests.jar         NOTICE.txt
    commons-math3-3.6.1-test-sources.jar  RELEASE-NOTES.txt
  2. Ajouter la bibliothèque au projet (IDE ou outil de build)

    <!-- Avec maven -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-math3</artifactId>
        <version>3.6.1</version>
    </dependency>
  3. Importer les classes des packages nécessaires

    import org.apache.commons.math3.fraction.Fraction;
    
    public class Main {
        public static void main(String[] args) {
            Fraction f = new Fraction(1, 3);
            System.out.println(f);
        }
    }
  4. Compiler en précisant la bibliothèque dans le CLASSPATH (en ligne de commande)

    $ javac -cp commons-math3-3.6.1/commons-math3-3.6.1.jar Main.java
  5. Éxécuter en précisant la bibliothèque dans le CLASSPATH (en ligne de commande)

    $ java -cp commons-math3-3.6.1/commons-math3-3.6.1.jar Main

6. Gestion d’erreurs et exceptions

6.1. Qu’est-ce qu’une erreur ?

Un événement dans une fonction f est une erreur dans l’un des cas suivants :

  • il viole une des préconditions de f,

    • peut être considéré comme une erreur de programmation ⇒ utilisation des assertions

  • il empêche f de remplir une des préconditions de ses appelés,

  • il empêche de réaliser une postcondition de f,

  • il empêche f de rétablir un invariant dont elle a la responsabilité.

Les autres événements ne doivent pas être considérés comme des erreurs

6.2. Erreur vs. bug

  • La gestion d’erreurs est chargée des erreurs d’exécution

  • Les erreurs de logique (bugs) doivent être éliminées durant le développement en utilisant :

    • les assertions

    • le débogage

    • les tests

6.3. Réactions possibles à une erreur

6.3.1. Ignorer le problème

+ WARNING: en général, c’est une mauvaise idée…​

6.3.2. Retourner un code d’erreur

public static double sqrtWithReturnCode(double d) {
    return Double.isNaN(d) || d < 0.0 ?
            Double.NaN : (1)
            sqrt(d);
}
1 Quand le paramètre est invalide (Not A Number ou négatif), la fonction retourne le code d’erreur Not A Number
Dans cette exemple, comme l’erreur est une violation de la pré-condition, on pourrait considérer que c’est une erreur de programmation et la traiter avec des assertions.
double result = sqrtWithReturnCode(value);
if (Double.isNaN(result)) { (1)
    System.err.println("Argument illégal (négatif ou égal à NaN).");
} else {
    System.out.printf("sqrt(%f) = %f\n", value, result);
}
1 Lors de l’appel, il faut tester la valeur de retour de la fonction.
possible si une valeur de retour est disponible pour cela

6.3.3. Utiliser une variable globale

public enum SqrtError { None, NegArg, NaNArg }

private static SqrtError sqrtError = SqrtError.None;

public static SqrtError getSqrtError() { return sqrtError; }

public static double sqrtWithGlobalCode(double d) {
    if (Double.isNaN(d)) { (1)
        sqrtError = SqrtError.NaNArg;
    } else if (d < 0) { (1)
        sqrtError = SqrtError.NegArg;
    }
    return sqrt(d);
}
1 Lorsque l’erreur est détectée, on fixe la valeur de la variable globale.
double result = sqrtWithGlobalCode(value);
if (getSqrtError() != SqrtError.None) { (1)
    System.err.println("Argument illégal (négatif ou égal à NaN).");
} else {
    System.out.printf("sqrt(%f) = %f\n", value, result);
}
1 Après l’appel, il faut vérifier l’état de la variable globale.

6.3.4. Lancer une exception

public static double sqrtWithException(double d) {
    if (Double.isNaN(d) || d < 0.0) { (1)
        throw new IllegalArgumentException("Argument négatif ou NaN");
    }
    return sqrt(d);
}
1 Lorsque l’erreur est détectée, une exception est lancée.
double result;
try { (1)
    result = sqrtWithException(value);
    System.out.printf("sqrt(%f) = %f\n", value, result);
} catch (IllegalArgumentException ex) { (1)
    ex.printStackTrace(System.err);
}
1 L’exception se propage selon la pile d’appel du programme et doit être traitée.

6.3.5. Utiliser le type `Option`

public static Optional<Double> sqrtWithOption(double d) {
    return Double.isNaN(d) || d < 0.0 ?
            Optional.empty() :  (1)
            Optional.of(sqrt(d));
}
1 Le résultat est encapsulé dans une instance du type Optional.
Optional<Double> result = sqrtWithOption(value); (1)
System.out.printf("sqrt(%f) = %f\n", value, result.orElse(Double.NaN));
1 Les accesseurs de Optional permettent d’extraire le résultat.
Cette technique est issue de la programmation fonctionnelle (cf. Option Type).

6.4. Exceptions en Java

  • Trois catégories d’exceptions

    • une exception non contrôlée (unchecked exceptions) n’est pas destinée à être traitée par le programme

      • une erreur (error) a une cause externe à l’application

      • une exception d’exécution (runtime exception) est provoquée par la JVM

    • une exception contrôlée (checked exception) est une exception qui n’est pas lancée par le système d’exécution Java (runtime exception)

  • Une exception est une instance d’une classe dérivée de Throwable

  • Une méthode doit soit traiter, soit spécifier toute exception contrôlée qui peut se produire dans cette méthode

    • traiter = fournir un gestionnaire d’exception pour ce type d’exception

    • spécifier = préciser dans la signature de la méthode que l’exception peut être lancée

  • Le compilateur ne requiert pas que les exceptions du système d’exécution soient traitée ou spécifiée

6.5. Traitement d’une exception

  • Un bloc try englobe la séquence d’instructions susceptible de lancer une exception

  • Un ou plusieurs blocs catch représentent les gestionnaires d’exceptions

  • Au plus un bloc finally est toujours exécuté

6.5.1. Le bloc try

  • Le bloc try englobe les instructions susceptibles de lancer une exception

    try {
        // Instructions
    }
  • L’instruction try gouverne les instructions englobées

  • Il définit la portée des gestionnaires d’exceptions qui lui sont associés

  • Une instruction try doit être accompagnée d’au moins un bloc catch ou finally

6.5.2. Les blocs catch

  • Les blocs catch représentent les gestionnaires d’exceptions

  • Un ou plusieurs blocs catch sont placés immédiatement après un bloc try

    try {
        // Instructions
    } catch ( /* ... */ ) {
        // Instructions
    } catch ( /* ... */ ) {
        // Instructions
    } // ...
  • Les blocs catch doivent être ordonnés du plus spécialisé au plus général

  • L’instruction catch requiert un unique paramètre

    catch (<Type> <variable>) {
        // Instructions
    }
    • <Type> représente le type de l’exception et doit être une classe dérivant de Throwable

    • <variable> est le nom de la variable locale au gestionnaire liée à l’exception

    • L’argument du catch ressemble à la déclaration d’un paramètre de méthode

  • Un gestionnaire peut capturer plusieurs types d’exceptions

    • en capturant une superclasse pour une exception

    • en utilisant plusieurs types dans la clause catch

      catch (Type1|Type2|Type3 ex) { //...

6.5.3. Le bloc finally

  • Le bloc finally founit un mécanisme pour "nettoyer" l’état du programme

  • Les instructions du bloc finally sont toujours exécutées

  • Le bloc finally se place après les gestionnaires d’exceptions du bloc try

    finally {
        // Instructions
    }

6.5.4. Exemple : gestionnaires d’exception pour une pile

try { (1)
    Pile unePile = new Pile(2);
    unePile.empile("azerty");
    unePile.empile("qsdfgh");
    unePile.empile("wxcvbn");
    assert false : "Jamais atteint";
    String str = (String) unePile.depile();
} catch (PileVideException e) { (2)
    assert e.getMessage().equals("La Pile est vide");
} catch (PileException e) { (3)
    assert e.getMessage().equals("La Pile est pleine");
}
1 le bloc try englobe les instructions pouvant lancer une exception
2 le premier gestionnaire capture le cas de la pile vice
3 le second gestionnaire traite tous les autres cas car le type PileException est la racine des exceptions de la pile

6.6. Exception et allocation de ressources

  • Du fait du mécanisme de propagation des exceptions, la libération des ressources utilisées dans un programme peut devenir délicate

  • La construction try-with-resources gère automatiquement la fermeture des ressources

  • Les ressources devant être gérées de cette façon sont allouées comme "paramètre" de try

  • Les classes représentant les ressources doivent implémenter l’interface AutoCloseable

try (
    java.util.zip.ZipFile zf = (1)
       new java.util.zip.ZipFile(zipFileName);
    java.io.BufferedWriter writer = (2)
       java.nio.file.Files.newBufferedWriter(outputFilePath, charset)
) {
    // ...
}
1 l’allocation de la variable zf est en "paramètre" de try : la fermeture du fichier est garantie même si une exception est lancée
2 il en est de même pour writer

6.7. Spécification d’exceptions

  • Une spécification d’exception précise qu’une méthode ne capture pas l’exception considérée mais peut la lancer

  • Pour spécifier qu’une ou plusieurs exceptions peuvent être lancées par une méthode, on utilise la clause throws dans la signature de la méthode

TypeRetour nomMethode throws Type1Exception, Type2Exception {
    //...
}

6.7.1. Exemple : spécification d’exceptions pour la pile

empile peut lancer une exception de type PilePleineException
/**
 * Empile un élément au sommet de la pile.
 *
 * @param unObjet l'objet à empiler
 * @throws PilePleineException s'il n'y a plus de place
 */
public void empile(Object unObjet) throws PilePleineException {
depile peut lancer une exception de type PileVideException
/**
 * Dépile l'élément se trouvant au sommet de la pile.
 *
 * @return l'élément au sommet
 * @throws PileVideException s'il n'y a pas d'élément
 */
public Object depile() throws PileVideException {

6.8. Lancement d’exceptions

  • L’instruction throw est utilisée pour lancer une exception

  • Le mot-clé throw doit être suivi d’une instance d’une classe dérivée de Throwable

    throw new ClasseDerivéeDeThrowable();
  • Une exception peut être relancée à partir d’un bloc catch

6.8.1. Lancements d’exceptions pour la pile

/**
 * Empile un élément au sommet de la pile.
 *
 * @param unObjet l'objet à empiler
 * @throws PilePleineException s'il n'y a plus de place
 */
public void empile(Object unObjet) throws PilePleineException {
    if (sommet == contenu.length) {
        throw new PilePleineException();
    }
    contenu[sommet++] = unObjet;
}

/**
 * Dépile l'élément se trouvant au sommet de la pile.
 *
 * @return l'élément au sommet
 * @throws PileVideException s'il n'y a pas d'élément
 */
public Object depile() throws PileVideException {
    if (sommet == 0) {
        throw new PileVideException();
    }
    return contenu[--sommet];
}

6.9. La classe Throwable

  • La classe Throwable est la super-classe de toutes les exceptions ou erreurs du langage Java

  • Seules les instances de cette classe (ou d’une de ses sous-classes) peuvent être lancées

  • Seule cette classe (ou l’une de ses sous-classes) peut être l’argument d’un catch

  • Contient un instantané de la pile d’exécution au moment de la création de l’instance

  • Peut contenir une cause (une autre instance de Throwable) afin de gérer une chaîne d’exceptions

throwable
Figure 10. Les méthodes de la classe Throwable

6.11. Créer des classes exceptions

  1. Déterminer dans quelles méthodes et sous quelles conditions des exceptions seront lancées

  2. Choisir le type de chaque exception

    • utiliser une exception existante

    • en créer une nouvelle

  3. Choisir quelle sera la super-classe des exceptions définies

6.11.1. Exemple : une hiérarchie d’exceptions pour une pile

pile exception hierarchie
La classe PileException
package fr.uvsq.refcardjava.errors;

/**
 * La racine de la hiérarchie d'exception pour la pile.
 *
 * @author Stéphane Lopes
 * @version fév. 2017
 */
class PileException extends Exception {
    /**
     * Initialise une instance de <code>PileException</code>.
     *
     * @param message le message d'erreur.
     */
    public PileException(String message) {
        super(message);
    }
}
La classe PileVideException
package fr.uvsq.refcardjava.errors;

/**
 * Exception pour la pile vide.
 *
 * @author Stéphane Lopes
 * @version fév. 2017
 */
class PileVideException extends PileException {
    /**
     * Initialise une instance de <code>PileVideException</code>.
     */
    public PileVideException() {
        super("La Pile est vide");
    }
}
La classe PilePleineException
package fr.uvsq.refcardjava.errors;

/**
 * Exception pour la pile pleine.
 *
 * @author Stéphane Lopes
 * @version fév. 2017
 */
class PilePleineException extends PileException {
    /**
     * Initialise une instance de <code>PilePleineException</code>.
     */
    public PilePleineException() {
        super("La Pile est pleine");
    }
}

7. Entrées/sorties et persistance

7.1. Structure et fonctionalités

La bibliothèque standard Java fournit de nombreuses fonctionnalités liées aux E/S

7.2. Gestion des flux

  • Un flux (stream) est un canal reliant une source (ou une destination) à un programme

  • Une source de données (ou une destination) peut être un fichier, la mémoire, le réseau, …​

  • Un flux peut être ouvert en lecture et/ou en écriture

    Les données sont lues ou écrites séquentiellement
  • La bibliothèque se divise en deux hiérarchies de classes

    • les flux de caractères (I/O de texte)

    • les flux d’octets (I/O binaire)

  • Un flux est automatiquement ouvert lors de sa création

  • La fermeture d’un flux se fait explicitement avec la méthode close

  • La plupart des méthodes peuvent lancer une exception dérivée de IOException

7.2.1. Flux de caractères

  • Les classes Reader et Writer sont les super-classes abstraites pour les flux de caractères

  • La plate-forme Java manipule des caractères en se basant sur Unicode

  • Les flux de caractères permettent de convertir ce format interne de/vers le format local

reader writer
Figure 11. Reader et Writer
reader hierarchie
Figure 12. Hiérarchie de Reader
writer hierarchie
Figure 13. Hiérarchie de Writer

7.2.2. Flux d’octets

  • Les classes InputStream et OutputStream sont les super-classes abstraites pour les flux d’octets

  • Les flux d’octets supportent la lecture et l’écriture d’octets (8 bits)

inputstream outputstream
Figure 14. InputStream et OutputStream
inputstream hierarchie
Figure 15. Hiérarchie de InputStream
outputstream hierarchie
Figure 16. Hiérarchie de OutputStream

7.2.3. Principaux flux par type d’I/O

Type d’I/O Flux de catactères Flux d’octets

Mémoire

CharArrayReader, CharArrayWriter

ByteArrayInputStream, ByteArrayOutputStream

StringReader, StringWriter

Fichier

FileReader, FileWriter

FileInputStream, FileOutputStream

Affichage

PrintWriter

PrintStream

7.2.4. Principaux flux par fonction

Type d’I/O Flux de catactères Flux d’octets

Avec buffer

BufferedReader, BufferedWriter

BufferedInputStream, BufferedOutputStream

Conv. de données

DataInputStream, DataOutputStream

Sérialisation

ObjectInputStream, ObjectOutputStream

Conv. oct./car.

InputStreamReader, OutputStreamWriter

7.2.5. Flux de fichiers

  • Les classes des flux de fichiers sont

    • FileReader/FileWriter pour l’accès aux fichiers textes

    • FileInputStream/FileOutputStream pour les fichiers binaires

  • Un flux de fichier peut être créé

    • à partir d’un nom de fichier sous la forme d’une chaîne de caractères

    • d’une instance de File

    • d’une méthode de classe de java.nio.file.Files (newInputStream, …​)

Copie d’un fichier texte
/**
 * Copie un fichier caractère par caractère.
 */
private static void textFileCopy(String inFilename, String outFilename) throws IOException {
    try (
            FileReader in = new FileReader(inFilename);
            FileWriter out = new FileWriter(outFilename)
    ) {
        int c;
        while ((c = in.read()) != END_OF_STREAM) {
            out.write(c);
        }
    }
}
Copie d’un fichier texte (avec buffer)
/**
 * Copie un fichier en utilisant un buffer.
 */
private static void bufferedTextFileCopy(String inFilename, String outFilename) throws IOException {
    Path inPath = Paths.get(inFilename);
    Path outPath = Paths.get(outFilename);
    try (
            BufferedReader in = Files.newBufferedReader(inPath);
            BufferedWriter out = Files.newBufferedWriter(outPath)
    ) {
        String line;
        while ((line = in.readLine()) != null) {
            out.write(line);
            out.newLine();
        }
    }
}
Copie d’un fichier texte (avec readAllLines)
/**
 * Copie un fichier texte.
 * Le fichier doit être de taille raisonnable car il est chargé en totalité en mémoire.
 */
private static void simpleTextFileCopy(String inFilename, String outFilename) throws IOException {
    Path inPath = Paths.get(inFilename);
    Path outPath = Paths.get(outFilename);
    List<String> lines = Files.readAllLines(inPath);
    Files.write(outPath, lines);
}
Copie d’un fichier binaire
/**
 * Copie un fichier octet par octet.
 */
private static void binaryFileCopy(String inFilename, String outFilename) throws IOException {
    try (
            FileInputStream in = new FileInputStream(inFilename);
            FileOutputStream out = new FileOutputStream(outFilename)
    ) {
        int c;
        while ((c = in.read()) != END_OF_STREAM) {
            out.write(c);
        }
    }
}

7.2.6. Flux de filtrage

  • Certaines classes de flux sont destinées à appliquer un traitement sur (à filtrer) un flux

  • Les super-classes abstraites pour cela sont

    • FilterReader/FilterWriter pour les caractères

    • FilterOutputStream/FilterInputStream pour les octets

  • Des flux personnalisés peuvent être définis en héritant de ces classes

Principaux flux de filtrage
  • Les principaux flux de filtrage pour les flux d’octets sont

    • DataInputStream et DataOutputStream pour les I/O des types primitifs

    • BufferedInputStream et BufferedOutputStream pour des I/O avec buffer

    • PrintStream pour l’affichage des données

    • PushbackInputStream pour pouvoir "annuler" la lecture d’une séquence d’octets

  • Le seul flux de filtrage pour les flux de caractères est PushbackReader (permet d'"annuler" la lecture d’une séquence de caractères)

Flux de filtrage et modèle de conception Décorateur
  • Un flux de filtrage est construit à partir d’un autre flux selon le modèle de conception Décorateur

  • Le flux résultant propose des fonctionnalités plus riches que le flux initial

FileInputStream words = new FileInputStream("words.dat");
BufferedInputStream in = new BufferedInputStream(words);
Écriture des types primitifs
public static void writeDateToFile(String filename, int numberOfItems, double[] prices, int[] units, String[] descs) throws IOException {
    try (DataOutputStream out =
                 new DataOutputStream(
                         new BufferedOutputStream(
                                 new FileOutputStream(filename)))
    ) {
        for (int i = 0; i < numberOfItems; i++) {
            out.writeDouble(prices[i]);
            out.writeInt(units[i]);
            out.writeUTF(descs[i]);
        }
    }
}
Lecture des types primitifs
public static void readDateFromFile(String filename, int numberOfItems, double[] prices, int[] units, String[] descs) throws IOException {
    try (DataInputStream out =
                 new DataInputStream(
                         new BufferedInputStream(
                                 new FileInputStream(filename)))
    ) {
        for (int i = 0; i < numberOfItems; i++) {
            prices[i] = out.readDouble();
            units[i] = out.readInt();
            descs[i] = out.readUTF();
        }
    }
}

7.2.7. Entrée et sortie standards en Java

La classe System fournit des flux pour :

  • l’entrée standard (attribut in de type PrintStream)

  • la sortie standard (attribut out de type InputStream)

  • la sortie d’erreurs (attribut err de type PrintStream)

Flux d’affichage PrintStream (ou PrintWriter)
  • PrintStream format( /* …​ */) affiche une chaîne selon un format

  • void print(/* …​ */) affiche différents types de données sur le flux

    • void println(/* …​ */) idem mais suivi d’un retour à la ligne

  • PrintStream append(/* …​ */) ajoute des caractères au flux

  • void flush() force la sortie des caractères

  • Ne lancent jamais d’exception mais positionnent un indicateur interne

    • interrogeable avec la méthode boolean checkError()

Lire à partir de l’entrée standard
  • L’entrée standard est utilisée en décorant System.in avec InputStreamReader (voire avec BufferedReader)

    InputStreamReader stdin = new InputStreamReader(System.in);
    BufferedReader bufferedStdin = new BufferedReader(stdin));
  • La classe java.util.Scanner simplifie le processus de saisie

    • permet de découper un flux en token

Accéder à l’entrée et la sortie standard (avec Scanner)
System.out.println("Votre nom et votre âge ? ");

Scanner s = new Scanner(System.in);
String nom = s.next();
int age = s.nextInt();

System.out.format("Votre nom est %s et vous avez %5d ans.%n", nom, age);
Accéder à l’entrée et la sortie standard (avec BufferedReader)
System.out.println("Votre nom et votre age ? ");

BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String nom = in.readLine();
int age = Integer.parseInt(in.readLine());

System.out.format("Votre nom est %s et vous avez %5d ans.%n", nom, age);
Utiliser la classe Console
  • La classe Console est une alternative pour l’accès à l’entrée et à la sortie standard

  • Un objet de ce type est initialisé avec la méthode System.console()

Console c = System.console();
if (c == null) {
    System.err.println("No console.");
    System.exit(1);
}

7.3. Persistance et sérialisation

  • La persistance est la capacité de sauvegarder l’état des objets, i.e. les données finales de l’application

  • Elle peut être réalisée avec

    • la bibliothèque d’I/O du langage,

    • à l’aide de bibliothèques spécialisées,

    • grâce à un SGBD externe.

  • La persistance pose un certain nombre de problèmes

    • sauvegarde de l’état de l’objet

    • gestion des types de données

    • gestion des références

  • La sérialisation est un processus permettant de transformer un objet en flux d’octets

7.4. Sérialisation

  • La sérialisation est assurée par les classes ObjectInputStream et ObjectOutputStream

  • ObjectOutputStream implémente les interfaces DataOutput et ObjectOutput

  • ObjectInputStream implémente les interfaces DataInput et ObjectInput

Écrire des objets dans un flux
private static void writeObjectsToFile(Student[] students, String filename) throws IOException {
    try (ObjectOutputStream oos =
                 new ObjectOutputStream(
                         new FileOutputStream(filename))
    ) {
        oos.writeObject(LocalDate.now());
        oos.writeObject(students);
    }
}
Lire des objets à partir d’un flux
private static Student[] readObjectsFromFile(String filename) throws IOException, ClassNotFoundException {
    try (ObjectInputStream ois =
                 new ObjectInputStream(
                         new FileInputStream(filename))
    ) {
        LocalDate date = (LocalDate) ois.readObject();
        return (Student[]) ois.readObject();
    }
}

7.4.1. Rendre une classe sérialisable

  • Un objet est sérialisable uniquement si sa classe implémente l’interface Serializable

  • L’interface Serializable ne comporte aucune méthode et ne sert qu’à spécifier les classes sérialisables

Une classe sérialisable
package fr.uvsq.refcardjava.io;

import java.io.Serializable;

public class Student implements Serializable {
    private int number;
    private String name;

    public Student(int number, String name) {
        this.number = number;
        this.name = name;
    }

    @Override
    public String toString() {
        return String.format("Student{number: %d, name: '%s'}", number, name);
    }
}

7.4.2. Gérer la version des classes

  • L’attribut de classe serialVersionUID précise la version d’une classe sérialisable

  • Il permet de déterminer si un objet correspond bien à la classe présente dans la JVM

  • Il est généré par la JVM s’il n’est pas défini dans la classe

  • Le développeur peut gérer les versions lui-même

private static final long serialVersionUID = 354054054054L;

7.4.3. Contrôler la sérialisation

  • La sérialisation est gérée par les méthodes

    • defaultWriteObject de ObjectOutputStream

    • defaultReadObject de ObjectInputStream

  • Le comportement par défaut de la sérialisation d’un objet est de stocker

    • la classe de l’objet

    • la signature de la classe

    • la valeur des attributs d’instances y compris les références (mais pas les attributs transcient)

  • Il est possible d’adapter le comportement par défaut en redéfinissant writeObject et readObject

  • L’interface Externalizable permet d’avoir un contrôle complet du processus de sérialisation

8. Bibliothèque pour la gestion des collections

8.1. Collection

  • Une collection (conteneur) est un objet qui regroupe plusieurs éléments en une seule unité

  • Une collection peut être utilisée pour stocker et manipuler des données et pour transmettre des données d’une méthode à une autre

  • Une collection regroupe généralement des objets de même type

8.2. Bibliothèques pour les collections

  • Une bibliothèque pour les collections (collection framework) est une architecture unifiée pour représenter et manipuler des collections

  • Elle différencie trois composants :

    • des interfaces définissent les types pour les collections

    • des implémentations représentent les structures de données proprement dites

    • des algorithmes effectuent des traitements sur les types de collections

  • Ce type de bibliothèque s’appuie en général sur la généricité pour le contrôle des types

Quelques exemples de bibliothèques pour les collections
Java

Java Collections Framework, Apache Commons Collections, Google Guava

C++

Standard Containers de la STL (Standard Template Library), Boost

Python

built-in containers dict, list, set, and tuple

C#

Collections

Rust

std::collections

8.3. Vue d’ensemble de la bibliothèque Java

collection overview
Figure 17. Collection simple
map overview
Figure 18. Dictionnaires

8.4. Caractéristiques communes

  • Une collection Java ne peut pas contenir une donnée d’un type primitif (uniquement des objets)

  • Les composants de la bibliothèque de collections se trouvent dans java.util

  • Le découpage interface/implémentation repose sur le modèle de conception Pont

8.5. Les interfaces

  • Les interfaces sont utilisées pour manipuler des collections et les transmettre d’une méthode à une autre

  • Les interfaces représentent les types de structures de données et permettent de manipuler les collections indépendamment des différentes implémentations

    il est préférable de manipuler les collections par les interfaces plutôt que par les implémentations
  • Une implémentation a la possibilité de ne pas supporter toutes les méthodes de modification de l’interface (lancement de l’exception UnsupportedOperationException)

  • Les implémentations du JDK implémentent toutes les méthodes optionnelles

collection hierarchie
Figure 19. Hiérarchie des types de collections simples
map hierarchie
Figure 20. Hiérarchie des types de dictionnaires

8.5.1. L’interface Collection

  • L’interface Collection est la racine de la hiérarchie de collection

  • Le JDK ne fournit pas d’implémentation spécifique pour cette interface

    toutes les implémentations conviennent

  • C’est le plus petit dénominateur commun pour les implémentations

  • Elle doit être utilisée quand un maximum de généralité est souhaitée

public interface Collection<E> extends Iterable<E> {
  // Opérations simples
  int size(); // Returns the number of elements in this collection.
  boolean isEmpty(); // Returns true if this collection contains no elements.
  boolean contains(Object element); // Returns true if this collection contains the specified element.
  boolean add(E element); // Ensures that this collection contains the specified element (optional operation).
  boolean remove(Object element); // Removes a single instance of the specified element from this collection, if it is present (optional operation).
  boolean equals(Object o); // Compares the specified object with this collection for equality.
  int hashCode(); // Returns the hash code value for this collection.

  // Opérations de groupe
  boolean containsAll(Collection<?> c); // Returns true if this collection contains all of the elements in the specified collection.
  boolean addAll(Collection<? extends E> c); // Adds all of the elements in the specified collection to this collection (optional operation).
  boolean removeAll(Collection<?> c); // Removes all of this collection's elements that are also contained in the specified collection (optional operation).
  default boolean removeIf(Predicate<? super E> filter); // Removes all of the elements of this collection that satisfy the given predicate.
  boolean retainAll(Collection<?> c); // Retains only the elements in this collection that are contained in the specified collection (optional operation).
  void clear();    // Removes all of the elements from this collection (optional operation).

  // Conversions
  Object[] toArray(); // Returns an array containing all of the elements in this collection.
  <T> T[] toArray(T[] a); // Returns an array containing all of the elements in this collection; the runtime type of the returned array is that of the specified array.
  default <T> T[] toArray(IntFunction<T[]> generator); // Returns an array containing all of the elements in this collection, using the provided generator function to allocate the returned array.

  // Itération et Streams
  Iterator<E> iterator(); // Returns an iterator over the elements in this collection.
  default Spliterator<E> spliterator(); // Creates a Spliterator over the elements in this collection.
  default Stream<E> stream(); // Returns a sequential Stream with this collection as its source.
  default Stream<E> parallelStream(); // Returns a possibly parallel Stream with this collection as its source.
}

8.5.2. Parcourir des collections

Trois techniques permettent de parcourir des collections

  • Les Streams

    String joined = elements.stream()
        .filter(e -> e.getColor() == Color.RED)
        .map(Object::toString)
        .collect(Collectors.joining(", "));
  • La boucle for-each

    for (String element : uneCollectionDeChaines) {
        // Manipuler element
    }
    • La boucle for-each ne permet pas de modifier la collection lors de l’itération

      utiliser un itérateur dans ce cas

  • Les itérateurs

    • La notion d’itérateur est implantée en Java par l’interface Iterator

      public interface Iterator<E> {
          boolean hasNext();
          E next();
          default void remove() {
              throw new UnsupportedOperationException("remove");
          }
          default void forEachRemaining(Consumer<? super E> action) {
              Objects.requireNonNull(action);
              while (hasNext())
                  action.accept(next());
          }
      }
    • Un itérateur peut être vu comme un marqueur se trouvant entre deux éléments

    • L’utilisation de remove est le seul moyen sûr de modifier une collection lors du parcours

    • On ne peut utiliser remove() qu’une seule fois par appel à next()

    • Un itérateur est basé sur le modèle de conception Itérateur

      for (Iterator<String> i = uneCollectionDeChaines.iterator(); i.hasNext(); ) {
        String element = i.next(); // Récupère l'élément et passe au suivant
        // ...
      }

8.5.3. L’interface Set

  • L’interface Set représente une collection sans doublon

    • modélise le concept mathématique d’ensemble

  • L’interface Set n’ajoute aucune méthode à l’interface Collection

  • Deux ensembles sont égaux s’ils contiennent les mêmes éléments

  • Sémantique des méthodes de groupe

    • s1.containsAll(s2) retourne true si \$s_2 sube s_1\$

    • s1.addAll(s2) transforme s1 en \$s_1 uu s_2\$

    • s1.retainAll(s2) transforme s1 en \$s_1 nn s_2\$

    • s1.removeAll(s2) transforme s1 en \$s_1 \\ s_2\$

8.5.4. L’interface List

  • L’interface List représente une séquence d’éléments, i.e. une collection ordonnée

  • L’interface List possède des opérations pour:

    • accéder aux éléments d’une liste par leurs indices

    • retourner l’indice d’un objet que l’on recherche

    • étendre la sémantique des itérateurs

    • manipuler des sous-listes

  • Deux listes sont égales si elles possèdent les mêmes éléments dans le même ordre

public interface List<E> extends Collection<E> {
  // Accès par position
  E get(int index); // Returns the element at the specified position in this list.
  E set(int index, E element); // Replaces the element at the specified position in this list with the specified element (optional operation).
  void add(int index, E element); // Inserts the specified element at the specified position in this list (optional operation).
  E remove(int index); // Removes the element at the specified position in this list (optional operation).
  boolean addAll(int index, Collection<? extends E> c); // Inserts all of the elements in the specified collection into this list at the specified position (optional operation).

  // Opération de groupe
  default void replaceAll(UnaryOperator<E> operator) {
        Objects.requireNonNull(operator);
        final ListIterator<E> li = this.listIterator();
        while (li.hasNext()) {
            li.set(operator.apply(li.next()));
        }
    }
  default void sort(Comparator<? super E> c) {
        Collections.sort(this, c);
    }

  // Recherche
  int indexOf(Object o); // Returns the index of the first occurrence of the specified element in this list, or -1 if this list does not contain the element.
  int lastIndexOf(Object o); // Returns the index of the last occurrence of the specified element in this list, or -1 if this list does not contain the element.

  // Itération
  ListIterator<E> listIterator(); // Returns a list iterator over the elements in this list (in proper sequence).
  ListIterator<E> listIterator(int index); // Returns a list iterator over the elements in this list (in proper sequence), starting at the specified position in the list.

  // Vue en sous-liste
  List<E> subList(int from, int to); // Returns a view of the portion of this list between the specified fromIndex, inclusive, and toIndex, exclusive.

  // Factories
  static <E> List<E> copyOf(Collection<? extends E> coll); // Returns an unmodifiable List containing the elements of the given Collection, in its iteration order.
  static <E> List<E> of(); // Returns an unmodifiable list containing zero elements.
  static <E> List<E> of(E e1); // Returns an unmodifiable list containing one element.
  static <E> List<E> of(E... elements); // Returns an unmodifiable list containing an arbitrary number of elements.
  static <E> List<E> of(E e1, E e2); // Returns an unmodifiable list containing two elements.
  // [...]
}

8.5.5. Itérateur de liste

  • L’interface ListIterator étend l’interface Iterator pour permettre un parcours dans les deux sens

public interface ListIterator<E> extends Iterator<E> {
    boolean hasPrevious();
    E previous();

    int nextIndex();
    int previousIndex();

    void set(E o);     // Optionnel
    void add(E o);     // Optionnel
}

8.5.6. L’interface Queue

  • L’interface Queue représente une file

  • Les méthodes sont proposées sous deux formes

    lance une exception retourne une valeur spéciale

    Insertion

    add(e)

    offer(e)

    Suppression

    remove()

    poll()

    Accès

    element()

    peek()

  • La stratégie d’insertion/suppression est définie par l’implémentation

8.5.7. L’interface DeQueue

  • L’interface DeQueue représente une file à double entrée

    Accès en tête Accès en queue

    Exception

    Valeur spéciale

    Exception

    Valeur spéciale

    Insertion

    addFirst(e)

    offerFirst(e)

    addLast(e)

    offerLast(e)

    Suppression

    removeFirst()

    pollFirst()

    removeLast()

    pollLast()

    Accès

    getFirst()

    peekFirst()

    getLast()

    peekLast()

8.5.8. L’interface Map

  • L’interface Map représente un tableau associatif (dictionnaire)

    • i.e. un objet qui associe une clé à chaque valeur

  • Une clé peut correspondre à au plus une valeur

    pas de multi-map en Java ⇒ utiliser une map avec une liste de valeurs associées à chaque clé
  • Deux tableaux associatifs sont égaux s’ils représentent les mêmes associations clé/valeur

Les objets mutables comme clés d’une Map peuvent poser problème
public interface Map<K, V> {
  // Opérations de base
  int size();
  boolean isEmpty();
  V put(K key, V value);
  V get(Object key);
  V remove(Object key);
  boolean containsKey(Object key);
  boolean containsValue(Object value);

  // Opérations sur des groupes
  void putAll(Map<? extends K,? extends V> t);
  void clear();

  // Vues
  public Set<K> keySet();
  public Collection<V> values();
  public Set<Map.Entry<K,V>> entrySet();

  // Interface pour entrySet
  public interface Entry<K, V> {
    K getKey();
    V getValue();
    V setValue(V value);
  }

  // Factories
  static <K, V> Map<K, V> copyOf(Map<? extends K,? extends V> map); // Returns an unmodifiable Map containing the entries of the given Map.
  static <K, V> Map.Entry<K, V> entry(K k, V v); // Returns an unmodifiable Map.Entry containing the given key and value.
  static <K, V> Map<K, V> of(); // Returns an unmodifiable map containing zero mappings.
  static <K, V> Map<K, V> of(K k1, V v1); // Returns an unmodifiable map containing a single mapping.
  static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2); // Returns an unmodifiable map containing two mappings.
  // [...]
  static <K, V> Map<K, V> ofEntries(Map.Entry<? extends K, ? extends V>... entries); // Returns an unmodifiable map containing keys and values extracted from the given entries.

  // + des méthodes par défaut
}

8.5.9. Parcourir une Map

  • Les méthodes de vue comme collection permettent de voir une Map comme une collection de trois façons différentes

    keySet

    représente l'ensemble des clés

    values

    représente la collection des valeurs

    entrySet

    représente l’ensemble des couples (clé, valeur)

  • Ces méthodes fournissent un moyen pour parcourir une Map

8.5.10. Exemple : construire l’histogramme d’une image

Méthode de construction de l’histogramme
/**
 * Produit l'histogramme d'une image.
 *
 * @param img l'image à analyser
 * @return l'histogramme de l'image sous la forme d'un dictionnaire (couleur, fréquence)
 */
public static Map<Integer, Long> frequency(int[][] img) {
    Stream<Integer> imgColors = Arrays.stream(img)
            .flatMap(c -> Arrays.stream(c).boxed());
    Map<Integer, Long> frequencyMap = imgColors.collect(
            Collectors.groupingBy(
                    Function.identity(), Collectors.counting()
            )
    );
    return frequencyMap;
}
Manipulation de l’histogramme
int[][] image = {
        {0, 1, 12, 1},
        {1, 12, 12, 12},
        {0, 12, 12, 0}
};

Map<Integer, Long> histogramme = frequency(image);
System.out.println(histogramme); // {0=3, 1=3, 12=6}
System.out.println(histogramme.keySet()); // [0, 1, 12]

Optional<Long> max = histogramme.values().stream()
        .max(Comparator.naturalOrder());
System.out.format("Fréq. max. : %d%n", max.orElse(-1L)); // Fréq. max. : 6

8.5.11. Ordonner des objets

Le problème
  • Comment ordonner des objets selon leur ordre naturel (lexicographique pour les chaînes, chronologique pour les dates, …​) ?

En Java
  • La solution proposée est d’implémenter l’interface Comparable

L’interface Comparable
public interface Comparable<T> {
  public int compareTo(<T> o);
}
  • La plupart des classes de la librairie Java implémentent cette interface

  • compareTo doit retourner un entier négatif (respectivement zéro, un entier positif) si l’objet est inférieur (respectivement égal, supérieur) au paramètre

  • Si l’argument n’est pas du bon type, compareTo doit lancer l’exception ClassCastException

  • L’ordre ainsi défini doit induire un ordre partiel (cf. la documentation de Comparable)

Exemple : définir un ordre naturel sur des personnes
Déclaration de la classe
class Person implements Comparable<Person> {
Redéfinition de la comparaison selon l’ordre naturel
/**
 * Compare deux personnes.
 * La comparaison se fait d'abord selon l'ordre
 * lexicographique du nom puis selon le prénom.
 *
 * @param p une personne
 * @return la valeur de comparaison entre les chaînes représentant les noms
 * ou entre les prénoms si les noms sont égaux.
 */
@Override
public int compareTo(Person p) {
    int cmpNom = nom.compareTo(p.nom);
    return (cmpNom != 0 ? cmpNom : prenom.compareTo(p.prenom));
}

8.5.12. Ordonner des objets selon un ordre spécifique

Le problème
  • Comment ordonner des objets selon un ordre particulier (différent de l’ordre naturel) ?

En Java
  • La solution proposée est de fournir un comparateur, i.e. une instance d’une classe implémentant l’interface Comparator

L’interface Comparator
public interface Comparator<T> {
  int compare(T o1, T o2);
}
  • compare doit retourner un entier négatif (respectivement zéro, un entier positif) si le premier paramètre est inférieur (respectivement égal, supérieur) au second

  • Si l’argument n’est pas du bon type, compare doit lancer l’exception ClassCastException

  • L’ordre ainsi défini doit induire un ordre partiel (cf. la documentation de Comparator)

Exemple : définir un ordre spécifique sur des personnes
package fr.uvsq.refcardjava.collections;

import java.util.Comparator;

/**
 * Permet de comparer des personnes selon leur âge.
 *
 * @author Stéphane Lopes
 * @version fév. 2017
 */
class ByAgeComparator implements Comparator<Person> {
    /**
     * Compare deux personnes selon leur âge.
     *
     * @return l'écart d'age entre les deux personnes.
     */
    public int compare(Person p1, Person p2) {
        return p1.getAge() - p2.getAge();
    }
}
Exemple : trier une liste de personnes
List<Person> lst = Arrays.asList(
        new Person("Ariane", "Dupond", 9),
        new Person("Cassiopée", "Dupond", 8),
        new Person("Hélios", "Martin", 4)
);

Collections.sort(lst); // tri selon l'ordre naturel
System.out.println(lst);

Collections.sort(lst, new ByAgeComparator()); // peut être remplacé par
lst.sort((p1, p2) -> p1.getAge() - p2.getAge()); // peut être remplacé par
lst.sort(Comparator.comparing(Person::getAge)); // tri selon l'âge
System.out.println(lst);

8.5.13. L’interface SortedSet

  • Cette interface permet de maintenir les éléments d’un ensemble en ordre croissant selon l’ordre naturel ou un ordre spécifié par un comparateur

  • Elle fournit des méthodes pour :

    • manipuler un interval d’éléments

    • accéder au plus petit ou au plus grand élément

    • récupérer le comparateur utilisé (s’il existe)

  • Les opérations héritées de Set fonctionnent à l’identique mais :

    • l’itérateur respecte l’ordre

    • toArray conserve l’ordre

public interface SortedSet<E> extends Set<E> {
  // Vue par intervalle
  SortedSet<E> subSet(E fromElement, E toElement);
  SortedSet<E> headSet(E toElement);
  SortedSet<E> tailSet(E fromElement);

  // Extrémités
  E first();
  E last();

  // Comparateur
  Comparator<? super E> comparator();
}

8.5.14. L’interface SortedMap

  • Cette interface permet de maintenir une Map ordonnée selon l’odre naturel de ses clés ou selon un comparateur

  • Elle fournit des méthodes pour:

    • manipuler un interval d’éléments

    • accéder au plus petit ou au plus grand élément

    • récupérer le comparateur utilisé (s’il existe)

public interface SortedMap<K, V> extends Map<K, V> {
  // Range-view
  SortedMap<K, V> subMap(K fromKey, K toKey);
  SortedMap<K, V> headMap(K toKey);
  SortedMap<K, V> tailMap(K fromKey);

  // Endpoints
  K firstKey();
  K lastKey();

  // Comparator access
  Comparator<? super K> comparator();
}

8.6. Les implémentations

  • Les implémentations sont les structures de données proprement dites

  • On en trouve plusieurs sortes en Java

    • les implémentations généralistes sont les plus couramment utilisées

    • les implémentations spécialisées sont conçues pour un cas particulier

    • les implémentations supportant la concurrence pour les applications multi-threads

    • les implémentations "décorations" permettent de modifier les caractéristiques d’une autre implémentation

    • les implémentations "simples" sont des implémentations minimalistes optimisées pour un cas particulier (singleton par exemple)

    • les implémentations abstraites servent de base pour le développement d’implémentation personnalisée

8.6.1. Implémentations généralistes

Interface

Hashage

Tableau dyn.

Arbre équ.

Liste chaînée

Hash. + chaîn.

Set

HashSet

TreeSet

LinkedHashSet

List

ArrayList

LinkedList

Queue

ArrayDeque

LinkedList

DeQueue

ArrayDeque

LinkedList

Map

HashMap

TreeMap

LinkedHashMap

  • Les implémentations principales sont en gras

  • TreeSet (respectivement TreeMap) implémente également SortedSet (respectivement SortedMap)

  • Queue possède aussi pour implémentation PriorityQueue

Caractéristiques des implémentations généralistes
  • Toutes les implémentations implémentent toutes les méthodes optionnelles

  • Toutes sont sérialisables

  • Toutes supportent l’opération clone

  • Elles ne sont pas synchronisées (pour des raisons de performance)

Utilisation des implémentations généralistes
  1. Choisir un type d’implémentation

  2. Créer une instance de cette implémentation

  3. La lier à une référence sur l’interface correspondante

    • le programme reste alors indépendant du choix de l’implémentation

Set<Integer> unEnsemble = new HashSet<>();
List<Integer> uneList = new ArrayList<>();
Map<String, String> uneMap = new HashMap<>();
Les implémentations généralistes de Set
  • HashSet est plus rapide mais ne garantit pas l’ordre

    • la plupart des opérations sont en temps constant

    • la capacité et le facteur de charge permettent d’affiner les performances de HashSet

  • TreeSet maintient l’ordre des éléments

    • la plupart des opérations sont en temps logarithmique

on choisit HashSet sauf si on a besoin d’un ordre sur les éléments
Les implémentations généralistes de Map
  • Situation analogue à Set

on choisit HashMap sauf si on a besoin d’un ordre sur les éléments
Les implémentations de List
  • ArrayList est plus rapide

    • permet un accès par position en temps constant

    • pas d’allocation à chaque ajout

    • on peut passer une capacité au constructeur de ArrayList

    • possède les opérations ensureCapacity et trimToSize en plus de celle de l’interface List

  • LinkedList est linéaire pour l’accès par position

    • adaptée si on fait beaucoup d’insertions/suppressions en milieu de liste (opérations en temps constant mais le facteur constant est élevé)

    • possède les opérations addFirst, getFirst, removeFirst, addLast, getLast, et removeLast

on choisit ArrayList sauf si on a beaucoup de modifications en milieu de liste

8.6.2. Quelques implémentations spécialisées

  • EnumSet (Set) est une implémentation efficace (vecteur de bits) pour les énumérations

  • CopyOnWriteArraySet (Set) effectue une copie de l’ensemble pour chaque modification

  • CopyOnWriteArrayList (List) effectue une copie de la liste pour chaque modification

  • EnumMap (Map) permet d’associer une instance d’énumération à une valeur

  • WeakHashMap (Map) autorise la libération de la mémoire d’une paire (clé, valeur) dès que la clé n’est plus référencée

8.6.3. Implémentations "décorations"

  • Une telle implémentation délègue les traitements principaux à une collection particulière mais y ajoute un certain nombre de fonctionnalités

    • modèle de conception Décorateur

  • Ces implémentations sont anonymes

    • pas de classe publique mais une méthode de fabrication statique

    • on les obtient par des méthodes de classe (static factory method) de la classe Collections

  • Plusieurs catégories sont disponibles :

    • avec synchronisation pour rendre les collections "tolérantes aux threads"

      List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());
    • non modifiables pour supprimer toute possibilité de modification d’une collection

      Collection<String> collec = Collections.unmodifiableCollection(uneCollection);
    • vérifiant le type dynamiquement

      Set<String> set = Collections.checkedSet(new HashSet<String>(), String.class);

8.6.4. Les implémentations "simples"

  • Ces implémentations sont des "mini" implémentations plus pratiques et généralement plus performantes que les implémentations généralistes

    • Arrays.asList permet de manipuler un tableau comme une liste

    • Collections.nCopies génère une liste non modifiable contenant de multiples copies du même élément

    • Collections.singleton génère un ensemble non modifiable contenant un unique élément

    • emptySet, emptyList et emptyMap de la classe Collections représentent l’ensemble, la liste et le tableau associatif vides

Utiliser des implémentations "simples"
// Créer une liste de taille fixe
List<String> list = Arrays.asList(new String[size]);

// Créer une liste de 1000 éléments initialisés à null
List<Type> l = new ArrayList<>(Collections.nCopies(1000, (Type)null));

// Ajouter 10 fois la chaîne "element" à une collection
uneCollection.addAll(Collections.nCopies(10, "element"));

// Supprimer toutes les occurences de e dans la collection
c.removeAll(Collections.singleton(e));

// Supprimer tous les juristes d'une map
profession.values().removeAll(Collections.singleton(JURISTE));

// Récupérer une liste vide
List<String> s = Collections.emptyList();

8.6.5. Écrire une implémentation

  • La notion d'implémentation abstraite simplifie l’écriture d’une implémentation

  • Une implémentation abstraite est un squelette d’implémentation d’une collection

  • Processus pour écrire son implémentation

    1. choisir une implémentation abstraite appropriée

    2. implémenter les méthodes abstraites (et éventuellement certaines méthodes concrêtes)

    3. tester l’implémentation obtenue

    4. si les performances sont importantes, étudier les caractéristiques des méthodes héritées et les redéfinir si nécessaire

Principales implémentations abstraites
  • AbstractCollection pour une collection quelconque

    • les méthodes iterator et size doivent être fournies

  • AbstractSet pour un ensemble (même utilisation que AbstractCollection)

  • AbstractList pour une liste basée sur une structure à accès aléatoire (comme un tableau)

    • les méthodes get(int) et size doivent être fournies

  • AbstractSequentialList pour une liste basée sur une structure à accès séquentiel (comme une liste chaînée)

    • les méthodes listIterator et size doivent être fournies

  • AbstractQueue nécessite de fournir les méthodes offer, peek, poll et size ainsi qu’un itérateur supportant remove

  • AbstractMap pour un tableau associatif

    • la vue entrySet doit être fournie

8.7. Les algorithmes

  • Les algorithmes sont des méthodes de classe de la classe Collections

  • Le premier paramètre de ces algorithmes est la collection traitée

  • La plupart opèrent sur des listes

8.7.1. Quelques algorithmes disponibles

  • tri : sort (complexité en \$n log(n)\$, stable)

  • mélange : shuffle

  • manipulation des données : reverse, fill, copy, swap, addAll

  • recherche dans une collection triée : binarySearch

  • composition : frequency, disjoint

  • extremum : min, max

List<Integer> l = new ArrayList<Integer>();
// ...
Collections.sort(l);
int pos = Collections.binarySearch(l, key);

9. La bibliothèques streams et la programmation fonctionnelle en Java

9.1. Fonction lambda

  • Une fonction lambda est une fonction anonyme

  • En Java, la syntaxe est composée

    • d’une liste de paramètres formels entre parenthèses

    • d’une flèche →

    • d’une expression ou d’un bloc d’instructions

// En précisant le type et avec un bloc
(Person p1, Person p2) -> {
    return p1.getAge() - p2.getAge()
}
// Avec une expression (sans return)
(Person p1, Person p2) -> p1.getAge() - p2.getAge()
// Le type des paramètres est optionnel
(person1, person2) -> person1.getAge() - person2.getAge()
// Avec un seul paramètres, les parenthèses sont optionnelles
person -> person.getAge()

9.2. Fonction lambda et interface

  • Une fonction lambda peut être utilisée quand une interface fonctionnelle est attendue

  • Une interface fonctionnelle (functional interface) ne doit comporter qu’une unique méthode abstraite

  • L’annotation @FunctionalInterface permet de marquer de telles interfaces

En utilisant l’interface et une classe anonyme
uneliste.sort(new Comparator<Person> {
    public int compare(Person p1, Person p2) {
        return p1.getAge() - p2.getAge();
    }
});
En utilisant une fonction lambda
uneliste.sort((person1, person2) -> person1.getAge() - person2.getAge());

9.3. Fermeture

  • Une fermeture est une fonction lamda avec son contexte

    public class ClosureDemo {
        public static Function<Integer, Integer> ajouteur(int n1) {
            return n2 -> n1 + n2;
        }
        public static void main(String[] args) {
            Function<Integer, Integer> ajouteur10 = ajouteur(10);
            assert ajouteur10(1) == 11;
        }
    }
  • En Java, une fermeture ne peut pas modifier les variables de son contexte

9.4. Référence de méthode

  • Une référence de méthode permet d’utiliser une méthode comme fonction lambda

  • Quatre types de référence de méthode existent

    Catégorie Exemple

    Référence à une méthode de classe

    ContainingClass::staticMethodName

    Référence à une méthode d’un objet précis

    containingObject::instanceMethodName

    Référence à une méthode d’un objet quelconque

    ContainingType::methodName

    Référence à un constructeur

    ClassName::new

Exemples de référence de méthode
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

numbers.forEach(e -> System.out.println(e)); // lambda
numbers.forEach(System.out::println); // référence de méthode (objet précis)

numbers.stream()
       // .map(e -> String.valueOf(e)) // lambda
       .map(String::valueOf) // référence de méthode (méthode de classe)
       .forEach(System.out::println);

numbers.stream()
       .map(String::valueOf(e))
       // .map(e -> e.toString()) // lambda
       .map(String::toString) // référence de méthode (objet quelconque)
       .forEach(System.out::println);

9.5. Parcourir une collection (de l’itératif au fonctionnel)

9.5.1. Itérateur externe avec une boucle classique

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

for(int i = 0; i < numbers.size(); i++) {
    System.out.println(numbers.get(i));
}
  • Beaucoup de "détails" sont visibles

    • indices limites

    • test d’arrêt

    • accès aux éléments

9.5.2. Itérateur externe avec une boucle foreach

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

for(int e : numbers) {
    System.out.println(e);
}
  • Masque les détails mais demeure impératif

9.5.3. Itérateur interne

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

numbers.forEach(new Consumer<Integer>() {
    public void accept(Integer value) {
        System.out.println(value);
    }
});

9.5.4. Itérateur interne avec lambda

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

numbers.forEach(value -> System.out.println(value));
  • Beaucoup plus concis et lisible

9.5.5. Itérateur interne avec référence de méthode

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

numbers.forEach(System.out::println);
  • Concis et lisible

9.6. Streams

  • Un flux (stream) est une séquence d’éléments

  • Il véhicule des éléments à partir d’une source à travers un pipeline

  • Un flux ne stocke aucune donnée

    Ce n’est pas une structure de données

9.6.1. Pipeline

  • Un pipeline est une séquence d’opérations applicables sur un flux

  • Il comporte

    • une source (collection, tableau, fonction génératrice, flux I/O)

    • une séquence d’opérations intermédiaires (chacune produit un nouveau stream)

    • une opération terminal qui calcule un résultat

  • Une opération ne modifie pas le flux d’origine

  • L’évaluation est paresseuse

  • Peut être exécuté séquentiellement ou en parallèle

9.6.2. Opération terminale

  • Une opération terminal traverse le flux pour produire un résultat ou un effet de bord

  • Après exécution, le flux est considéré comme consommé et ne peut pas être réutilisé

  • Une opération terminale est également nommée réduction

Un exemple de stream
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// Calcul le total des doubles des nombres pairs
System.out.println(
    numbers.stream()
           .filter(e -> e % 2 == 0)
           .mapToInt(e -> e * 2)
           .sum());

9.6.3. L’interface Stream

  • L’interface java.util.stream.Stream regroupe l’ensemble des opérations applicables aux pipelines

  • Les interfaces IntStream, LongStream et DoubleStream sont spécialisés pour les types primitifs

9.6.4. Création d’un flux

  • À partir d’une collection

    • Collection.stream, Collection.parallelStream (flux parallèle)

  • À partir d’un tableau (Arrays.stream)

  • À partir d’un intervalle

    • IntStream.range, IntStream.rangeClosed (aussi avec LongStream)

  • À partir de valeurs

    • Stream.of (aussi dans IntStream, LongStream et DoubleStream)

  • À partir des méthodes de classe de Stream

    • concat, empty, generate/iterate (flux infini)

  • À partir de nombres aléatoires (doubles, ints et longs de la classe Random)

  • À partir d’un fichier (Files.lines, BufferedReader.lines)

9.6.5. Quelques opérations intermédiaires

Opération Description

filter

retourne les éléments respectant un prédicat

map

applique une fonction à chaque élément

flatMap

désimbrique des flux

limit

tronque un flux

skip

ignore les premiers éléments

distinct

élimine les doublons (avec état)

sorted

retourne un flux trié (avec état)

Exemples d’opérations intermédiaires
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

numbers.stream()
       .filter(e -> e % 2 == 0)
       .forEach(System.out::println)); // 2 4 6 8 10

numbers.stream()
       .filter(e -> e % 2 == 0)
       .map(e -> e * 2)
       .forEach(System.out::println)); // 4 8 12 16 20

9.6.6. Quelques opérations terminales

Opération Description

reduce

applique une réduction avec une fonction d’accumulation

count

compte les éléments

sum, …​

réduction spécialisée sur les flux de types primitifs

collect

réalise une réduction par modification

allMatch

teste si tous les éléments respectent un prédicat

forEach

exécute une action pour chaque élément

Calculer une somme avec reduce
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

System.out.println(
    numbers.stream()
           .filter(e -> e % 2 == 0)
           .map(e -> e * 2.0)
           .reduce(0.0, (carry, e) -> carry + e));
Calculer une somme avec sum et DoubleStream
System.out.println(
    numbers.stream()
           .filter(e -> e % 2 == 0)
           .mapToDouble(e -> e * 2.0)
           .sum());
Les doubles des nombres pairs dans une liste avec collect
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 1, 2, 3, 4, 5);

List<Integer> doubleOfEven =
    numbers.stream()
           .filter(e -> e % 2 == 0)
           .map(e -> e * 2)
           .collect(Collectors.toList());
System.out.println(doubleOfEven);
Les doubles des nombres pairs dans un dictionnaire avec collect
Map<Integer, Integer> doubleOfEven =
    numbers.stream()
           .filter(e -> e % 2 == 0)
           .collect(Collectors.toMap(
               Function.identity(),
               i -> i * 2));
System.out.println(doubleOfEven);
Un dictionnaire des personnes regroupées par nom
List<Person> persons = //...

Map<Person, List<Person>> personsByName =
    persons.stream()
           .collect(Collectors.groupingBy(Person::getName));
System.out.println(personsByName);
Un dictionnaire des noms de personnes regroupés par sexe
Map<Person.Gender, List<String>> namesByGender =
    persons.stream()
           .collect(Collectors.groupingBy(
               Person::getGender,
               Collectors.mapping(
                   Person::getName,
                   Collectors.toList())));

9.6.7. Flux infinis

  • Les méthodes de classe Stream.generate et Stream.iterate créent un flux infini

  • Ce type de flux peut exister grâce à l'évaluation paresseuse

    • l’application d’opérations élémentaires ne provoque pas la traversée du pipeline

    • seules les opérations terminales déclenchent le traitement

Afficher les 10 premiers entiers
Stream<Integer> integers = Stream.iterate(0, i -> i + 1);
integers.limit(10)
        .forEach(System.out::println);
Il ne faut jamais réduire l’intégralité d’un flux infini.

10. Pour aller plus loin…​

10.1. Lisez !

pour les langages, attention aux versions (Java >= 8)
Ayez l’esprit critique (toutes les pages ne se valent pas)

10.2. Pratiquez !

  • Kata, Koans, Coding Dojo

    Ajoutez des contraintes
    • les types primitifs doivent systématiquement être encapsulés

    • pas de méthodes de plus de 4 lignes

    • un seul niveau d’indentation par méthode

    • pas plus de 2 paramètres par méthode

    • pas d’utilisation de la souris

    • …​

  • Codewars, CodinGame

  • Participez à un projet Open Source