1. Langage Java
1.1. Le langage Java
-
Le langage Java est un langage de programmation orienté objet développé chez Sun Microsystems (racheté par Oracle en 2009) et lancé officiellement en 1995
The Java Language Environment: A White Paper, James Gosling, 1996. -
Différentes éditions de la plateforme Java sont disponibles
-
Java SE pour les postes de travail,
-
Java EE pour les serveurs, et
-
Java Embedded pour les terminaux à ressources contraintes.
Cet aide-mémoire se focalise sur Java SE
-
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
1.4.1. Compilation en ligne de commande (JDK)
javac
génère le bytecode en ligne de commande.$ javac <options> <fichiers source>
--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 ( |
-sourcepath
|
fixe le chemin de recherche des sources |
--target |-target
|
précise la version de la VM cible ( |
$ 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...]
class
|
le nom de la classe contenant le programme principal (le |
--class-path |-classpath |-cp
|
fixe le chemin de recherche des classes compilées |
-jar
|
exécute un programme encapsulé dans un fichier |
$ 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)
-
David J. Barnes & Michael Kölling. Objects First with Java. Pearson. 2016.
-
Marc Loy, Patrick Niemeyer, Daniel Leuck, Learning Java: An Introduction to Real-world Programming With Java. O’Reilly Media. 2020.
1.7.3. Bibliographie (Perfectionnement)
-
Joshua Bloch. Effective Java 3rd Edition. Addison-Wesley Professional. 2017.
-
Kevlin Henney & Trisha Gee, 97 Things Every Java Programmer Should Know. 2020.
-
Joshua Bloch & Neal Gafter. Java Puzzlers. Addison-Wesley. 2005.
-
Cay S. Horstmann & Gary Cornell, Core Java. Prentice Hall. 2015.
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
|
|
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 |
2.3. Littéraux
Un littéral est la représentation dans le code source d’une valeur d’un type.
Entiers |
|
||
Flottants |
|
||
Booléens |
|
||
Caractères |
|
||
Chaînes |
|
||
Null |
|
-
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.
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
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émentsint[] 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 variableint[] 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
deSystem
permet de copier efficacement un tableau
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.
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.
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
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. |
|
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. |
|
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
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 ; :
|
4 | i a pour valeurs successives 1 , 2 , …, 10 |
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 variablevariable
à l’objetobjet
-
Il est possible de lier la variable lors de sa déclaration (initialisation)
Si possible, toujours initialiser une variable lors de sa déclaration
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
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 classeObject
-
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).
-
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
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 |
|
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
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

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
etabstract
-
Toutes les constantes de l’interface sont implicitement
public
,static
, etfinal
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 typeComparable
// 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 ouvoid
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 absentNiveau 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. |
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);
}
}
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
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;
}
}
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. |
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 :
|
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<>(/* ... */);
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;
}
}
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
Collection<E>
et List<E>
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 clauseextends
si elle existe) -
La classe doit alors implémenter toutes les méthodes de l’interface ou être déclarée abstraite
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é
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éclarationclass 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 classeObject
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
-
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 |
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

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)

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
|

toString
)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());
}
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);
}
// 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 deObject
) -
Certaines méthodes de
Object
peuvent être redéfinies pour s’adapter à la sous-classeprotected 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
ethashCode
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’objetla 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éthodeObject.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 classeObject
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
|
/**
* 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();
}
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éclarationL’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

/**
* 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);
}
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);
}
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);
}
// 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
-
Il existe d’autres adaptateurs :
Boolean
,Character
,Void
-
Les adaptateurs sont des exemples du pattern de conception Adaptateur
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

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 sourcepackage 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éclarationpublic 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’instructionpackage
-
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 fichierMaClasse.java
du répertoirefr/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
-
Récupérer la bibliothèque
-
manuellement (téléchargement du
jar
) -
automatiquement (outils de gestion des dépendances comme maven ou gradle)
-
-
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
-
-
Consulter l’interface de la bibliothèque
-
toute bibliothèque Java est distribuée avec sa documentation au format javadoc
-
-
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
-
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
-
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>
-
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); } }
-
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
-
É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 exceptiontry { // 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 bloccatch
oufinally
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 bloctry
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ètrecatch (<Type> <variable>) { // Instructions }
-
<Type>
représente le type de l’exception et doit être une classe dérivant deThrowable
-
<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 bloctry
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 deThrowable
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
6.11. Créer des classes exceptions
-
Déterminer dans quelles méthodes et sous quelles conditions des exceptions seront lancées
-
Choisir le type de chaque exception
-
utiliser une exception existante
-
en créer une nouvelle
-
-
Choisir quelle sera la super-classe des exceptions définies
-
Exception
ou l’une de ses sous-classes -
les exceptions dérivées de
RuntimeException
ne sont pas contrôlées
-
6.11.1. Exemple : une hiérarchie d’exceptions pour une pile

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);
}
}
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");
}
}
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
-
la gestion des flux (
java.io
) -
les fichiers à accés aléatoires (
java.nio.channels.SeekableByteChannel
,RandomAccessFile
) -
des fonctionnalités avancées (
java.nio.channels
) -
l’interaction avec le système de fichiers (
java.nio.file
,File
) -
l’entrée et la sortie standard (
PrintStream
,java.util.Scanner
,Console
) -
la gestion des archives (
java.util.zip
,java.util.jar
) -
les fichiers d’images (
javax.imageio
)
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

Reader
et Writer

Reader

Writer
7.2.2. Flux d’octets
-
Les classes
InputStream
etOutputStream
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
et OutputStream

InputStream

OutputStream
7.2.3. Principaux flux par type d’I/O
Type d’I/O | Flux de catactères | Flux d’octets |
---|---|---|
Mémoire |
|
|
|
||
Fichier |
|
|
Affichage |
|
|
7.2.4. Principaux flux par fonction
Type d’I/O | Flux de catactères | Flux d’octets |
---|---|---|
Avec buffer |
|
|
Conv. de données |
|
|
Sérialisation |
|
|
Conv. oct./car. |
|
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 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 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 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 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
etDataOutputStream
pour les I/O des types primitifs -
BufferedInputStream
etBufferedOutputStream
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);
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]);
}
}
}
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 typePrintStream
) -
la sortie standard (attribut
out
de typeInputStream
) -
la sortie d’erreurs (attribut
err
de typePrintStream
)
Flux d’affichage PrintStream
(ou PrintWriter
)
-
PrintStream format( /* … */)
affiche une chaîne selon un format-
PrintStream printf(/* … */)
fait la même chose
-
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
avecInputStreamReader
(voire avecBufferedReader
)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
-
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);
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
etObjectOutputStream
-
ObjectOutputStream
implémente les interfacesDataOutput
etObjectOutput
-
ObjectInputStream
implémente les interfacesDataInput
etObjectInput
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);
}
}
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
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
deObjectOutputStream
-
defaultReadObject
deObjectInputStream
-
-
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
etreadObject
-
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
- Java
-
Java Collections Framework, Apache Commons Collections, Google Guava
- C++
-
Standard Containers de la STL (Standard Template Library), Boost
- Python
- C#
- Rust
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
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’interfaceCollection
-
Deux ensembles sont égaux s’ils contiennent les mêmes éléments
-
Sémantique des méthodes de groupe
-
s1.containsAll(s2)
retournetrue
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’interfaceIterator
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éeAccè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érenteskeySet
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
/**
* 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;
}
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
-
Comment ordonner des objets selon leur ordre naturel (lexicographique pour les chaînes, chronologique pour les dates, …) ?
-
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’exceptionClassCastException
-
L’ordre ainsi défini doit induire un ordre partiel (cf. la documentation de
Comparable
)
Exemple : définir un ordre naturel sur des personnes
class Person implements Comparable<Person> {
/**
* 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
-
Comment ordonner des objets selon un ordre particulier (différent de l’ordre naturel) ?
-
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’exceptionClassCastException
-
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. |
|
|
|
|
||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|
-
Les implémentations principales sont en gras
-
TreeSet
(respectivementTreeMap
) implémente égalementSortedSet
(respectivementSortedMap
) -
Queue
possède aussi pour implémentationPriorityQueue
-
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)
-
Choisir un type d’implémentation
-
Créer une instance de cette implémentation
-
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
ettrimToSize
en plus de celle de l’interfaceList
-
-
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
, etremoveLast
-
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
etemptyMap
de la classeCollections
représentent l’ensemble, la liste et le tableau associatif vides
-
// 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
-
choisir une implémentation abstraite appropriée
-
implémenter les méthodes abstraites (et éventuellement certaines méthodes concrêtes)
-
tester l’implémentation obtenue
-
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
etsize
doivent être fournies
-
-
AbstractSet
pour un ensemble (même utilisation queAbstractCollection
) -
AbstractList
pour une liste basée sur une structure à accès aléatoire (comme un tableau)-
les méthodes
get(int)
etsize
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
etsize
doivent être fournies
-
-
AbstractQueue
nécessite de fournir les méthodesoffer
,peek
,poll
etsize
ainsi qu’un itérateur supportantremove
-
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
uneliste.sort(new Comparator<Person> {
public int compare(Person p1, Person p2) {
return p1.getAge() - p2.getAge();
}
});
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
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);
}
});
-
Syntaxe plus déclarative
-
L’argument de
forEach
est ici une classe anonyme -
java.util.function.Consumer<T>
est une interface fonctionnelle -
Consumer
opère par effet de bord
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
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
etDoubleStream
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 avecLongStream
)
-
-
À partir de valeurs
-
Stream.of
(aussi dansIntStream
,LongStream
etDoubleStream
)
-
-
À partir des méthodes de classe de
Stream
-
concat
,empty
,generate
/iterate
(flux infini)
-
-
À partir de nombres aléatoires (
doubles
,ints
etlongs
de la classeRandom
) -
À partir d’un fichier (
Files.lines
,BufferedReader.lines
)
9.6.5. Quelques opérations intermédiaires
Opération | Description |
---|---|
|
retourne les éléments respectant un prédicat |
|
applique une fonction à chaque élément |
|
désimbrique des flux |
|
tronque un flux |
|
ignore les premiers éléments |
|
élimine les doublons (avec état) |
|
retourne un flux trié (avec état) |
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 |
---|---|
|
applique une réduction avec une fonction d’accumulation |
|
compte les éléments |
|
réduction spécialisée sur les flux de types primitifs |
|
réalise une réduction par modification |
|
teste si tous les éléments respectent un prédicat |
|
exécute une action pour chaque élément |
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));
sum
et DoubleStream
System.out.println(
numbers.stream()
.filter(e -> e % 2 == 0)
.mapToDouble(e -> e * 2.0)
.sum());
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);
collect
Map<Integer, Integer> doubleOfEven =
numbers.stream()
.filter(e -> e % 2 == 0)
.collect(Collectors.toMap(
Function.identity(),
i -> i * 2));
System.out.println(doubleOfEven);
List<Person> persons = //...
Map<Person, List<Person>> personsByName =
persons.stream()
.collect(Collectors.groupingBy(Person::getName));
System.out.println(personsByName);
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
etStream.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
-
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 !
-
Des livres (cf. la bibliographie en début de cours)
-
Des blogs (DZone, InfoQ, Java Annotated Monthly), wiki (C2 wiki), flux, …
-
Des questions/réponses (Stack Overflow)
-
Du code (Tips For Reading Code, github, Programs To Read)
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
-
…
-
-
Participez à un projet Open Source