1. Introduction

1.1. Caractéristiques des langages de programmation

1.1.1. Implémentation des langages

  • Avec un langage compilé, le code source du programme est transformé en code machine par le compilateur

  • Dans un langage interprété, le code source du programme est exécuté "à la volée" par l'interpréteur

  • Certains langages sont à la fois compilés et interprétés

  • Il existe des approches intermédiaires (compilation Just In Time (JIT))

1.1.2. C est compilé

Compilation séparée (produit util.o et main.o)
$ gcc -c util.c
$ gcc -c main.c
Édition de liens (produit monprog)
$ gcc -o monprog main.o util.o
monprog est exécutable
$ ./monprog

1.1.3. PHP est interprété

Exécution d’un programme PHP
$ php monprog.php

1.1.4. Java est compilé puis interprété (JIT)

Compilation en bytecode (produit Main.class)
$ javac Main.java
Exécution avec la JVM (Java Virtual Machine)
$ java Main

1.1.5. Langage de scripts

  • Un script est un programme destiné à automatiser l’enchaînement de tâches dans un environnement particulier

  • Un langage de scripts est un langage de programmation permettant de développer des scripts

  • Il permet d’invoquer les primitives du système sous-jacent

  • Il dispose en général d’un REPL (Read-Eval-Print Loop)

  • Quelques exemples

    • shells pour les OS : Bash, Zsh, tcsh

    • ECMAScript (Javascript) pour les navigateurs web

    • Lua embarqué dans une application (VLC Media Player, jeu Battle for Wesnoth)

1.1.6. Système de typage

  • Un système de typage attribue des types aux éléments du langage

  • Attribuer un type à une expression permet de limiter les erreurs de programmation

    • en définissant ce qu’il est possible de faire avec une expression

    • en définissant les règles de compatibilité entre expressions

    • en vérifiant ces contraintes

1.1.7. Typage explicite vs. implicite

  • Le typage est explicite si les annotations de type sont visibles dans le code source

En C, chaque déclaration de variable précise son type
int nombre = 1;
double pi = 3.141592;
  • Le typage est implicite si les types ne sont pas précisés dans le code source

En PHP, la première affectation crée la variable
$nombre = 1;
$pi = 3.141592;
  • Des langages à typage explicite peuvent faire appel à l'inférence de types dans certaines situations

    • permet la déduction automatique des types

1.1.8. Typage statique vs. dynamique

  • Le typage est statique si l’information de type est associée à l’identificateur

    • ⇒ la vérification des types peut être réalisée lors de la compilation

  • Le typage est dynamique si l’information de type est portée par l’objet lui-même

    • ⇒ la vérification se fait durant l’exécution

1.1.9. Typage statique

  • Améliore la fiabilité du programme (plus de vérifications plus précoces)

  • Meilleur support des outils (IDE)

  • Meilleures performances

En C, les erreurs de type sont identifiées par le compilateur
double a = "une chaine";
// error: incompatible types when initializing type ‘double’ using type ‘char *’

1.1.10. Typage dynamique

  • Offre plus de souplesse dans l’écriture du code source

    • duck typing, data as code, métaprogrammation

  • Permet le prototypage rapide

En PHP, les erreurs de type peuvent passer inaperçues
$a = 1;
$a = "une chaine";
echo $a + 2; // affiche 2

1.1.11. Typage fort vs. faible

  • Le typage est fort si les manipulations entre données de types différents sont limitées et contrôlées

  • Le typage est faible si les possibilités de transtypage sont nombreuses et implicites

  • Ces notions sont relativement floues

Le C est à typage fort mais…​
int a = "une chaine";
printf("%d\n", a); // 443215...

1.1.12. Support des paradigmes de programmation

  • Un paradigme de programmation représente la façon d’aborder un problème et d’en concevoir la solution.

  • Quelques paradigmes

    • Programmation impérative

      • Programmation structurée

      • Programmation modulaire

      • Programmation par abstraction de données

      • Programmation objet

    • Programmation fonctionnelle

    • Programmation logique

  • Un langage supporte un paradigme quand il fournit les fonctionnalités pour utiliser ce style (de façon simple, sécurisée et efficace)

1.1.13. Exemple - Programmation logique avec Prolog

  • Prolog permet de définir et d’interroger une base de faits

  • Prolog est un langage déclaratif

  • Un fait est une assertion simple

- Idéfix est un chien
chien(idefix).
  • Une règle décrit une inférence à partir des faits

- Les chiens aiment les arbres
aimeLesArbres(X):- chien(X).
  • Une requête est une question sur la base de connaissance

- Idéfix aime-t'il les arbres ?
?- aimeLesArbres(idefix)

1.1.14. Solveur de Sudoku \$4 xx 4\$ en Prolog - Requête

- requête

| ?- sudoku([_, _, 2, 3,
             _, _, _, _,
             _, _, _, _,
             3, 4, _, _],
             Solution).

1.1.15. Solveur de Sudoku \$4 xx 4\$ en Prolog - Résolution 1/3

- la solution doit être unifiée avec le problème
- le problème comporte 16 chiffres
- chaque chiffre est compris entre 1 et 4 (fd_domain)

sudoku(Puzzle, Solution) :-
  Solution = Puzzle,
  Puzzle = [A1, A2, A3, A4,
            B1, B2, B3, B4,
            C1, C2, C3, C4,
            D1, D2, D3, D4],
  fd_domain(Puzzle, 1, 4),

1.1.16. Solveur de Sudoku \$4 xx 4\$ en Prolog - Résolution 2/3

- les blocs (lignes, colonnes et carrés) sont définis

  Row1 = [A1, A2, A3, A4],
  Row2 = [B1, B2, B3, B4],
  Row3 = [C1, C2, C3, C4],
  Row4 = [D1, D2, D3, D4],

  Col1 = [A1, B1, C1, D1],
  Col2 = [A2, B2, C2, D2],
  Col3 = [A3, B3, C3, D3],
  Col4 = [A4, B4, C4, D4],

  Square1 = [A1, A2, B1, B2],
  Square2 = [A3, A4, B3, B4],
  Square3 = [C1, C2, D1, D2],
  Square4 = [C3, C4, D3, D4],

1.1.17. Solveur de Sudoku \$4 xx 4\$ en Prolog - Résolution 3/3

- le prédicat valid reçoit une liste de 12 blocs
- la liste vide est valide
- la tête de la liste ne comporte pas de doublons (fd_all_different)
- le reste de la liste doit être valide

valid([]).
valid([Head|Tail]) :-
  fd_all_different(Head),
  valid(Tail).

- une solution possède des blocs valides

  valid([Row1, Row2, Row3, Row4,
         Col1, Col2, Col3, Col4,
         Square1, Square2, Square3, Square4]).

1.1.18. Exemple - Programmation fonctionnelle avec Haskell

  • Haskell est un langage fonctionnel

  • Possède un système de typage statique, fort et principalement implicite (inférence de types)

1.1.19. Haskell 1/2

-- Calcul de la fonction factorielle

-- Récursive
fact x = if x == 0 then 1 else fact (x - 1) * x

-- Pattern matching
fact 0 = 1
fact x = x * fact (x - 1)

-- Gardes
fact x
   | x > 1 = x * fact (x - 1)
   | otherwise = 1

-- Liste et intervalle
fac x = product [1..x]

1.1.20. Haskell 2/2

-- Fonctions d'ordre supérieure
mapList f [] = []
mapList f (x:xs) = f x : mapList f xs

-- Listes en compréhension et évaluation paresseuse
take 10 [ (i,j) | i <- [1..], j <- [1..], i < j ]

1.1.21. Langage impératif

  • Un langage impératif représente un programme comme une séquence d’instructions qui modifient son état au cours de son exécution

  • Un programme décrit comment aboutir à la solution du problème

  • Proche de l’architecture matérielle des ordinateurs (architecture de von Neumann)

  • De nombreux langages populaires sont de ce type (C, Java, Python)

1.1.22. Langage déclaratif

  • Un langage déclaratif permet de décrire ce que le programme doit faire (le quoi) et non pas comment il doit le faire (le comment)

  • Un programme respectant ce style décrit le problème à traiter

  • Quelques exemples : Prolog, SQL

  • Certains langages impératifs embarquent des constructions déclaratives

1.1.23. Gestion de la mémoire

  • La gestion de la mémoire dans un langage de programmation décrit comment les objets inutilisés sont identifiés et désalloués

    • nécessaire pour éviter les fuites de mémoire (memory leaks)

  • La plupart des langages ont une gestion automatique de la mémoire et s’appuient sur un ramasse-miettes (garbage collector)

En Java, le ramasse-miettes est chargé de libérer la mémoire allouée dynamiquement
int[] tableau = new int[10]; // allocation d'un tableau de 10 cases
// la désallocation est automatique
  • Les langages C et C++ ont une gestion manuelle de la mémoire

int[] tableau = malloc(10 * sizeof(int)); // allocation d'un tableau de 10 cases
// ...
free(tableau); // libération de la mémoire

1.1.24. Caractéristiques de quelques langages

Langage Implémentation Scripts Typage Paradigme Mémoire

C

Compilé

Non

explicite, statique

procédural

manuelle

Java

Compilé, interprété

Non

explicite, statique

OO, fonc., générique

auto

PHP

Interprété

Oui

implicite, dynamique

proc., OO

auto

Python

Compilé, Interprété

Oui

implicite, dynamique

proc., OO, fonc.

auto

Scala

Compilé, interprété

Oui

implicite, statique

OO, fonc., générique

auto

1.2. Quelques langages de programmation

1.2.1. Les langages "historiques"

1.2.6. Les langages "tendances"

  • Go, Robert Griesemer, Rob Pike, Ken Thompson (2009)

  • Groovy, Java Community Process (2003)

  • Julia, Jeff Bezanson, Stefan Karpinski, Viral B. Shah, Alan Edelman (2012)

  • Kotlin, JetBrains (2011)

  • R, Ross Ihaka, Robert Gentleman (1993)

  • Rust, Graydon Hoare (2010)

  • Scala, Martin Odersky (2003)

  • Swift, Apple (2014)

1.2.7. Critères de choix d’un langage

  • Adéquation aux besoins

    • domaine d’application, plateforme cible

  • Simplicité d’apprentissage/lisibilité du code source

    • "Programs must be written for people to read, and only incidentally for machines to execute", Harold Abelson

    • "Any fool can write code that a computer can understand. Good programmers write code that humans can understand.", Martin Fowler

    • "Perl – The only language that looks the same before and after RSA encryption", Keith Bostic

  • Richesse de la bibliothèque standard

  • Écosystème (bibliothèques tierces, outils)

  • Popularité (support)

  • Employabilité

1.3. Évolution des langages de programmation impératifs

1.3.1. Programmation structurée

Choisissez les procédures. Utiliser les meilleurs algorithmes que vous pourrez trouver.
  • L’accent est mis sur les traitements

  • Approche très utilisée depuis de nombreuses années

  • Approche de haut en bas (top-down)

  • A beaucoup amélioré la qualité du logiciel

Les données et les traitements restent indépendants.

  • Les données changent moins que les traitements

  • difficile d’assurer la cohérence entre les structures de données et les traitements qui les utilisent

1.3.2. Programmation modulaire

Choisissez vos modules. Découpez le programme de telle sorte que les données soient masquées par ces modules.
  • Un module est un ensemble de procédures connexes avec les données qu’elles manipulent

  • L’élément primordial passe de la conception des procédures à l’organisation des données

  • Principe de masquage de l’information

  • Permet la compilation séparée

Les types ainsi définis diffèrent dans leur utilisation (création, …​) des types de base.

  • l’initialisation d’une variable d’un type défini par l’utilisateur nécessite l’appel d’une fonction particulière du type (laissé à la charge du programmeur)

1.3.3. Programmation par abstraction de données

Choisissez les types dont vous avez besoin. Fournissez un ensemble complet d’opérations pour chaque type.
  • Utilise les types abstraits de données (TAD)

  • L'interface d’un type abstrait isole complètement l’utilisateur des détails d’implémentation

Il est nécessaire de modifier un type pour l’adapter.

1.3.4. Programmation orientée objet

Choisissez vos classes. Fournissez un ensemble complet d’opérations pour chaque classe. Rendez toute similitude explicite à l’aide de l’héritage.
  • Consiste à définir les classes puis à préciser les relations entre elles (notamment l'héritage)

    • définir des classes = définir des types utilisateurs

    • factoriser des comportements en créant des hiérarchies d’héritage

    • permet d’adapter un type existant sans le modifier

La conception objet est une tâche difficile.

1.3.5. Approche objet vs. approche structurée

  • L’approche objet est moins intuitive

    • décomposer un problème en une hiérarchie de fonctions atomiques et de données est plus naturel

  • La modélisation objet est difficile

    • rien dans les concepts de base de l’approche objet ne dicte comment modéliser la structure objet d’un système de manière pertinente.

    • Comment mener une analyse qui respecte les concepts objet ?

    • Sans un cadre méthodologique approprié, la dérive structurée de la conception est inévitable

  • L’application des concepts objet nécessite de la rigueur

    • Le vocabulaire précis est un facteur d’incompréhensions

1.3.6. Approche objet vs. langage de programmation

  • Certains développeurs ne pensent objet qu’à travers un langage de programmation

    • les langages ne sont que des outils implémentant certains concepts objet d’une certaine façon

    • les langages ne garantissent en rien l’utilisation adéquat de ces moyens techniques

Programmer en Java, en Python ou en C# n’est pas concevoir objet !

  • Seule une analyse objet conduit à une solution objet

    • i.e. qui respecte les concepts de base de l’approche objet.

  • Le langage de programmation est un moyen d’implémentation

    • il ne garantit pas le respect des concepts objet

1.3.7. Conception objet

Concevoir objet, c’est d’abord concevoir un modèle qui respecte les concepts objet.
  • Pour penser et concevoir objet, il faut savoir "prendre de la hauteur", jongler avec des concepts abstraits, indépendants des langages d’implémentation et des contraintes purement techniques

  • Les langages de programmation ne sont pas un support adéquat pour cela

  • Pour conduire une analyse objet cohérente, il ne faut pas directement penser en terme de pointeurs, d’attributs et de tableaux, mais en terme d’association, de propriétés et de cardinalités

1.3.8. Évolutions récentes

  • Concepts OO/modularité : Mixin/ Trait, Délégation, Aspect

  • Concepts issus de la programmation fonctionnelle : fonctions lambda/fermeture (closure), fonction d’ordre supérieure, évaluation parasseuse

  • Programmation parallèle/gestion de la concurrence : support des processeurs multi-cœurs

1.4. Le langage Java

1.4.1. Les plateformes Java

  • Java Platform Standard Edition (Java SE)

    • dédiée aux postes clients et aux stations de travail

  • Java Platform Enterprise Edition (Java EE)

    • dédiée aux applications d’entreprises (serveur, postes clients, …​)

  • Java Embedded

    • dédiée aux systèmes embarqués (Internet des Objets, …​)

1.4.3. Java Runtime Environment

  • 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

  • Déjà installé sur la plupart des systèmes d’exploitation

  • Le lanceur d’application (java) est l’outil ligne de commande permettant l’exécution de programme Java

Plus d’informations : Java Platform Overview

1.4.4. Java Development Kit

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

  • Doit être installé pour développer en Java

  • L’outil javac est le compilateur en ligne de commande

Plus d’informations : Java Platform Overview

1.4.5. Caractéristiques du langage

Simple

un développeur peut être rapidement opérationnel

Orienté-objet

bon respect des concepts OO

Interprété

la compilation génère un code intermédiaire qui est interprété

Portable

un programme compilé fonctionne sans modification sur différentes plateformes

Robuste

vérifications à la compilation et à l’exécution

Multithread

supporte la programmation concurrente

Adaptable

supporte le chargement dynamique de code

Sécurisé

le langage intègre un modèle de sécurité sophistiqué

1.4.6. 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.7. Compilation en ligne de commande (JDK)

$ javac <options> <fichiers source>
-g|\-g$:$none

gère les informations pour le débogage

-classpath|-cp

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

-source

précise la version des fichiers sources (1.6, …​, 1.8)

-sourcepath

fixe le chemin de recherche des sources

-encoding

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

-d

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

-target

précise la version de la VM cible

Compilation séparant les sources des fichiers compilés
$ javac -sourcepath src -source 1.7 \
-d classes -classpath classes \
-g src/MonApplication.java

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

$ java [-options] class [args...]
$ java [-options] -jar jarfile [args...]
class

est ici le nom de la classe (le .class doit pouvoir être trouvé dans le CLASSPATH)

-cp|-classpath

fixe le chemin de recherche des classes compilées

-jar

exécute un programme encapsulé dans un fichier jar

Exécution}
$ java -cp classes MonApplication

1.4.9. Classpath

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

  • Le compilateur ou 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.4.10. Constructions de base du langage Java

  • Java différencie majuscules et minuscules

  • Java possède une syntaxe proche du C

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

    • chaque instruction se termine par un ;

  • Commentaires

    /* …​ */

    le texte entre / et / est ignoré

    // …​

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

1.4.11. Types primitifs

Un type primitif est un type de base du langage, i.e. non défini par l’utilisateur. En Java, les valeurs de ces types ne sont pas des objets.

boolean

true ou false

byte

entier de -128 à 127 (les types entiers sont signés)

short

entier de -32768 à 32767

int

entier de -231 à 231 - 1

long

entier de -263 à 263 - 1

char

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

float

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

double

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

1.4.12. Littéraux

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

Entiers

123 de type int, 123L de type long, 0x123 en hexadécimal, 0b101 en binaire

Flottants

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

Booléens

true ou false

Caractères

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

Chaînes

"texte"

Null

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

1.4.13. Exemple de déclarations et initialisations de variables

// Exemples de déclaration avec initialisation
// (pas indispensable mais conseillé)

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 aConst = 0;       // Une constante

1.4.14. Références 1/2

  • 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

1.4.16. Référence vs. pointeur

  • Dans les deux cas, ce sont des variables (ou des constantes) dont la valeur (le contenu) est une adresse mémoire

  • Un pointeur est un concept de bas-niveau permettant une manipulation précise de l’adresse (arithmétique des pointeurs, pointeur de fonction, …​)

  • Une référence est une abstraction de plus haut niveau qui fournit une interface plus simple mais plus limitée pour manipuler l’adresse

1.4.17. 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 object, 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, …​)

1.4.18. 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)

  • La taille d’un tableau ne varie pas par la suite

  • Un tableau peut contenir des références

    • tableau d’objets ou tableau de tableaux

    • permet de créer des tableaux à plusieurs dimensions

1.4.19. Déclaration et création de tableaux

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

int[] unTableau;
  • La création du tableau se fait en utilisant l’opérateur new suivi du type des éléments du tableau et de sa taille entre []

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

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

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

1.4.20. Manipulation de tableaux

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

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

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

1.4.21. Tableaux et références 1/6

int[] unTableau = new int[10];
tabref1

1.4.22. Tableaux et références 2/6

int[] unTableau = new int[10];
int[] leMemeTableau = unTableau;
tabref2

1.4.23. Tableaux et références 3/6

int[] unTableau = new int[10];
int[] leMemeTableau = unTableau;
leMemeTableau[0] = 12;
tabref3

1.4.24. Tableaux et références 4/6

int[] unTableau = new int[10];
int[] leMemeTableau = unTableau;
leMemeTableau[0] = 12;
int[] unAutreTableau = new int[5];
tabref4

1.4.25. Tableaux et références 5/6

int[] unTableau = new int[10];
int[] leMemeTableau = unTableau;
leMemeTableau[0] = 12;
int[] unAutreTableau = new int[5];
leMemeTableau = unAutreTableau;
tabref5

1.4.26. Tableaux et références 6/6

int[] unTableau = new int[10];
int[] leMemeTableau = unTableau;
leMemeTableau[0] = 12;
int[] unAutreTableau = new int[5];
leMemeTableau = unAutreTableau;
leMemeTableau[0] = 21;
tabref6

1.5. Python RefCard

1.5.1. Le langage Python

  • Créé en 1990 par Guido Van Rossum

  • Caractéristiques

    • compilé/interprété

    • langage de scripts

    • système de typage implicite, dynamique, fort

    • support de la programmation procédurale, OO, fonctionnelle (partiel)

En 2017, deux versions incompatibles de Python co-existent (2.7 et 3.6)

1.5.2. Quelques domaines d’application

  • Langage de scripts

  • Programmation scientifique

    • analyse de données, bioinformatique (analyse du génôme)

  • Développement web

    • plusieurs frameworks populaires

  • Développement d’outils pour le développement logiciel

  • Enseignement de l’informatique

1.5.3. Implémentations

  • CPython est l’implémentation de référence

  • Principales implémentations alternatives

1.5.5. Quelques outils de développement

1.5.6. Lancer un interpréteur interactif

  • Lancer le REPL (sous Linux)

$ python3
Python 3.5.3rc1 (default, Jan  3 2017, 04:40:57)
[GCC 6.3.0 20161229] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> quit()
$
  • Lancer IPython (sous Linux)

$ ipython3
Python 3.5.3rc1 (default, Jan  3 2017, 04:40:57)
Type "copyright", "credits" or "license" for more information.

IPython 5.1.0 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object', use 'object??' for extra details.

In [1]: quit
$

1.5.7. Lancer un notebook Jupiter

$ jupyter-notebook
[I 09:55:27.102 NotebookApp] Writing notebook server cookie secret to /run/user/1000/jupyter/notebook_cookie_secret
[W 09:55:27.136 NotebookApp] Widgets are unavailable. On Debian, notebook support for widgets is provided by the package jupyter-nbextension-jupyter-js-widgets
[I 09:55:27.151 NotebookApp] Serving notebooks from local directory: /home/hal
[I 09:55:27.151 NotebookApp] 0 active kernels
[I 09:55:27.151 NotebookApp] The Jupyter Notebook is running at: http://localhost:8888/
[I 09:55:27.151 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
  • Arrêt en tapant deux fois Ctrl+C

1.5.8. Exemple de script Python

Fichier intro/first_script.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""Ce module est un exemple simple de script Python.

    Un commentaire débute par # et se termine en fin de ligne.
    Un script débute par un shebang (optionnel).
    Le shebang est inutile dans le cas d'un module non exécuté directement
    La seconde ligne de commentaire précise l'encodage du fichier (optionnel).
    On trouve ensuite la docstring du module qui documente le module lui-même (optionnel).
"""

print(
    """
        Ce code est exécuté dans tous les cas.
    """)

if __name__ == '__main__':
    # L'indentation définit un bloc de code imbriqué
    print(
        """
            Ce bloc de code est exécuté uniquement quand le module est invoqué
            directement par l'interpréteur, i.e. pas importé
        """)

1.5.9. Exécuter un programme Python

  • En appelant l’interpréteur

$ python3 intro/first_script.py

        Ce code est exécuté dans tous les cas.


            Ce bloc de code est exécuté uniquement quand le module est invoqué
            directement par l'interpréteur, i.e. pas importé
  • Directement par le shell (utilise le shebang)

$ chmod +x intro/first_script.py
$ intro/first_script.py

1.5.10. Généralités

  • Commentaire introduit par #

  • Différence entre minuscules et majuscules

  • L’indentation définit l’imbrication des blocs

  • Pas de constante

    • variable qu’on ne modifie pas

    • par convention, nommée en majuscule et avec des _

  • Toutes les données sont représentées par des objets

1.5.11. Principaux types prédéfinis 1/2

  • numbers.Number représente les nombres (immuables)

    • numbers.Integral pour les entiers

      • int nombre entier sans limite de taille

      • bool possède les valeurs True et False

    • float (numbers.Real) nombre en virgule flottante double précision

    • complex (numbers.Complex) nombre complexe représenté comme une paire de float

  • None possède une seule valeur None

  • cf. The standard type hierarchy

Les exemples de cette section sont disponibles dans le notebook intro/Python RefCard.ipynb

1.5.12. Principaux types prédéfinis 2/2

  • Une séquence représente une collection ordonnée (indexée par des entiers)

    • une séquence immuable ne change pas après sa création

      • une chaîne de caractère est une séquence de caractères unicodes

      • un tuple représente un n-uplet d’objets quelconques

    • une séquence mutable supporte les modifications

      • une liste est formée d’éléments quelconques

  • Un ensemble représente une collection non ordonnée d’éléments uniques

    • un ensemble est modifiable

  • Un dictionnaire représente une collection modifiable de couples (clé, valeur)

1.5.13. Littéraux

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

  • int : 1234, 0b11001 en binaire, 0o177 en octal, 0xAFF en héxa, _ est permis (v 3.6)

  • float : 3.14, 10., .001, 1e100, 3.14e-10

  • Imaginaire : littéral float suffixé par j construit un complexe de partie réelle nulle (3+4j pour une partie réelle non nulle)

  • Chaîne : "chaîne", 'chaîne', """chaîne multi-lignes""", '''chaîne multi-lignes''', préfixée par r pour éviter l’interprétation, par f pour une chaîne formatée (v 3.6)

1.5.14. Liaison des variables et référence

  • Les variables 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

    • dans les deux cas, ce sont des variables dont la valeur (le contenu) est une adresse mémoire

  • Une référence est une abstraction de plus haut niveau

    • fournit une interface plus simple pour manipuler l’adresse

    • ne permet pas de manipuler directement l’adresse mémoire

    • un pointeur est un concept de bas-niveau permettant une manipulation directe de l’adresse (arithmétique des pointeurs, pointeur de fonction, …​)

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

1.5.15. Structures de contrôle

  • instruction pass

  • if i < 10:, elif i > 100:, else:

  • while i < 10:

  • for idx in seq:, for i in range(0, 10, 3):

  • break, continue, else: pour une boucle

Il faut bien respecter l’indentation.

1.5.16. Quelques opérateurs spécifiques

  • / représente la division en virgule flottante

  • // représente la division entière

  • ** représente l’élévation à la puissance

  • a if condition else b est une expression conditionnelle

  • pas d’opérateur de conversion de types

1.5.17. Fonction

  • Lors de l’appel, les paramètres formels sont liés aux arguments

  • Un appel de fonction retourne toujours une valeur (éventuellement None)

  • Les fonctions peuvent être imbriquées

def fib(n):
    """Print a Fibonacci series up to n."""
    a, b = 0, 1
    while a < n:
        print(a, end=' ')
        a, b = b, a+b
    print()

fib(2000) # Appel de la fonction

1.5.18. Exemple de manipulation de chaînes

  • Une chaîne est une instance immuable de la classe str

  • Le module string complète les possibilités de manipulation de chaînes

word = 3 * 'un' + 'ium' # 'unununium'
word[0]   # character in position 0
word[-1]  # last character
word[2:5] # characters from position 2 (included) to 5 (excluded)
word[:2]  # character from the beginning to position 2 (excluded)
word[4:]  # characters from position 4 (included) to the end
len(word) # size of the string

1.5.19. Exemple de manipulation de tuples

  • Un tuple supporte les opérations sur les séquences

  • Un tuple est une instance immuable de la classe tuple

t = () # tuple vide
t = 12345, # singleton
t = 12345, 54321, 'hello!'
t[0] # premier élément

1.5.20. Exemple de manipulation de listes

  • Une liste supporte les opérations sur les séquences

  • Une liste est une instance de la classe list

squares = [1, 4, 9, 16, 25]
squares = [x**2 for x in range(10)] # liste en compréhension
del squares[0] # supprime le premier élément
cubes = [1, 8, 27, 65, 125]
cubes[3] = 64 # modifiable
del cubes # supprime la variable

1.5.21. Exemple de manipulation d’ensembles

  • Un ensemble est une instance de la classe set

basket = set() # ensemble vide
basket = {'apple', 'orange', 'apple', 'pear', 'orange', 'banana'}
'orange' in basket # test d'appartenance
a = set('abracadabra') # les doublons sont éliminés
b = set('alacazam')
a - b # différence
a | b # union
a & b # intersection
a ^ b # a union b privé de a inter b
a = {x for x in 'abracadabra' if x not in 'abc'} # ensemble en compréhension

1.5.22. Exemple de manipulation de dictionnaires

  • Un dictionnaire est une instance de la classe dict

tel = {'jack': 4098, 'sape': 4139}
tel['guido'] = 4127
del tel['sape']

Références (POO/Java)

1.6. Exercices (Python)

1.6.1. Installation de l’environnement

Vérification de l’installation Python
Création d’un compte Bitbucket

Les plateformes Bitbucket (et Github) permettent d’héberger des projets en utilisant un système de gestion de versions (git).

  1. Créez-vous un compte sur Bitbucket.

  2. Faites un fork du dépôt https://bitbucket.org/hal_prism/cours.poo.exemples.python.git dans votre espace Bitbucket.

  3. Cloner localement votre nouveau dépôt.

Création d’un compte SageMathCloud (optionnel)

La plateforme SageMathCloud offre un ensemble d’outils pour le calcul scientifique. Un compte gratuit donne accès à une machine virtuelle sur laquelle sont installés de nombreux logiciels de calcul scientifique ainsi qu’à une interface graphique dans le navigateur permettant d’ouvrir un notebook, un terminal…​

  1. Créez-vous un compte sur SageMathCloud.

  2. Créez un projet.

  3. Ajoutez un terminal dans votre projet pour reproduir les instructions des sections Lancer un interpréteur interactif et Lancer un notebook Jupyter.

1.6.2. Premiers pas en Python

Dans cette section, vous utiliserez comme base le répertoire intro des exemples du cours. N’hésitez pas à faire des modifications dans les exemples proposés.

  1. Exécutez le script de la section Exemple de script Python en suivant les instructions de la section Exécuter un programme Python.

  2. Dans un notebook, ouvrez (ou reproduisez) et exécutez le fichier Python RefCard.ipynb qui reprend les exemples du cours.

  3. Répondez aux questions suivantes

    1. Dans la documentation du langage, où se trouve la documentation de la fonction print (pour Python 3.6, pour Python 2.7) ? Même question pour l’instruction print de Python 2.7.

    2. Où se trouve la documentation de l’instruction assert ?

    3. Quelle convention utilise-t-on pour nommer les fonctions en Python ?

    4. En dehors de la classe str, quels modules de la bibliothèque standard permettent de manipuler du texte ?

    5. Dans l’index PyPI, combien de bibliothèques sont liées au thème Games/Entertainment pour la version 3.6 de Python ?

    6. En Python 3, quelle est la différence entre les opérateurs / et // ?

    7. En utilisant les listes en compréhension, générer les tables de multiplication jusqu’à 10.

2. Vue d’ensemble des concepts objet

2.1. Objet et message

2.1.1. Système orienté objet

  • Lors de son exécution, un système OO est un ensemble d’objets qui interagissent

  • Les objets forment donc l'aspect dynamique (à l’exécution) d’un système OO

  • Ces objets représentent soit

    • des entités du monde réel (⇒ ce sont donc des modèles d’entités réelles), soit

    • des objets "techniques" nécessaires durant l’exécution du programme.

2.1.2. 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

  • Un objet peut avoir plusieurs types, i.e. supporter plusieurs interfaces

2.1.3. Exemple : des points et des cercles

points and circles
Figure 7. Diagramme d’objets UML
  • Les objets point1 et point2 sont des points, cercle1 est un cercle

  • L’état de chaque objet est représenté par la valeur de ses propriétés

  • Le centre du cercle est une référence sur un objet point

  • Le comportement n’est pas représenté au niveau des objets

    • une opération est invoquée par rapport à un objet

    • mais est rattachée à la classe (le code est partagé par tous les objets d’une classe)

  • Les objets point1 et point2 sont égaux mais pas identiques

2.1.4. Exemple : des points et des cercles (Java)

Point2D p1 = new Point2D(1.0, 2.0);
Point2D p2 = new Point2D(1.0);
Point2D p3 = new Point2D();
Point2D unAutreP3 = p3;
assert p3 == unAutreP3; // 2 points identiques

Cercle2D c1 = new Cercle2D(p1, 3.0);
Cercle2D c2 = new Cercle2D(new Point2D(2.0, 4.0), 5.0);
Cercle2D c3 = new Cercle2D();

2.1.5. Exemple : des points et des cercles (Python)

point1 = point2d.Point2D(1.0, 2.0)
point2 = point2d.Point2D(1.0, 2.0)
cercle1 = cercle2d.Cercle2D(point1, 2.0)

2.1.6. Communication par messages

  • Un objet solitaire n’a que peu d’intérêt ⇒ différents objets doivent pouvoir interagir

  • Un message est un moyen de communication (d’interaction) entre objets

  • Les messages sont les seuls moyens d’interaction entre objets

    • ⇒ l’état interne ne doit pas être manipulé directement

  • Le (ou les) type(s) d’un objet détermine les messages qu’il peut recevoir

2.1.7. Message

  • Un message est une requête envoyée à un objet pour demander l’exécution d’une opération

  • Un message comporte trois composants:

    • l’objet auquel il est envoyé (le destinataire du message),

    • le nom de l’opération à invoquer,

    • les paramètres effectifs.

2.1.8. Exemple : déplacer un cercle

moving circle
Figure 8. Diagramme de séquence UML
  • L’utilisateur envoie un message à un objet (à une instance de) cercle

  • Le message se traduit par l’exécution de l’opération translate(1.0, 2.0) par l’objet cercle

  • Durant cette exécution, le cercle envoie un message à l’objet point (translater le cercle revient à translater son centre)

2.1.9. Exemple : déplacer un cercle (Java)

  • Le message translate est envoyé à cercle1 (cercle1.translate(1.0, 2.0))

    • lors de cette exécution, le message translate est envoyé au centre du cercle (centre.translate(1.0, 2.0))

      • l’opération translate du point est exécutée

2.1.10. Exemple : déplacer un cercle (Python)

cercle1.translate(2.0, 3.0)
  • Le message translate est envoyé à cercle1 (cercle1.translate(1.0, 2.0))

    • lors de cette exécution , le message translate est envoyé au centre du cercle (self.centre.translate(1.0, 2.0))

      • l’opération translate du point est exécutée

2.2. Type et classe

2.2.1. Type

  • Un type est un modèle abstrait réunissant à un haut degré les traits essentiels de tous les êtres ou de tous les objets de même nature

  • En informatique, 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

2.2.3. Exemple : un type Déplaçable (Java)

  • En Java, un type peut être représenté par une interface.

package fr.uvsq.info.poo.coo;

public interface Deplacable {
    /**
     * Translate l'objet.
     *
     * @param dx deplacement en abscisse
     * @param dy deplacement en ordonnées
     */
    void translate(double dx, double dy);
}

2.2.4. Type (Python)

  • En Python, tout objet acceptant les messages déclarés dans un type est de ce type

  • Cette propriété se nomme duck typing

  • Elle est commune à plusieurs langages à typage dynamique

2.2.5. Classe I

  • Une classe est un "modèle" (un "moule") pour une catégorie d’objets structurellement identiques

  • Une classe définit donc l’implémentation d’un objet (son état interne et le codage de ses opérations)

  • L’ensemble des classes décrit l'aspect statique d’un système OO

2.2.6. Classe II

  • Une classe comporte:

    • la définition des attributs (ou variables d’instance),

    • la signature des opérations (ou méthodes),

    • la réalisation (ou définition) des méthodes.

  • Chaque instance aura sa propre copie des attributs (son état)

  • La signature d’une opération englobe son nom et le type de ses paramètres

  • L’ensemble des signatures de méthodes représente l’interface de la classe (publique)

  • L’ensemble des définitions d’attributs et de méthodes forme l’implémentation de la classe (privé)

2.2.7. Exemple : les classes Cercle2D et Point2D

classe point cercle
Figure 9. Diagramme de classes UML
  • Un rectangle représente une classe

    • 1er pavé: nom de la classe

    • 2ième pavé: attributs

    • 3ième pavé: signature des méthodes

  • en général, les attributs sont privés et les méthodes publiques

2.2.9. Exemple : la classe Cercle2D (Java)

package fr.uvsq.info.poo.coo;

public class Cercle2D implements Deplacable {
    /** Le centre du cercle. */
    private Point2D centre;

    /** Le rayon du cercle */
    private double rayon;

    /**
     * Initialise un cercle avec un centre et un rayon.
     * @param centre Le centre
     * @param rayon Le rayon
     */
    public Cercle2D(Point2D centre, double rayon) { /* ... */ }

    /**
     * Initialise un cercle centré à l'origine et de rayon 1.
     */
    public Cercle2D() { /* ... */ }

    public Point2D getCentre() { return centre; }
    public double getRayon() { return rayon; }

    /**
     * Translate le cercle.
     * @param dx déplacement en abscisse.
     * @param dy déplacement en ordonnées.
     */
    public void translate(double dx, double dy) { /* ... */ }
}

2.2.10. Exemple : la classe Cercle2D (Python)

"""Définition de la classe Cercle2D"""
from coo.forme import Forme


class Cercle2D(Forme):
    """Représente un cercle en 2 dimensions"""

    def __init__(self, centre, rayon):
        """Initialise un cercle à partir d'un point et d'un rayon"""
        self.centre = centre
        self.rayon = rayon

    def __str__(self):
        return "Cercle2D({}, {})".format(self.centre, self.rayon)

    def translate(self, dx, dy):
        """Déplace le cercle"""
        self.centre.translate(dx, dy)

2.2.11. Classe et type

  • Une classe implémente un ou plusieurs types, i.e. respecte une ou plusieurs interfaces

  • Un objet peut avoir plusieurs types mais est une instance d’une seule classe

  • Des objets de classes différentes peuvent avoir le même type

2.2.13. Instanciation d’une classe

  • Le mécanisme d'instanciation permet de créer des objets à partir d’une classe

  • Chaque objet est une instance d’une classe

  • Lors de l’instanciation,

    • de la mémoire est allouée pour l’objet,

    • l’objet est initialisé (appel du constructeur) afin de respecter l’invariant de la classe.

2.2.14. Application : modélisation d’un robot

On veut modéliser des robots se déplaçant sur un terrain. Ce terrain est découpé en cases carrées repérées par deux coordonnées. Chaque case peut être vide ou contenir un mur ou un robot. Les robots sont très rudimentaires et ne disposent que d’une boussole. Ils ne connaissent donc que leur orientation (Nord, Est, Sud, Ouest). Un robot doit pouvoir avancer d’une case et tourner d’un quart de tour à droite. Un robot ne peut se déplacer d’une case à une autre que si la case de destination est vide.

  1. Représenter en UML la classe Robot

  2. Faire de même avec la classe Terrain

  3. Représenter sur un diagramme de séquence les échanges de messages pour le déplacement d’un robot

On suppose maintenant qu’un robot peut en détecter un autre qui passe devant lui. Par exemple, quand un robot se déplace, il peut passer dans le champ de vision d’un autre. Ce dernier devra alors être averti.

  1. Modifier le diagramme précédent pour y intégrer la détection des déplacements

2.3. Héritage

2.3.1. Sous-type

Un type T1 est un sous-type d’un type T2 si l’interface de T1 contient l’interface 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.

  • Un sous-type possède une interface plus riche, i.e. au moins toutes les opérations du super-type

  • De manière équivalente, l’extension du super-type contient l’extension du sous-type, i.e. tout objet du sous-type est aussi instance du super-type

2.3.3. Héritage

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

  • Ce mécanisme permet, lors de la définition d’une nouvelle classe, de ne préciser que ce qui change par rapport à une classe existante

  • Une hiérarchie de classes permet de gérer la complexité, en ordonnant les classes au sein d’arborescences d’abstraction croissante

  • Si Y hérite de X, on dit que

    • Y est une classe fille (sous-classe, classe dérivée) et que

    • X est une classe mère (super-classe, classe de base)

2.3.5. Exemple : la classe Rectangle2DPlein (Java)

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

    /**
     * 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 supGauche,
                            Point2D infDroit,
                            Color couleur) {
        super(supGauche, infDroit);
        assert couleur != null;
        this.couleur = couleur;
    }

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

2.3.6. Exemple : utilisation de la classe Rectangle2DPlein (Java)

// Déclaration et création d'un rectangle rouge
Rectangle2DPlein rp = new Rectangle2DPlein(new Point2D(1.0, 2.0),
        new Point2D(3.0, 0.0),
        Color.RED);
assert rp.getCouleur() == Color.RED;

// Déclaration d'un rectangle et
// liaison avec un rectangle plein
Rectangle2D r = new Rectangle2DPlein(new Point2D(1.0, 2.0),
        new Point2D(2.0, 1.0),
        Color.YELLOW);
assert r.getLargeur() == 1;
  • L’affectation d’une instance de Rectangle2DPlein à une références sur un Rectangle2D est autorisée

  • À partir de r1, l’accès à getCouleur est impossible (échoue à la compilation) car getCouleur ne fait pas parti de l’interface de Rectangle2D

2.3.7. Exemple : la classe Rectangle2DPlein (Python)

"""Définition de la classe Rectangle2DPlein"""

from coo.rectangle2d import Rectangle2D


class Rectangle2DPlein(Rectangle2D):
    def __init__(self, orig, tailleX, tailleY, couleur):
        """Initialise un rectangle à partir d'un point, d'une taille et d'une couleur"""
        Rectangle2D.__init__(self, orig, tailleX, tailleY)
        self.couleur = couleur

    def __str__(self):
        return "Rectangle2DPlein({}, {}, {})".format(self.orig, self.fin, self.couleur)

    def colorie(self, couleur):
        """Modifie la couleur du rectangle"""
        self.couleur = couleur
  • La super-classe est indiqué entre parenthèse après le nom de la classe

  • Le constructeur de la super-classe est appelé dans le constructeur

2.3.8. 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

2.3.10. 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)

2.3.12. Exemple : la description dans Rectangle2D (Java)

/**
 * 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());
}

2.3.13. Exemple : la description dans Rectangle2DPlein (Java)

/**
 * Retourn une chaîne représentant l'objet.
 *
 * @return la chaîne.
 */
@Override
public String toString() {
    return String.format("%s, couleur : %s", super.getDesc(), couleur);
}

2.3.14. Exemple : la description dans Rectangle2D (Python)

"""Définition de la classe Rectangle2D"""

from coo.forme import Forme
from coo.point2d import Point2D

class Rectangle2D(Forme):
    def __init__(self, orig, tailleX, tailleY):
        """Initialise un rectangle à partir d'un point et d'une taille en X et Y"""
        self.orig = orig
        self.fin = Point2D(orig.x + tailleX, orig.y + tailleY)

    def __str__(self):
        return "Rectangle2D({}, {})".format(self.orig, self.fin)

    def translate(self, dx, dy):
        """Déplace le rectangle de dx en abscisse et de dy en ordonnée"""
        self.orig.translate(dx, dy)
        self.fin.translate(dx, dy)
  • La méthode str fournit la description

2.3.15. Exemple : la description dans Rectangle2DPlein (Python)

"""Définition de la classe Rectangle2DPlein"""

from coo.rectangle2d import Rectangle2D


class Rectangle2DPlein(Rectangle2D):
    def __init__(self, orig, tailleX, tailleY, couleur):
        """Initialise un rectangle à partir d'un point, d'une taille et d'une couleur"""
        Rectangle2D.__init__(self, orig, tailleX, tailleY)
        self.couleur = couleur

    def __str__(self):
        return "Rectangle2DPlein({}, {}, {})".format(self.orig, self.fin, self.couleur)

    def colorie(self, couleur):
        """Modifie la couleur du rectangle"""
        self.couleur = couleur
  • La méthode str est redéfinie dans Rectangle2DPlein

2.3.16. 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

2.3.18. Exemple : une classe abstraite (Java)

/**
 * Une figure fermée.
 *
 * @version  jan. 2017
 * @author   Stéphane Lopes
 * 
 */
abstract class FigureFermee2D {
    /**
     * Translate la figure.
     * @param dx déplacement en abscisse.
     * @param dy déplacement en ordonnée.
     */
    public abstract void translate(double dx, double dy);
}

2.3.19. Exemple : redéfinition d’une méthode (Java)

Translation dans la classe Rectangle2D
/**
 * Translate le rectangle.
 * @param dx déplacement en abscisse.
 * @param dy déplacement en ordonnées.
 */
@Override
public void translate(double dx, double dy) {
    orig.add(dx, dy);
    fin.add(dx, dy);
}
Translation dans la classe Cercle2D
/**
 * Translate le cercle.
 *
 * @param dx déplacement en abscisse.
 * @param dy déplacement en ordonnées.
 */
@Override
public void translate(double dx, double dy) {
    centre.add(dx, dy);
}

2.3.20. Exemple : utilisation d’une classe abstraite (Java)

// Création du tableau de références
final int NB_FIGURES = 4;
FigureFermee2D[] figures = new FigureFermee2D[NB_FIGURES];

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

// Réalise une translation de la figure
for (int i = 0; i < figures.length; ++i) {
    figures[i].translate(1.0, 2.0);
}

2.3.21. Exemple : la classe abstraite Forme (Python)

"""Classe de base pour les formes"""

from abc import ABC, abstractmethod

class Forme(ABC):
    @abstractmethod
    def translate(self, dx, dy):
        """Déplace la forme d'un décalage en x et en y"""
        pass

2.3.22. Héritage multiple et à répétition

  • Un héritage multiple se produit lorsqu’une classe possède plusieurs super-classes

  • Un héritage à répétition se produit lorsqu’une classe hérite plusieurs fois d’une même super-classe

  • Ces types d’héritage peuvent provoquer des conflits aux niveaux des attributs et méthodes

    • deux classes de base peuvent posséder la même méthode,

    • un attribut peut être hérité selon plusieurs chemins dans le graphe d’héritage.

2.3.23. Exemple : une hiérarchie pour les véhicules

heritage multiple
  • Combien de numéros d’immatriculation possède la voiture amphibie ?

  • Quelle opération est invoquée quand une voiture amphibie reçoit le message avance ?

2.3.24. Exemple : la hiérarchie de véhicules (Python)

#!/usr/bin/env python3

"""Exemple d'héritage multiple"""

class Vehicule(object):
    def __init__(self, immat):
        self.immat = immat

    def avance(self, distance):
        print("avance dans Vehicule")

class Bateau(Vehicule):
    def __init__(self, immat):
        Vehicule.__init__(self, immat)

    def avance(self, distance):
        print("avance dans Bateau")

class Voiture(Vehicule):
    def __init__(self, immat):
        Vehicule.__init__(self, immat)

    def avance(self, distance):
        print("avance dans Voiture")

class VoitureAmphibie(Bateau, Vehicule):
    pass

if __name__ == '__main__':
    va = VoitureAmphibie("VA123")
    va.avance(12)
    print(va.immat)

2.4. Relations entre classes

2.4.1. Types de relations entre classes

Dépendance
  • liaison limitée dans le temps entre objets (non structurelle)

Association
  • relation structurelle

  • cas particuliers : Agrégation, Composition

Spécialisation/généralisation
  • relation d’héritage ou de sous-typage

  • cas particulier : Réalisation (relation entre une classe et un type)

2.4.2. Relation de dépendance

  • Un élément A dépend d’un élément B si A utilise les services de B, i.e. son interface

  • Un changement dans l’interface de B peut donc avoir des répercussions sur A

    • par contre, son implémentation peut être modifiée sans affecter A

  • Une objet local à une méthode ou un paramètre de méthode sont des exemples de ce type de relation

2.4.3. Association

  • Une association représente une connexion sémantique bidirectionnelle entre une (association réflexive) ou plusieurs classes

    • les participants de l’association possèdent des références des uns vers les autres

    • est donc structurelle et permanente

  • L'arité d’une association représente le nombre de participants à l’association (association binaire, ternaire, n-aire)

2.4.5. Exemple : "Apparaît Dans" (Java)

class ApparaitDans {
  private Cercle figure;
  private Dessin dessin;
  private int nbOccurences;

  public ApparaitDans(Cercle figure, Dessin dessin, int nbOccurences) {
    this.figure = figure;
    this.dessin = dessin
    this.nbOccurences = nbOccurences;
  }
  // ...
}

2.4.6. Exemple : "Apparaît Dans" (Python)

class Cercle:
    pass

class Dessin:
    pass

class ApparaitDans:
    def __init__(self, dessin, cercle, position):
        self.dessin = dessin
        self.cercle = cercle
        self.position = position

2.4.8. Exemple : une association ternaire (Java)

class Cours {
    private Etudiant etudiant;
    private Professeur professeur;
    private Salle salle;
    private int jour;
    private int heure;
    private int duree;
    // ...
}

2.4.9. Exemple : une association ternaire (Python)

class Cours:
    def __init__(self, etudiant, professeur, salle,
                 jour, heure, durée):
        self.etudiant = etudiant
        self.professeur = professeur
        self.salle = salle
        self.jour = jour
        self.heure = heure
        self.duree = duree

2.4.10. Agrégation

  • L'agrégation est une association non symétrique, qui exprime un couplage fort et une relation de subordination

    • représente une relation de type "ensemble/élément" (ou tout/partie) entre des classes

  • La classe représentant l’ensemble est parfois appelée agrégat

  • À un même moment, une instance d’élément agrégé peut être liée à plusieurs agrégats (l’élément agrégé peut être partagé)

    • une instance d’élément agrégé peut exister sans agrégat (et inversement)

    • les cycles de vies de l’agrégat et de ses éléments agrégés peuvent être indépendants

    • si on détruit une agrégation, on ne détruit pas automatiquement ses composants

    • agrégation par "référence"

  • L’agrégat est en général responsable de la création/suppression du composant

    • ⇒ doit savoir comment créer le composant

2.4.12. Exemple : Figure et Cercle (Java)

class Figure {
  /** L'ensemble de cercles composants la figure. */
  private List<Cercle> elements;
  // ...
}

2.4.13. Exemple : Figure et Cercle (Python)

class Figure:
    def __init__(self, listeDeCercles):
        self.listeDeCercles = copy.copy(listeDeCercles)

2.4.14. Composition

  • Une composition est une agrégation forte (agrégation par valeur)

  • À un même moment, une instance de composant ne peut être liée qu’à un seul agrégat

    • les cycles de vies des éléments (les "composants") et de l’agrégat sont liés

    • si l’agrégat est détruit (ou copié), ses composants le sont aussi

Le choix de modélisation entre agrégation et composition est souvent subjectif

2.4.16. Exemple : le centre d’un Cercle (Java)

class Cercle {
  /** Le centre du cercle. */
  private Point centre;

  public Cercle(final Point centre, final double rayon) {
    this.centre = centre.clone();
    // ...
  }
  // ...
}

2.4.17. Exemple : le centre d’un Cercle (Python)

class Cercle:
    def __init__(self, centre, rayon):
        self.centre = copy.deepcopy(centre)
        self.rayon = rayon

2.4.18. Spécialisation/Généralisation

  • Une classe fille hérite de l’ensemble des caractéristiques des super-classes

    • tout changement dans la classe mère peut entraîner des changements dans la classe fille

  • Hériter d’un type limite l’impact des changements

    • seule l’interface est réutilisée

  • Contrôler la visibilité des membres d’une classe permet également de limiter l’impact des changements

2.4.19. Relations et dépendances

  • Les relations entre classes peuvent être ordonnées par rapport aux dépendances qu’elles génèrent

types dependencies

Source : Dependency (blog)

2.4.20. Dépendances et changements

  • Quand une dépendance existe entre deux éléments, un changement de l’un affecte le second

  • Le couplage est une mesure de la probabilité qu’un changement d’un élément entraîne une modification de l’autre

  • En développement logiciel, les dépendances entre élément doivent être minimisées

    • pour construire des composantes faiblement couplés

    • le changement d’un composant aura moins d’impact sur le reste du code

2.5. Module

2.5.1. Module

  • Un module (ou package) 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é

2.5.2. Utilité d’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

2.5.3. Qualité d’un module

  • 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

2.5.4. Exemple : une architecture multi-couches

package diagram
Figure 10. Diagramme de packages UML

2.5.5. Package (Java)

  • Le concept de module est implémenté par les packages

  • Le mot clé package placé en début de fichier permet l’ajout d’éléments dans un module

  • Le mot clé import permet l’accès aux éléments d’un module

2.5.6. Module (Python)

  • Un module est un fichier source contenant des définitions et des instructions Python

    • le fichier source doit être nommé comme le module et avec l’extension .py

  • À l’intérieur d’un module, son nom est accessible par la variable globale name

    • un module exécuté comme un script possède le nom main

  • Un module peut ensuite être importé dans un autre

  • Les modules sont recherchés par l’interpréteur dans les répertoires de la variable sys.path

2.5.7. Package (Python)

  • Un package regroupe et structure un ensemble de modules

  • Chaque répertoire d’un package doit contenir un fichier init.py (même vide)

  • Un module d’un package est référencé en séparant par un . les noms des éléments

    • par exemple, sound.effects.echo fait référence au module echo du sous-package effects du package echo

2.6. Exercices (Python)

Dans cette section, la bibliothèque pygame est utilisée pour l’affichage graphique (documentation de l’API).

2.6.1. Une application graphique simple

  1. Créez un script simple_app.py dans le répertoire gui.

  2. Importez la classe PygameApplication du module pgutil.pygame_application.

  3. Créez une instance de la classe PygameApplication

  4. Explorez le contenu de la classe

    1. quelles sont les grandes étapes de l’éxécution ?

    2. quel attribut encapsule la logique de l’affichage ?

  5. Définissez une sous-classe de PygameScene.

    1. quelles méthodes propose cette class ?

    2. quand sont-elles invoquées ?

    3. redéfinissez la méthode draw pour afficher un cercle au centre de la fenêtre.

2.6.2. Gérer la souris

  1. Copiez le script précédent sous le nom simple_app_with_mouse.py.

  2. Affichez maintenant un nouveau cercle à l’endroit où l’utilisateur clique.

  3. Même question mais le cercle doit cette fois être déplacé, i.e. un seul cercle est présent à l’écran.

2.6.3. Échapper aux robots

Le but de cet exercice est de développer un jeu simple où un joueur, Tux, doit échapper à des robots. Ce jeu fait appel à la notion de Sprite (tutoriel).

  1. Selon le même modèle que précédement, créez une application robot_escape.py utilisant une scène robot_escape_scene.py.

  2. Définissez la classe RobotSprite comme sous-classe de la classe Sprite de pygame.

    1. dans le constructeur, charger l’image du robot et initialiser le sprite

    2. ajouter la méthode update qui permet le déplacement du robot

    3. intégrez le robot dans la scène

    4. vous pouvez définir plusieurs robots avec des déplacements différents.

  3. Définissez la classe PlayerSprite qui représente le joueur humain. Il doit pouvoir être déplacé à l’aide des touches fléchées.

  4. Ajoutez la détection de collision entre le joueur et le (ou les) robots. Par exemple, suite à une collision, vous pouvez faire redémarrer le jeu dans la situation initiale.

  5. Ajoutez un objectif (une zone identifiée sur le plateau) : le joueur gagne s’il atteint cet objectif

2.7. Exercices (Java)

  • Utilisation de l’environnement de développement BlueJ.

  • Les seules sources d’information externes autorisées sont

  • Exécution interactive uniquement avec BlueJ (pas de méthode main ni d’affichage).

2.7.1. Découverte de BlueJ

Dans cette exercice, vous apprendrez les manipulations de base de l’environnement BlueJ.

  1. Ouvrez le projet d’exemple people2 (Projects/Open Projet…​). Les projets d’exemples se trouvent dans le répertoire d’installation de BlueJ sous Windows ou dans /usr/share/doc/BlueJ sous Linux.

    1. Quelles classes y-a-t-il dans ce projet ?

    2. Quelles relations existe-t-il entre ces classes ?

  2. Compilez le projet (Bouton Compile)

    1. Quels logiciels faut-il pour compiler un projet en Java ? pour l’exécuter ?

  3. Création d’objet (menu contextuel d’une classe)

    1. Quels constructeurs sont disponibles pour chaque classe ?

    2. Pourquoi la classe Person n’en a-t-elle aucun ?

    3. Créez un employé (Staff) p1 ayant pour nom Smith, pour année de naissance 1980 et pour numéro de bureau D100.

  4. Invocation de méthode (menu contextuel d’un objet)

    1. Quelles méthodes propose p1 ?

    2. Exécutez les méthodes getRoom() et getName. Dans quelles classes sont-elles définies ?

    3. Exécutez la méthode setAddress .

  5. Évaluation à la volée de code Java (View/Show Code Pad). Les manipulations ci-dessous sont à réaliser dans le Code Pad.

    1. Créez un étudiant en précisant son nom, son année de naissance et son identifiant.

    2. Copiez-le (glisser-déposer) dans la fenêtre des objets.

    3. Exécutez la méthode getName dans le Code Pad.

  6. Inspection d’un objet (menu contextuel d’un objet)

    1. Inspectez l’objet p1 ?

    2. De quels attributs son état est-il formé ?

    3. Pouvez-vous inspecter tous ses attributs ? Pourquoi ?

  7. Éditez le code Java de la classe Staff (double-click sur la classe)

    1. Quels éléments du langage trouve-t-on dans cette classe ?

    2. À quoi correspondent les couleurs de fond ?

    3. Visualisez la documentation de la classe (liste déroulante en haut à droite).

    4. Quelle relation y-a-t-il entre la documentation et le code source ? Modifiez un commentaire dans le code et visualisez à nouveau la documentation ?

    5. Quel parallèle peut-on établir avec les fichiers .h et .c du langage C ?

2.7.2. Manipulation d’objets

Dans cette exercice, vous effectuerez les manipulations tout d’abord de façon interactive, puis en tapant les instructions dans le Code Pad.

  1. Ouvrez le projet d’exemple BlueJ shapes et compiler-le.

  2. Réalisez les manipulations suivantes

    1. Créez un cercle et rendez-le visible.

    2. Déplacez-le horizontalement de 100 unités.

    3. Changez sa couleur en rouge.

    4. Déplacez-le verticalement de 50 unités.

  3. Représentez la scène suivante :

    • une maison (carré bleu et triangle rouge),

    • un soleil jaune au-dessus et à droite de la maison.

2.7.3. Modification d’une classe

Cet exercice utilise le projet Entreprise (cf. figure ci-dessous) à ouvrir sous BlueJ (Project/Open Non Bluej…​).

entreprise
Figure 11. Diagramme de classe du projet Entreprise
  1. Modifiez la classe Employe comme suit:

    1. ajoutez l’attribut privé age,

    2. modifiez les méthodes pour prendre en compte ce changement,

    3. recompilez et testez ce changement.

  2. Mettez à jour les commentaires Javadoc et vérifier la documentation de la classe.

  3. Ajoutez la méthode toString à la classe Entreprise : cette méthode retourne une chaîne de caractères contenant le nom de l’entreprise et la liste de ses employés (prénom, nom et âge),

  4. recompiler les deux classes et tester les nouvelles méthodes.

2.7.4. Utilisation d’un débogueur

Cet exercice utilise le même projet que l’exercice précédent. Pour chaque test, vous créerez une entreprise et deux employés que l’entreprise embauchera puis vous invoquerez toString sur l’entreprise.

  1. Faites afficher le débogueur intégré à BlueJ (View/Show Debugger). Quels éléments sont visibles dans le débogueur ? Quelles actions peut-on effectuer à partir du débogueur ?

  2. Lancer un premier test et vérifier le comportement du programme.

  3. Lancer un second test en saisissant la valeur null lors de l’embauche du deuxième employé

    • Que se passe-t-il ?

    • Quelle instruction pose problème ?

  4. Placer un point d’arrêt (breakpoint) au début de la méthode toString de Entreprise (click dans la marge de l’éditeur).

  5. Reproduisez le second test:

    1. l’exécution doit s’arrêter au niveau du breakpoint,

    2. faites exécuter la méthode pas à pas en consultant la valeur des variables,

    3. quelle différence existe-t-il entre les commandes Step et Step Into du débogueur ?

    4. pour quel employé le problème se pose-t-il ?

    5. quelle est donc finalement la cause de l’erreur ?

  6. Modifiez la classe Entreprise pour éviter le problème.

  7. Vérifiez en reproduisant à nouveau le second test.

3. Objets et classes

3.1. Caractéristiques d’un objet

3.1.1. État d’un objet

  • L’état d’un objet est l’ensemble de ses caractéristiques internes, cachées aux autres objets

  • Les propriétés représentant l’état d’un objet particulier sont appelées variables d’instance ou attributs ou données membres

  • Chaque objet possède un état courant décrit par la valeur de ses attributs et donc sa propre copie des variables d’instance

  • Les attributs conservent leurs valeurs durant toute la durée de vie d’un objet

  • Les attributs peuvent être des objets

    • un objet peut donc être la racine d’une arborescence d’autres objets

3.1.2. Comportement d’un objet

  • Le comportement d’un objet regroupe les caractéristiques externes mises à la disposition des autres objets

  • Chaque opération est décrite par sa signature (son nom, les objets qu’elle prend comme paramètre et le type de retour)

  • L’ensemble de ces opérations forme l'interface de l’objet

  • Les opérations propres à un objet sont appelées méthodes d’instance ou fonctions membres (fonctions associées à l’objet)

  • Ces opérations sont invoquées par rapport à un objet particulier

    • une méthode sait quel objet l’a invoquée

    • donc, une méthode a accès aux attributs de l’objet qui l’a invoquée

    • comme une fonction avec un paramètre caché vers l’objet lui-même

  • Les opérations sont les seules moyens de manipuler l’état interne d’un objet (encapsulation)

3.1.3. Création d’objets

  • Un objet est créé (instancié) à partir d’une classe

    • certains (rares) langages créent de nouveaux objets par clonage d’un objet (prototype)

  • La création provoque la réservation de mémoire pour l’objet et l’invocation du constructeur qui initialise l’objet

    • le constructeur est une méthode invoquée automatiquement lors de la création de l’objet

    • son rôle est d’initialiser l’objet pour que ce dernier respecte l’invariant de la classe

  • L’opération de création retourne une référence sur l’objet

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

3.1.4. Identité et égalité

  • Un objet possède une identité (identifiant interne) qui caractérise son existence de façon indépendante de son état

    • tester l’identité de deux objets compare leurs identités

  • Deux objets sont égaux si leurs états sont les mêmes

    • tester l’égalité de deux objets compare leurs attributs

égalité ≠ identité

  • deux objets peuvent être égaux (même état interne) sans être identiques (même objet)

  • deux objets identiques sont forcément égaux

3.1.6. Affectation et copie d’objets

  • L'affectation d’un objet à une variable crée un lien entre la variable et l’objet

  • Créer une copie d’un objet nécessite de créer récursivement une copie de ses attributs (copie profonde ou deep copy)

  • Une autre sémantique possible est de ne copier que l’objet racine et de partager ses attributs (copie superficielle ou shallow copy)

  • La simple déclaration d’une variable ne crée pas d’objet mais uniquement une référence invalide

  • La déclaration crée un identificateur qui sera lié à l’objet en utilisant une affectation

3.2. Les objets en Java

3.2.1. Création d’objets

  • Un objet est créé à partir d’une classe en utilisant le mot-clé new (new Point2D(1.0, 2.0))

  • L’utilisation de new provoque la réservation de mémoire pour l’objet et l’invocation du constructeur qui initialise l’objet

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

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

3.2.2. Déclaration et affectation

  • La syntaxe pour la déclaration d’une variable est type nom (Point2D p1)

    • la déclaration ne crée pas d’objet mais uniquement une référence

    • la variable est invalide tant qu’elle n’est pas liée à un objet (null reference)

  • Une affectation va lier une variable à un objet variable = objet

  • Il est possible de lier la variable lors de sa déclaration (Point2D p1 = new Point2D(1.0, 2.0);)

  • Il est conseillé de toujours initialiser une variable lors de sa déclaration (si possible)

objets java

3.2.3. Exemple : instanciation de cercle et de points

Point2D p1 = new Point2D(1.0, 2.0);
Point2D p2 = new Point2D(1.0);
Point2D p3 = new Point2D();
Point2D unAutreP3 = p3;
assert p3 == unAutreP3; // 2 points identiques

Cercle2D c1 = new Cercle2D(p1, 3.0);
Cercle2D c2 = new Cercle2D(new Point2D(2.0, 4.0), 5.0);
Cercle2D c3 = new Cercle2D();

3.2.4. Manipulation

Accès aux attributs
  • Juste par le nom de l’attribut quand on se trouve dans la portée de l’attribut (exemple: x)

  • En qualifiant/préfixant avec le nom d’une référence sur l’objet à l’extérieur (exemple: p1.x)

  • La manipulation directe d’attributs en dehors de la classe est interdite (violation de l’encapsulation)

Invocation d’une méthode
  • Même syntaxe que pour les attributs mais avec la liste des paramètres (exemple: p1.getAbscisse())

3.2.5. 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 ramasse-miette (garbage collector)

  • Avant de détruire l’objet, le ramasse-miette invoque la méthode protected void finalize() de l’objet

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

    • finalize est membre de la classe Object

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

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

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

  • Java fournit trois classes pour les chaînes de caractères: String, StringBuffer et StringBuilder

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

  • StringBuilder est dédiée aux chaînes de caractères pouvant être modifiées (contexte mono-thread)

  • StringBuffer est dédiée aux chaînes de caractères pouvant être modifiées (contexte multi-threads)

3.3.2. Création d’une chaîne (String)

  • Une instance de String représente une chaîne au format UTF-16

  • Une chaîne est souvent créée à partir d’un littéral de type chaîne (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

3.3.3. Quelques accesseurs de String

length()

taille de la chaîne,

charAt(int)

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

substring(int, int)

extraction d’une sous-chaîne,

indexOf(…​), lastIndexOf(…​)

recherche dans la chaîne

3.3.4. Autres usages de String

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

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

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

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

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

3.3.5. Les classes StringBuilder et StringBuffer

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

  • Quelques mutateurs:

    append(…​)

    ajout de caractères

    delete(…​)

    suppression de caractères

    insert(…​)

    insertion de caractères

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

  • StringBuffer est à utiliser dans un contexte multi-threads

3.3.6. Exemple : Manipulation de chaînes de caractères

String source = "abcde";
int len = source.length();
StringBuilder dest = new StringBuilder(len);

for (int i = (len - 1); i >= 0; --i) {
    dest.append(source.charAt(i));
}

assert dest.toString().equals("edcba") : dest;

3.4. Caractéristiques d’une classe

3.4.1. Catégories de méthodes

Accesseur

permet de consulter l’état d’un objet

Mutateur

modifie l’état d’un objet (doit préserver l’invariant)

Constructeur

initialise un objet afin de le placer dans un état cohérent

Destructeur

libère les ressources allouées par l’objet

3.4.2. Surcharge de méthode

  • La surcharge (ou polymorphisme ad hoc) d’une méthode consiste à définir plusieurs méthodes de même nom mais ayant des signatures différentes

  • Une opération n’est donc plus simplement identifiée par son nom mais également par le nombre, l’ordre et le type de ses arguments

  • Le choix de la méthode est résolu lors de la compilation

  • La surcharge est souvent utilisée pour définir plusieurs constructeurs de façon à initialiser les objets de différentes manières

  • Une alternative plus souple consiste à appliquer un modèle de conception Fabrique (Factory)

3.4.3. Accès aux membres

  • Une classe peut contrôler l’accès à ses membres (données ou fonctions)

    privé

    accès limité à la classe

    protégé

    accès limité à la classe et à ses sous-classes

    module

    accès limité au module (package) contenant la classe

    public

    accès non limité

3.4.4. Invariant de classe

l'invariant d’une classe impose une contrainte sur l’état des instances de la classe. Chaque objet doit respecter les conditions imposées par l’invariant.
  • L’invariant est établi lors de la création d’une instance

  • Il doit être vérifié avant l’exécution d’une opération publique

  • Chaque opération publique doit rétablir l’invariant avant de se terminer

  • L’invariant peut être violé temporairement lors de l’exécution d’une opération

  • Avec un langage de programmation, l’invariant peut être exprimé avec des assertions ou avec une construction spécifique

3.5. Les classes en Java

3.5.1. Définir 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 janv. 2017
 * @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.5.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 se fait dans un constructeur

  • final est optionnel et permet de déclarer un attribut qui ne pourra être affecté qu’une unique fois

3.5.3. Pseudo-attribut this

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

  • Cette 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

  • Usage

    • 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.5.4. Exemple : les attributs d’un cercle

package fr.uvsq.info.poo.classes;

import fr.uvsq.info.poo.coo.Point2D;

/**
 * Un cercle en deux dimensions.
 *
 * @version  jan. 2017
 * @author   Stéphane Lopes
 * 
 */
class Cercle2D {
    /** Le centre du cercle. */
    private Point2D centre;

    /** Le rayon du cercle */
    private final double rayon;

3.5.5. Définir des méthodes

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

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

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

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

    • bien que l’on puisse initialiser les attributs directement (affection lors de leur déclaration ou bloc d’initialisation), il est préférable de toujours le faire dans le constructeur (plus de souplesse, initialisations à un seul endroit)

3.5.6. 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 ne peut pas être modifiée

    • la valeur d’une référence ne peut pas être modifiée mais l’objet référencé peut l’être (comme avec un pointeur en C)

3.5.7. Exemple : les constructeurs de la classe Cercle2D

/**
 * Initialise un cercle avec un centre et un rayon.
 * @param centre Le centre.
 * @param rayon Le rayon.
 */
public Cercle2D(final Point2D centre, final double rayon) {
    this.centre = centre;
    this.rayon = rayon;
}

/**
 * Initialise un cercle centré à l'origine et de rayon 1
 */
public Cercle2D() {
    this(new Point2D(), 1.0);
}

3.5.8. Exemple : les accesseurs de la classe Cercle2D

/**
 * Renvoie le centre du cercle.
 * @return le centre du cercle.
 */
public Point2D getCentre() {
    return centre;
}

/**
 * Renvoie le rayon du cercle.
 * @return le rayon du cercle.
 */
public double getRayon() {
    return rayon;
}

3.5.9. Exemple : le mutateur de la classe Cercle2D

/**
 * Translate le cercle.
 * @param dx deplacement en abscisse.
 * @param dy deplacement en ordonnees.
 */
public void translate(final double dx, final double dy) {
    centre.translate(dx, dy);
}

3.5.10. Contrôle d’accès aux membres en Java

  • Le contrôle de l’accès aux membres permet de spécifier l’interface d’un classe

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

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

Niveau Classe Module Sous-classe Extérieur

private

X

aucun

X

X

protected

X

X

X

public

X

X

X

X

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

  • Les attributs sont déclarés private pour respecter l’encapsulation

3.5.11. La classe Cercle2D dans son ensemble

// tag::cercle-attr[]
package fr.uvsq.info.poo.classes;

import fr.uvsq.info.poo.coo.Point2D;

/**
 * Un cercle en deux dimensions.
 *
 * @version  jan. 2017
 * @author   Stéphane Lopes
 * 
 */
class Cercle2D {
    /** Le centre du cercle. */
    private Point2D centre;

    /** Le rayon du cercle */
    private final double rayon;
// end::cercle-attr[]
// tag::cercle-cons[]
    /**
     * Initialise un cercle avec un centre et un rayon.
     * @param centre Le centre.
     * @param rayon Le rayon.
     */
    public Cercle2D(final Point2D centre, final double rayon) {
        this.centre = centre;
        this.rayon = rayon;
    }

    /**
     * Initialise un cercle centré à l'origine et de rayon 1
     */
    public Cercle2D() {
        this(new Point2D(), 1.0);
    }
// end::cercle-cons[]

// tag::cercle-access[]
    /**
     * Renvoie le centre du cercle.
     * @return le centre du cercle.
     */
    public Point2D getCentre() {
        return centre;
    }

    /**
     * Renvoie le rayon du cercle.
     * @return le rayon du cercle.
     */
    public double getRayon() {
        return rayon;
    }
// end::cercle-access[]

// tag::cercle-mut[]
    /**
     * Translate le cercle.
     * @param dx deplacement en abscisse.
     * @param dy deplacement en ordonnees.
     */
    public void translate(final double dx, final double dy) {
        centre.translate(dx, dy);
    }
// end::cercle-mut[]

    /**
     * Retourne une chaine decrivant le cercle.
     * @return la representation textuelle du cercle.
     */
    @Override
    public String toString() {
        return String.format("[(%f, %f), %f]", centre.getAbscisse(), centre.getOrdonnee(), rayon);
    }
}

3.5.12. Exercice : manipulation d’objets et définition de classes

Pour cet exercice, on reprendra les spécifications obtenues lors de l’exercice précédent.

  • Soit le terrain suivant (M = mur, R = robot, N = nord, O = ouest).

Diagram
  1. Écrire les instructions Java permettant de créer ce terrain puis de déplacer le robot (0, 0) en (1, 1).

  2. Écrire en Java la classe Robot.

3.6. Métaclasse et membres de classe

3.6.1. Métaclasse

  • La relation qui existe entre objet et classe est une relation d’instanciation

  • De même, on peut considérer une classe comme une instance de classe de plus "haut niveau", dite métaclasse

  • Cette notion permet d’introduire des méthodes de classe et des attributs de classe, i.e. des membres associés à la classe et non pas à une instance particulière

  • Une méthode de classe peut être invoquée sans instance particulière

  • Un attribut de classe est associé à la classe elle-même et non pas à une instance particulière

3.7. Membres de classe en Java

3.7.1. Membres de classe en Java

  • Un membre de classe se déclare avec le mot-clé static

  • L’accès à un membre de classe se fait par l’intermédiaire de la classe

  • 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

  • Un bloc d’initialisation statique 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 de this)

3.7.2. Exemple : compter les instances de cercle

/**
 * Un cercle en deux dimensions.
 *
 * @author Stephane Lopes
 * @version jan. 2017
 */
class Cercle2DWithCpt extends Cercle2D {
    /**
     * Le nombre d'instances de Cercle2DWithCpt
     */
    static int nbInstances = 0;

    /**
     * Initialise un cercle avec un centre et un rayon.
     *
     * @param centre Le centre.
     * @param rayon  Le rayon.
     */
    public Cercle2DWithCpt(Point2D centre, double rayon) {
        super(centre, rayon);
        ++nbInstances;
    }

    /**
     * Retourne le nombre d'instances de la classe.
     *
     * @return le nombre d'instances.
     */
    public static int getNbInstances() {
        return nbInstances;
    }

    /**
     * Décrémentation du nombre d'instances quand l'objet est détruit.
     */
    @Override
    protected void finalize() throws Throwable {
        --nbInstances;
        super.finalize();
    }

3.7.3. Exemple : invoquer une méthode de classe

assert Cercle2DWithCpt.getNbInstances() == 0 :
        Cercle2DWithCpt.getNbInstances();

// Creation des cercles
Cercle2DWithCpt c1 = new Cercle2DWithCpt(new Point2D(2.0, 4.0),
        5.0);
Cercle2DWithCpt c2 = new Cercle2DWithCpt(new Point2D(1.0, 2.0),
        4.0);
Cercle2DWithCpt c3 = new Cercle2DWithCpt(new Point2D(3.0, 4.0),
        2.0);

assert Cercle2DWithCpt.getNbInstances() == 3 :
        Cercle2DWithCpt.getNbInstances();

// Simple liaison
Cercle2DWithCpt unAutreC3 = c3;

assert Cercle2DWithCpt.getNbInstances() == 3 :
        Cercle2DWithCpt.getNbInstances();

// Suppression d'un instance
c1 = null;
System.gc();
Thread.sleep(1000);

assert Cercle2DWithCpt.getNbInstances() == 2 :
        Cercle2DWithCpt.getNbInstances();

3.8. Le programme principal en Java

3.8.1. Rôle du programme principal

  • Un système OO en cours d’exécution est une collection d’objets qui interagissent

  • Le lancement du programme doit donc permettre d’instancier ces objets

    • en général, le programme principal se limite à créer un objet application qui se charge d’instancier les autres objets

3.8.2. 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 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

3.8.3. Exemple : le programme principal en Java (avec une énumération)

package fr.uvsq.info.poo.classes;

/**
 * Représente l'application.
 *
 * @version  jan. 2017
 * @author   Stéphane Lopes
 * 
 */
enum ApplicationVide {
    ENVIRONNEMENT;
    
    /*
     * 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) {
      ENVIRONNEMENT.run(args);
    }
}

3.9. Énumération en Java

3.9.1. Complément sur le type énuméré

  • Un type énuméré en Java est en fait une classe dont les instances sont connues lors de la compilation

  • Les constantes d’un type énuméré peuvent être utilisées partout où un objet peut l’être

  • Un type énuméré peut donc 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

3.9.2. Exemple : un type énuméré pour les planètes

public enum Planet {
    MERCURY (3.303e+23, 2.4397e6),
    VENUS   (4.869e+24, 6.0518e6),
    EARTH   (5.976e+24, 6.37814e6),
    MARS    (6.421e+23, 3.3972e6),
    JUPITER (1.9e+27,   7.1492e7),
    SATURN  (5.688e+26, 6.0268e7),
    URANUS  (8.686e+25, 2.5559e7),
    NEPTUNE (1.024e+26, 2.4746e7);

    private final double mass;   // in kilograms
    private final double radius; // in meters
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }
    private double mass() { return mass; }
    private double radius() { return radius; }

    // universal gravitational constant  (m3 kg-1 s-2)
    public static final double G = 6.67300E-11;

    double surfaceGravity() {
        return G * mass / (radius * radius);
    }
    double surfaceWeight(double otherMass) {
        return otherMass * surfaceGravity();
    }
    public static void main(String[] args) {
        if (args.length != 1) {
            System.err.println("Usage: java Planet <earth_weight>");
            System.exit(-1);
        }
        double earthWeight = Double.parseDouble(args[0]);
        double mass = earthWeight/EARTH.surfaceGravity();
        for (Planet p : Planet.values())
           System.out.printf("Your weight on %s is %f%n",
                             p, p.surfaceWeight(mass));
    }
}

3.10. Généricité

3.10.1. 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

  • Une classe paramétrée (ou générique) est donc une métaclasse particulière

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

  • Utiliser un type paramétré améliore la vérification de type lors de la compilation

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

3.11. Généricité en Java

3.11.1. Généricité 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

3.11.2. Classe générique en Java

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

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

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

Cercle<Point2D> c = //...
  • la création d’une instance ne répète pas le type effectif (diamond notation)

Cercle<Point2D> c = new Cercle<>(/* ... */);

3.11.3. Exemple : définition de la classe générique Cercle

/**
 * Un cercle générique.
 * Ce cercle peut s'adapter au cas à 2 ou à 3 dimensions.
 *
 * @version  jan. 2017
 * @author   Stephane Lopes
 * 
 */
class Cercle<T> {
    /** Le centre du cercle. */
    private T centre;

    /** Le rayon du cercle */
    private double rayon;

    /**
     * Initialise un cercle avec un centre et un rayon.
     * @param centre Le centre.
     * @param rayon Le rayon.
     */
    public Cercle(T centre, double rayon) {
        this.centre = centre;
        this.rayon = rayon;
    }

    public T getCentre() {
        return centre;
    }

    public double getRayon() {
        return rayon;
    }

3.11.4. Exemple : utilisation de la classe générique Cercle

// En 2 dimensions
Point2D p1 = new Point2D(1.0, 2.0);
Cercle<Point2D> c1 = new Cercle<Point2D>(p1, 3.0);

// Invocation d'une methode de Point2D
double xCentre = c1.getCentre().getAbscisse();

// En 3 dimensions
Point3D p2 = new Point3D(3.0, 4.0, 5.0);
Cercle<Point3D> c2 = new Cercle<Point3D>(p2, 3.0);

// Invocation d'une methode de Point3D
double zCentre = c2.getCentre().getZ();

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

  • Il est possible de déclarer des méthodes génériques

  • Le paramètre de type formel est alors précisé avant la 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.11.6. 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 = //...

3.12. Exercices (Java)

  • Utilisation de l’environnement de développement BlueJ : chaque exercice nécessite la création d’un nouveau projet.

  • Les seules sources d’information externes autorisées sont

  • Exécution interactive uniquement avec BlueJ (pas de méthode main ni d’affichage).

3.12.1. Création d’une classe simple

L’objet de cet exercice est de réaliser une classe ChaineCryptee qui permettra de chiffrer/déchiffrer une chaîne de caractères (composée de lettres majuscules et d’espace). Le chiffrement utilise une méthode par décalage. La valeur du décalage représente la clé de chiffrement. Par exemple, une clé de valeur trois transformera un 'A' en 'D'.

  1. Créez la classe ChaineCryptee avec pour attribut la chaîne en clair et le décalage.

  2. Ajoutez un constructeur pour initialiser les instances à partir d’une chaîne en clair et du décalage.

  3. Ajoutez la méthode String decrypte() qui retourne la chaîne en clair.

  4. Ajoutez la méthode String crypte() qui retourne la chaîne chiffrée. Vous pourrez utilisez pour cela la méthode decaleCaractere (voir le listing ci-dessous).

  5. Comment se comporte votre classe si la chaîne passée au constructeur est null ? Vous pouvez utiliser le débogueur pour identifier le problème (s’il y a un problème) au niveau de crypte.

    1. si la méthode a échoué, modifiez la classe pour prendre en compte la chaîne null,

    2. vérifiez avec le débogueur que le problème est réglé.

  6. Faites afficher l’interface de la classe (la documentation JavaDoc). Le comportement de votre classe en cas de chaîne null doit être expliqué dans la documentation.

  7. Changez la représentation interne de la classe : seule la chaîne cryptée est stockée (plus la chaîne en clair).

    1. effectuez les modifications nécessaires sans changer l’interface de la classe.

  8. Ajoutez la possibilité d’initialiser une instance à partir d’une chaîne cryptée et d’un décalage. Pour éviter l’ambiguïté au niveau du constructeur, vous utiliserez le modèle de conception Fabrication. Pour cela,

    1. créez les méthodes de classe ChaineCryptee deCryptee(String, int) et ChaineCryptee deEnClair(String, int),

    2. rendez le constructeur privé. La création des instances se fait maintenant à l’aide des deux méthodes de classe.

/**
 * Décale un caractère majuscule.
 * Les lettres en majuscule ('A' - 'Z') sont décalées de <code>decalage</code>
 * caractères de façon circulaire. Les autres caractères ne sont pas modifiés.
 *
 * @param c le caractère à décaler
 * @param decalage le décalage à appliquer
 * @return le caractère décalé
 */
private static char decaleCaractere(char c, int decalage) {
    return (c < 'A' || c > 'Z')? c : (char)(((c - 'A' + decalage) % 26) + 'A');
}

À la fin de cet exercice, la classe ChaineCryptee doit avoir la structure suivante

encryptedstring

3.12.2. Classes et associations

On souhaite simuler le comportement d’un logiciel de discussion client/serveur. Pour pouvoir discuter, un client doit au préalable se connecter au serveur. Lorsque le client envoie un message au serveur, ce dernier le transmet à tous les clients avec le nom de l’émetteur.

Un diagramme de classes possible pour cet énoncé est donné par la figure suivante.

chat
  1. Donnez un diagramme de séquence UML décrivant le scénario suivant:

    1. le serveur s est actuellement connecté avec les clients c1 et c2,

    2. le client c3 se connecte à s,

    3. le client envoie le message Bonjour aux clients connectés.

  2. Implémentez les classes Client et Serveur.

  3. déroulez le scénario de la question 1 dans Bluej.

3.12.3. Agrégation et composition

Un document est décrit par un titre, le nom de son auteur, l’année de publication et une liste de références sur d’autres documents (par exemple, des documents traitant du même sujet). Une bibliothèque gère un ensemble de documents.

Les opérations que l’on souhaite pouvoir effectuer sont les suivantes :

  • initialisation d’un document avec son titre, l’auteur et une année,

  • ajout d’une référence dans un document,

  • affichage (construire une chaîne de caractères le représentant) d’un document (avec ses références sous la forme titre, auteur),

  • ajout d’un document dans la bibliothèque,

  • recherche par titre d’un document dans la bibliothèque,

  • recherche des documents citant un document.

    1. Modélisez cette énoncé à l’aide d’un diagramme de classe.

    2. Implémentez ce modèle.

4. Héritage

4.1. Héritage en Java

4.1.1. Héritage en Java

  • On spécifie qu’une classe est une sous-classe d’une autre en utilisant extends dans la déclaration class NomClasse extends NomSuperClasse

  • Une classe ne peut avoir qu’une seule super-classe (pas d’héritage multiple)

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

  • Une classe Java a une et une seule super-classe

  • Une classe déclarée final ne peut plus être spécialisée

4.1.2. 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 formcément accessibles

    • les attributs privées 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

4.1.3. Masquage de membres

  • 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

4.1.5. Exemple : définition de la classe Rectangle2DPlein en Java

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

    /**
     * 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 supGauche,
                            Point2D infDroit,
                            Color couleur) {
        super(supGauche, infDroit);
        assert couleur != null;
        this.couleur = couleur;
    }

    /**
     * Renvoie la couleur.
     *
     * @return la couleur.
     */
    public Color getCouleur() {
        return couleur;
    }
  • extends exprime l’héritage

  • seule les attributs supplémentaires sont déclarés dans la sous-classe

  • dans le constructeur, super permet d’appeler le constructeur de la super-classe

  • seule les méthodes supplémentaires sont définies dans la sous-classe

4.1.6. Exemple : utilisation de la classe Rectangle2DPlein

// Déclaration et création d'un rectangle rouge
Rectangle2DPlein rp = new Rectangle2DPlein(new Point2D(1.0, 2.0),
        new Point2D(3.0, 0.0),
        Color.RED);
assert rp.getCouleur() == Color.RED;

// Déclaration d'un rectangle et
// liaison avec un rectangle plein
Rectangle2D r = new Rectangle2DPlein(new Point2D(1.0, 2.0),
        new Point2D(2.0, 1.0),
        Color.YELLOW);
assert r.getLargeur() == 1;
  • L’affectation d’une instance de Rectangle2DPlein à une référence sur un Rectangle2D respecte le principe de substitution de Liskov

  • À partir de r1, l’accès à getCouleur est impossible (échoue à la compilation) car getCouleur ne fait pas partie de l’interface de Rectangle2D

4.1.7. Application : l’héritage

Deux nouveaux types de robot sont créés :

  • Les transporteurs qui peuvent ramasser un objet, le transporter et le déposer,

  • Les destructeurs qui peuvent détruire ce qui se trouve sur la case devant eux.

Soit le terrain suivant (M = mur, T = transporteur, D = destructeur, N = nord, E = est, S = sud, O = ouest).

Diagram
  1. Donner un diagramme de classes représentant les nouveaux robots

  2. Écrire les instructions permettant de créer ce terrain puis de déplacer l’objet se trouvant en (0, 0) en (2, 2).

  3. Donner l’implémentation des deux classes Transporteur et Destructeur.

4.2. Polymorphisme en Java

4.2.1. Redéfinition de méthode

  • Une sous-classe peut redéfinir (override) une ou plusieurs méthodes de sa super-classe

  • 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 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

4.2.2. Redéfinition et polymorphisme

  • Le polymorphisme est le mécanisme permettant de sélectionner la méthode redéfinie appropriée

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

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

4.2.3. Compléments sur la redéfinition de méthode

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

  • 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

4.2.5. Exemple : la classe Rectangle2D

/**
 * Un rectangle en deux dimensions.
 * Les côtés du rectangle sont toujours parallèles aux axes.
 *
 * @version  jan. 2017
 * @author   Stéphane Lopes
 * 
 */
class Rectangle2D extends FigureFermee2D implements Cloneable {
    /** Coordonnées du coin supérieur gauche */
    private Point2D orig;

    /** Coordonnées du coin inférieur droit */
    private Point2D fin;

    /**
     * Initialise le rectangle.
     * @param supGauche Le coin supérieur gauche.
     * @param infDroit Le coin inférieur droit.
     */
    public Rectangle2D(Point2D supGauche, Point2D infDroit) {
        assert supGauche.getX() <= infDroit.getX() &&
               supGauche.getY() >= infDroit.getY();
        orig = supGauche;
        fin = infDroit;
    }

    public Point2D getSupGauche() { return orig; }

    public double getLargeur() {
        return fin.getX() - orig.getX();
    }

    public double getHauteur() {
        return orig.getY() - fin.getY();
    }

    /**
     * Retourne une description du rectangle.
     * @return la description.
     */
    public String getDesc() {
        return String.format("O = %s L = %s, H = %s", orig, getLargeur(), getHauteur());
    }

4.2.6. Exemple : la classe Rectangle2DPlein

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

    /**
     * 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 supGauche,
                            Point2D infDroit,
                            Color couleur) {
        super(supGauche, infDroit);
        assert couleur != null;
        this.couleur = couleur;
    }

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

4.2.7. Exemple : utilisation du polymorphisme

// Création d'un tableau de références sur des Rectangle2D
final int NB_RECTANGLES = 2;
Rectangle2D[] figures = new Rectangle2D[NB_RECTANGLES];

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

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

// getDesc() de Rectangle2D
assert figures[0].getDesc().equals("O = (0.0, 5.0) L = 2.0 H = 3.0");

// getDesc() de Rectangle2DPlein
assert figures[1].getDesc().equals("O = (1.0, 3.0) L = 2.0 H = 1.0, couleur : (0, 0, 255)");

4.2.8. Application : le polymorphisme et la redéfinition

Une amélioration importante a été apportée aux robots: ils sont maintenant programmables. Chaque type de robot possède son propre comportement. Quand ils en reçoivent l’ordre, ils exécutent l’action programmée (un ensemble d’instructions élémentaires). Plusieurs robots de types différents se trouvent sur le terrain. On veut pouvoir déclencher l’exécution du programme de l’ensemble des robots.

  • Donner un diagramme de classe des robots

  • Implémenter les changements dans les classes Robot, Transporteur et Destructeur

  • Écrire un programme déclenchant l’exécution de l’action pour l’ensemble des robots

4.3. La classe Object

4.3.1. La classe Object

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

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

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

4.3.2. Les méthodes de la classe Object

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

  • protected Object clone() permet de dupliquer un objet

  • boolean equals(Object obj) permet de tester l’égalité de deux objets et int hashCode() de renvoyer une valeur de hashage

    • Object.equals teste l’identité

    • equals et hashCode doivent être redéfinies ensembles

  • protected void finalize() représente le destructeur d’un objet

  • String toString() retourne une chaîne représentant l’objet

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

  • Autres méthodes

    • Class getClass() retourne un objet de type Class représentant la classe de l’objet

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

    • quelques méthodes pour les threads

4.3.3. Exemple : redéfinition de la méthode toString() de 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());
}

4.3.4. Exemple : redéfinition de la méthode toString() de Rectangle2DPlein

/**
 * Retourn une chaîne représentant l'objet.
 *
 * @return la chaîne.
 */
@Override
public String toString() {
    return String.format("%s, couleur : %s", super.getDesc(), couleur);
}
// Test de toString
assert figures[0].toString().equals("O = (0.0, 5.0) [5.0, 0.0] L = 2.0 H = 3.0");
assert figures[1].toString().equals("O = (1.0, 3.0) [3.1622776601683795, 0.3217505543966422] L = 2.0 H = 1.0 couleur = java.awt.Color[r=0,g=0,b=255]");

4.3.5. Copie d’objets

4.3.6. Egalité d’objets : la méthode equals

  • La méthode boolean equals(Object o) teste l’égalité de deux objets

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

  • Il est donc en général nécessaire de redéfinir equals pour le test d’égalité

  • Rappel: l’opérateur == teste l’identité de ses opérandes, i.e. l’égalité des références

4.3.7. Egalité d’objets : contraintes de equals

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

    • x.equals(x) == true

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

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

    • x.equals(null) == false

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

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

4.3.8. Exemple : égalité de rectangles

package fr.uvsq.info.poo.inheritance;

import javafx.geometry.Point2D;

// tag::rect[]
/**
 * Un rectangle en deux dimensions.
 * Les côtés du rectangle sont toujours parallèles aux axes.
 *
 * @version  jan. 2017
 * @author   Stéphane Lopes
 * 
 */
class Rectangle2D extends FigureFermee2D implements Cloneable {
    /** Coordonnées du coin supérieur gauche */
    private Point2D orig;

    /** Coordonnées du coin inférieur droit */
    private Point2D fin;

    /**
     * Initialise le rectangle.
     * @param supGauche Le coin supérieur gauche.
     * @param infDroit Le coin inférieur droit.
     */
    public Rectangle2D(Point2D supGauche, Point2D infDroit) {
        assert supGauche.getX() <= infDroit.getX() &&
               supGauche.getY() >= infDroit.getY();
        orig = supGauche;
        fin = infDroit;
    }

    public Point2D getSupGauche() { return orig; }

    public double getLargeur() {
        return fin.getX() - orig.getX();
    }

    public double getHauteur() {
        return orig.getY() - fin.getY();
    }

    /**
     * Retourne une description du rectangle.
     * @return la description.
     */
    public String getDesc() {
        return String.format("O = %s L = %s, H = %s", orig, getLargeur(), getHauteur());
    }
// end::rect[]

    // tag::rect-tostring[]
    /**
     * 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());
    }
    // end::rect-tostring[]

    /**
     * Retourne une copie "profonde" de l'objet.
     * @return la copie.
     */
    @Override
    public Object clone() throws CloneNotSupportedException {
        Rectangle2D r = (Rectangle2D)super.clone();
        r.orig = new Point2D(orig.getX(), orig.getY());
        r.fin = new Point2D(fin.getX(), fin.getY());
        return r;
    }

    // tag::rect-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();
    }
    // end::rect-equals[]

    // tag::rect-translate[]
    /**
     * 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.add(dx, dy);
        fin.add(dx, dy);
    }
    // end::rect-translate[]
}

4.3.9. Exemple : contraintes de equals

Rectangle2D r1 = new Rectangle2D(new Point2D(0.0, 5.0),
        new Point2D(2.0, 2.0));
Rectangle2D r2 = new Rectangle2D(new Point2D(0.0, 5.0),
        new Point2D(2.0, 2.0));
Rectangle2D r3 = new Rectangle2D(new Point2D(0.0, 5.0),
        new Point2D(2.0, 2.0));
assert r1.equals(r1);    // Réflexivité
assert r1.equals(r2) && r2.equals(r1); // Symétrie
assert r1.equals(r2) && r2.equals(r3) &&
        !r1.equals(r3) == false; // Transitivité
assert r1.equals(null) == false;
assert r1.hashCode() == r2.hashCode();

4.4. Classe abstraite en Java

4.4.1. Classe abstraite en Java

  • Une classe est spécifiée abstraite en ajoutant le mot-clé abstract dans sa déclaration

  • L’instanciation d’une telle classe est alors refusée par le compilateur

  • Une classe abstraite contient généralement des méthodes abstraites, i.e. qui ne possèdent pas d’implémentation

    • une classe abstraite peut cependant ne pas avoir de méthodes abstraites

  • 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

4.4.3. Exemple : la classe abstraite FigureFermee2D

package fr.uvsq.info.poo.inheritance;

/**
 * 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);
}

4.4.4. Exemple : redéfinition du déplacement

/**
 * 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.add(dx, dy);
    fin.add(dx, dy);
}
/**
 * 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.add(dx, dy);
}

4.4.5. Exemple : utilisation de la classe abstraite FigureFermee2D

// Création du tableau de références
final int NB_FIGURES = 4;
FigureFermee2D[] figures = new FigureFermee2D[NB_FIGURES];

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

// Réalise une translation de la figure
for (int i = 0; i < figures.length; ++i) {
    figures[i].translate(1.0, 2.0);
}

4.4.6. Application : les classes abstraites

On souhaite maintenant ajouter sur le terrain un type de case sensible à la charge (pont par exemple). Chaque élément mobile (robot ou objet transportable) devra donc posséder un poids. Le poids d’une instance de robot sera toujours de 1 unité. Une instance de destructeur aura un poids de 2 unités de plus que le robot. Une instance de transporteur aura un poids de 1 unités de plus que le robot auquel il faudra ajouter le poids de l’objet transporté. Le poids par défaut des objets transportables est de 1 unité mais chaque objet pourra avoir un poids différent précisé lors de sa création. On souhaite pouvoir connaître le poids de tout élément se trouvant sur le terrain.

  1. Modéliser la prise en compte du poids au niveau des éléments mobiles

  2. Implémenter les classes correspondantes

4.5. La classe Number

4.5.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.

4.5.2. 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();

4.5.3. Les adaptateurs

4.6. Interface

4.6.1. Du point de vue des types

  • Une interface regroupe uniquement des signatures d’opérations et des déclarations de constantes mais aucune implémentation

  • Une interface permet donc de définir un type

  • Les interfaces peuvent être organisées en hiérarchies

  • Un lien entre interface est une relation de sous-typage

4.6.2. Du point de vue des services

  • Une interface fournit une vue totale ou partielle d’un ensemble de services offerts par une classe (un composant)

  • Une interface est analogue à un protocole de comportement (un contrat sur un comportement)

  • Une classe peut implémenter une ou plusieurs interfaces

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

4.6.3. Utilisation d’une interface

  • Permet à des objets d’interagir même s’ils ne sont pas en relation

  • Permet de capturer des similarités entre classes non reliées sans définir artificiellement une relation

  • Permet de déclarer des méthodes qu’une ou plusieurs classes doivent implémenter

  • Permet de révéler l’interface de programmation d’un objet sans révéler sa classe

4.6.5. Définition d’une interface en Java

  • La définition d’une interface comporte une déclaration et un corps

interface UneInterface extends UneSecondeInterface, UneAutreInterface {
  final String uneChaine = "abcde";
  final double unDouble = 123.456;
  void uneMethode(int unEntier, String uneChaine);
}
  • Une interface peut avoir plusieurs super-interfaces

  • Toutes les méthodes de l’interface sont implicitement public et abstract

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

4.6.6. Utilisation d’une interface

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

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

  • Une interface est utilisée comme un type (par exemple comme paramètre d’une méthode)

4.6.7. Exemple : définir l'ordre naturel des objets (interface Comparable)

// Comparable est une interface de la librairie Java

/**
 * 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. ...
   */
  public int compareTo(T o);
}

// ...
class Rectangle2D implements Comparable<Rectangle2D> {
  // ...
  public int compareTo(Rectangle2D o) {
    // Code pour la comparaison
  }
  // ...
}

4.7. Exercices (Java)

  • Utilisation de l’environnement de développement BlueJ

  • Les seules sources d’information externes autorisées sont

4.7.1. Héritage et polymorphisme

On souhaite réaliser une application pour gérer une collection de CD et de DVD. Un CD possède un titre, le nom de l’artiste ou du groupe, et le nombre de titres. Un DVD possède un titre, un réalisateur et une année de sortie.

L’application devra permettre de :

  • créer des documents (CD ou DVD),

  • consulter les informations d’un document,

  • ajouter un document dans la collection,

  • lister les documents de la collection,

  • rechercher les documents contenant un mot clé dans le titre ou dans le groupe/réalisateur.

    1. Donner un diagramme de classe UML modélisant ce problème.

    2. Proposer une implémentation de cette modélisation.

4.7.2. Création d’une classe respectant les conventions du langage Java

L’objectif de cet exercice est de réaliser une classe immuable Fraction qui représente un nombre rationnel. Un exemple d’interface pour une telle classe est donné par la classe Fraction de la bibliothèque Apache Commons Math.

Une fraction comporte un numérateur et un dénominateur (nombres entiers).

Vous respecterez les conventions Java quand cela est approprié (égalité, conversion, comparaison, …​).

Implémentez la classe en fournissant l’interface suivante:

  1. initialisation avec (i) un numérateur et un dénominateur, (ii) juste avec le numérateur (dénominateur égal à 1) ou (iii) sans argument (numérateur égal 0 et dénominateur égal à 1),

  2. les constantes ZERO (0, 1) et UN (1, 1),

  3. consultation du numérateur et du dénominateur,

  4. consultation de la valeur sous la forme d’un nombre en virgule flottante (double),

  5. addition de deux fractions,

  6. test d’égalité entre fractions (deux fractions sont égales si elles représentent la même fraction réduite),

  7. conversion en chaîne de caractères,

  8. comparaison de fractions selon l’ordre naturel.

4.7.3. Héritage, polymorphisme et classe abstraite

On veut simuler le fonctionnement d’un système de fichiers. Un fichier est représenté par son nom et sa taille. Un répertoire est défini par son nom et peut contenir des fichiers et/ou des répertoires. La base de l’arborescence du système de fichier est le répertoire racine.

On veut pouvoir calculer la taille totale d’un répertoire.

  1. Représenter sur un diagramme de classes UML une hiérarchie de classe modélisant ce problème. Le modèle de conception Composite peut être utilisé pour cela.

  2. Proposer une implémentation de ce modèle.

  3. Créer une arborescence de test et vérifier le calcul de la taille.

  4. Modifier le programme pour qu’un répertoire ne puisse pas être ajouté à lui-même.

  5. Modifier le programme pour qu’un répertoire ne puisse pas être ajouté comme descendant de lui-même.

4.7.4. Utilisation de la ligne de commande

Le but de cet exercice est de vous familiariser avec les outils Java en ligne de commande.

  1. Créer le répertoire FileSystem contenant les sous-répertoires src et classes.

  2. Placer les fichiers .java de l’exercice précédent dans le sous-répertoire src.

  3. Compiler les sources en incluant les informations de débogage et de façon à placer les .class dans le répertoire classes (cf. documentation de javac).

  4. Ajouter un programme principal contenant le code de test de l’exercice précédent et recompiler.

  5. Exécuter le programme (cf. documentation de java).

  6. Créer une archive Java pour le programme (cf. documentation de jar) et exécuter à nouveau le programme à partir de l’archive.

5. Module et bibliothèques

5.1. Module en Java

5.1.1. Définition d’un module

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

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

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

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

  • La librairie Java est organisée en module (java.lang, java.util, java.io, \dots)

5.1.2. Interface d’un module

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

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

public class MaClasse { //...

5.1.3. Utilisation d’un module

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

    • utiliser son nom qualifié (par exemple, uvsq.in404.monpackage.MaClasse)

    • importer l’élément (par exemple, import uvsq.in404.monpackage.MaClasse; placé en début de fichier source)

    • importer le module complet (par exemple, import uvsq.in404.monpackage.*; placé en début de fichier source)

    • importer les classes imbriquées (import uvsq.in404.monpackage.MaClasse.*;)

    • importer les membres de classes (import static uvsq.in404.monpackage.MaClasse.*;)

  • Les directives import se placent avant toute définition de classes ou d’interfaces mais après l’instruction package

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

5.1.4. Module 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

  • 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 uvsq.in404.monpackage.MaClasse doit se trouver dans le fichier MaClasse.java du répertoire uvsq/in404/monpackage

5.1.5. Module et compilation

  • Lors de la compilation, un fichier .class est créé pour chaque élément

  • 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 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.2. Bibliothèques en Java

5.2.1. É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)

5.2.2. Utilisation d’un bibliothèque tierce

  • Récupérer la bibliothèque

    • manuellement (téléchargement)

    • 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.2.3. Exemple : utilisation de la bibliothèque Apache Commons Math 1/4

~/commons-math3-3.4.1 $ ls
commons-math3-3.4.1.jar          docs/        NOTICE.txt
commons-math3-3.4.1-javadoc.jar  LICENSE.txt  RELEASE-NOTES.txt

5.2.4. Exemple : utilisation de la bibliothèque Apache Commons Math 2/4

  • Ajouter la bibliothèque au projet (IDE ou outil de build)

bluej 3rdparty

5.2.5. Exemple : utilisation de la bibliothèque Apache Commons Math 3/4

  • 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);
    }
}

5.2.6. Exemple : utilisation de la bibliothèque Apache Commons Math 4/4

  • Compiler en précisant la bibliothèque dans le CLASSPATH (en ligne de commande)

$ javac -cp ../commons-math3-3.6.1/commons-math3-3.6.1.jar Main.java
  • É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

5.3. Exercices (Java)

5.3.1. Module et bibliothèque

Dans cet exercice, vous reprendrez le code source de l’exercice simulation de client/serveur.

  1. Placez l’ensemble du code source dans le package in404.exo61.

  2. Ajoutez une classe contenant le programme principal qui implémentera le scénario de la question 1 de l’exercice.

  3. Déplacez la classe Client dans le package in404.exo61.client et la classe Serveur dans le package in404.exo61.serveur. Apportez les modifications nécessaires au programme principal.

  4. Construisez une archive jar contenant les classes compilées in404.exo61.client.Client et in404.exo61.serveur.Serveur. Modifiez le projet pour qu’il utilise cette bibliothèque.

5.3.2. Utilisation d’une bibliothèque tierce

Vous utiliserez ici la bibliothèque Apache Commons Math.

  1. En consultant la documentation Javadoc sur le site, identifiez la classe (et son package) permettant la manipulation de nombres complexes.

  2. Ajoutez la bibliothèque à votre projet.

  3. Écrivez un programme principal pour tester quelques méthodes de la classe.

6. Gestion d’erreurs et exceptions

6.1. Les erreurs en programmation

6.1.1. Les erreurs sont communes

Des erreurs se produisent lors de l’exécution d’un programme

Différents types d’erreurs
  • Erreurs de syntaxe

    • détectées par le compilateur

  • Erreurs d’exécution

    • générées par l’environnement (plus de mémoire disponible, division par zéro, …​)

  • Erreurs de logique (bogues)

    • liées à une erreur du développeur

6.1.2. Plusieurs questions à se poser

  • Que considère-t’on comme une erreur ?

  • Quelle est la gravité de l’erreur ?

  • Comment détecter l’erreur ?

  • Comment propager l’information sur l’erreur ?

  • Comment et où gérer l’erreur ?

  • Comment signaler l’erreur ?

6.1.3. 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. Gestion d’erreurs

6.2.1. Erreur vs. bogue

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

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

    • les assertions

    • le débogage

    • les tests

6.2.2. Réaction à une erreur

  • Ignorer le problème

    • en général, c’est une mauvaise idée…​

  • Retourner un code d’erreur

    • possible si une valeur de retour est disponible pour cela

  • Utiliser une variable globale

    • son état doit être consulté

  • Lancer une exception

    • propage l’erreur selon la pile d’appel du programme

  • Utiliser le type `Option`

    • technique issue de la programmation fonctionnelle

6.2.3. Retourner un code d’erreur

public static double sqrtWithReturnCode(double d) {
    return Double.isNaN(d) || d < 0.0 ?
            Double.NaN :
            sqrt(d);
}
double result = sqrtWithReturnCode(value);
if (Double.isNaN(result)) {
    System.err.println("Argument illégal (négatif ou égal à NaN).");
} else {
    System.out.printf("sqrt(%f) = %f\n", value, result);
}

6.2.4. Utiliser une variable globale

public static 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)) {
        sqrtError = SqrtError.NaNArg;
    } else if (d < 0) {
        sqrtError = SqrtError.NegArg;
    }
    return sqrt(d);
}
double result = sqrtWithGlobalCode(value);
if (getSqrtError() != SqrtError.None) {
    System.err.println("Argument illégal (négatif ou égal à NaN).");
} else {
    System.out.printf("sqrt(%f) = %f\n", value, result);
}

6.2.5. Lancer une exception

public static double sqrtWithException(double d) {
    if (Double.isNaN(d) || d < 0.0) {
        throw new IllegalArgumentException("Argument négatif ou NaN");
    }
    return sqrt(d);
}
double result;
try {
    result = sqrtWithException(value);
    System.out.printf("sqrt(%f) = %f\n", value, result);
} catch (IllegalArgumentException ex) {
    ex.printStackTrace(System.err);
}

6.2.6. Utiliser le type Option

public static Optional<Double> sqrtWithOption(double d) {
    return Double.isNaN(d) || d < 0.0 ?
            Optional.empty() :
            Optional.of(sqrt(d));
}
Optional<Double> result = sqrtWithOption(value);
System.out.printf("sqrt(%f) = %f\n", value, result.orElse(Double.NaN));

6.3. Exceptions

6.3.1. Définition

  • Le terme exception est un raccourci pour événement exceptionnel

  • Une exception est un événement se produisant lors de l’exécution d’un programme et qui bouleverse le flôt normal d’instructions

  • Le mécanisme des exceptions est destiné à gérer les erreurs ou les cas exceptionnels (une partie du système n’a pas pu réaliser ce qui lui était demandé)

    • "Exceptionnel" ne signifie pas "qui ne se produit presque jamais" ou "désastreux"

  • Différents types d’erreurs peuvent générer des exceptions

  • Une exception contient des informations sur l’erreur qui l’a produite

  • Les exceptions peuvent être regroupées et organisées en hiérarchie

6.3.2. Fonctionnement

  • L’action de générer une exception s’appelle lancer (throw) ou lever (raise) une exception

  • Le système d’exécution doit alors trouver une portion de code sachant gérer (handle) ou capturer (catch) cette exception

  • Les candidats pour la gestion de l’exception sont les méthodes de la pile d’appel

  • Le système d’exécution remonte la pile d’appel jusqu’à trouver un gestionnaire d’exception (exception handler) approprié

  • Chaque gestionnaire d’exception gère un type d’exception particulier

  • Le gestionnaire d’exception choisi par le système traite l’exception

  • Si aucun gestionnaire approprié n’est trouvé, le système stoppe le programme

6.3.3. Remarques

  • Une exception peut être redéclenchée

  • un gestionnaire d’exceptions peut faire un traitement partiel puis "passer la main"

  • L’ordre des gestionnaires est important

    • toujours du plus spécialisé au plus général

6.3.4. Avantages des exceptions

  • Séparation du code de gestion d’erreurs et du code "normal"

    • évite les "empilements" d’instructions conditionnelles

    • améliore la lisibilité du code

  • Propagation des erreurs en suivant la pile d’appels de méthodes

    • simplification de la propagation

    • les méthodes que l’erreur ne concerne pas n’en tiennent pas compte

  • Regroupement des types d’erreurs

    • possibilité de gérer ensemble des exceptions de même type

    • par exemple, exceptions concernant un tableau ou un fichier

La gestion d’erreurs reste une tâche difficile !

6.3.5. Inconvénients des exceptions

  • Moins structurées qu’une gestion locale

    • revers de la séparation code de gestion d’erreurs/code normal

  • Moins efficace

    • le mécanisme de propagation a un coût

  • Peut rendre certaines situations complexes

Le mécanisme des exceptions offre une alternative aux techniques traditionnelles lorsque celles-ci se révèlent insuffisantes, peu élégantes et susceptibles d’introduire des erreurs

6.4. Exceptions en Java

6.4.1. Généralités

  • 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 sa signature qu’elle peut la lancer

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

6.4.2. Le traitement d’une exception comprend

  • Un bloc try

    • autour de la séquence d’instructions susceptible de lancer une exception

  • Un ou plusieurs blocs catch

    • représentant les gestionnaires d’exceptions

  • Au plus un bloc finally

    • toujours exécuté

6.4.3. Le bloc try

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

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

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

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

6.4.4. Les blocs catch

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

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

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

6.4.5. Détail d’un bloc catch

  • L’instruction catch requiert un unique paramètre

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

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

  • 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.4.6. Le bloc finally

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

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

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

finally {
    // Instructions
}

6.4.7. Des gestionnaires d’exception pour une pile

try {
    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) {
    assert e.getMessage().equals("La Pile est vide");
} catch (PileException e) {
    assert e.getMessage().equals("La Pile est pleine");
}

6.4.8. Exception et allocation de ressources

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

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

try (
    java.util.zip.ZipFile zf =
       new java.util.zip.ZipFile(zipFileName);
    java.io.BufferedWriter writer =
       java.nio.file.Files.newBufferedWriter(outputFilePath, charset)
) {
    // ...
}

6.4.9. 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.4.10. 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.4.11. Lancement d’exceptions

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

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

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

6.4.12. 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.4.13. 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-classe) peuvent être lancées

  • Seule cette classe (ou l’une de ses sous-classe) 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

6.4.16. 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

6.4.18. La classe PileException

package fr.uvsq.info.poo.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);
    }
}

6.4.19. La classe PileVideException

package fr.uvsq.info.poo.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");
    }
}

6.4.20. La classe PilePleineException

package fr.uvsq.info.poo.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");
    }
}

6.5. Exercices

6.5.1. Calculatrice RPN

Dans cette exercice, on souhaite réaliser une calculatrice fonctionnant en mode RPN (Reverse Polish Notation). Cette notation post-fixée permet de représenter des formules arithmétiques sans parenthèses. Par exemple, l’expression \$2 xx (3 + 4)\$ pourra s’écrire \$2 3 4 + xx\$.

Cette calculatrice devra supporter les opérations de base (+, -, *, /) sur des nombres réels. L’intervalle de nombres supporté sera spécifié par deux constantes (MAX_VALUE pour le plus grand nombre en valeur absolue, MIN_VALUE pour le plus petit nombre en valeur absolue). L’utilisateur saisira au clavier soit un nombre, soit une opération, soit exit pour sortir. Chaque saisie se terminera par entrée. Après chaque saisie, le programme affichera l’expression courante.

L’implémentation pourra utiliser une pile de la façon suivante :

  • les opérandes sont empilées lors de leur saisie,

  • les opérations sont effectuées immédiatement en considérant les opérandes se trouvant au sommet de la pile,

  • le résultat d’une opération est empilé.

Questions
  1. Déterminer les cas d’erreur. En déduire les exceptions nécessaires pour la gestion des erreurs de cette application.

  2. Organiser ces exceptions en hiérarchie et choisissez une classe de base dans la librairie Java

  3. Implémenter l’énumération Operation de la façon suivante :

    1. déclarer l’attribut symbole représentant le symbole de l’opération (+, -, …​),

    2. définir le constructeur prenant en paramètre le symbole de l’opération,

    3. déclarer la méthode abstraite eval retournant le résultat de l’évaluation de l’opération sur deux opérandes,

    4. définir les constantes PLUS, MOINS, MULT et DIV,

    5. pour chacune des constantes, redéfinir la méthode eval.

  4. Implémenter la classe MoteurRPN possédant les capacités suivantes :

    1. enregistrer une opérandes,

    2. appliquer une opération sur les opérandes,

    3. retourner l’ensemble des opérandes stockées.

  5. Implémenter la classe SaisieRPN qui gère les interactions avec l’utilisateur et invoque le moteur RPN. La classe java.util.Scanner permet de gérer les saisies.

  6. Implémenter l’énumération CalculatriceRPN qui contiendra le programme principal.

7. Entrées/sorties et persistance

7.1. Entrées/sorties, flux et persistance

7.1.1. Entrée/sortie et langage de programmation

  • La conception et l’implémentation d’une bibliothèque d’entrée/sortie (I/O) est une tâche difficile

    • la source ou la destination des données peut varier (mémoire, fichier, réseau, …​)

    • les types définis par l’utilisateur doivent être pris en charge

  • Le concept de flux est une proposition pour cela

  • La persistance et la sérialisation sont également nécessaires pour les I/O en POO

7.1.2. 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

7.1.5. Persistance

  • C’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

7.1.6. Sérialisation

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

7.2. I/O en Java

7.2.1. Structure et fonctionalités

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

7.2.2. Gestion des flux

  • La librairie 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.3. Flux de caractères

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

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

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

7.2.7. Flux d’octets

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

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

7.2.11. java.nio

nio fileiomethods

Source : Java Tutorial - Reading, Writing, and Creating Files

7.3. Utilisation des flux

7.3.1. Principaux flux par type d’I/O

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

Mémoire

CharArrayReader, CharArrayWriter

ByteArrayInputStream, ByteArrayOutputStream

StringReader, StringWriter

Fichier

FileReader, FileWriter

FileInputStream, FileOutputStream

Affichage

PrintWriter

PrintStream

7.3.2. Principaux flux par fonction

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

Avec buffer

BufferedReader, BufferedWriter

BufferedInputStream, BufferedOutputStream

Conv. de données

DataInputStream, DataOutputStream

Sérialisation

ObjectInputStream, ObjectOutputStream

Conv. oct./car.

InputStreamReader, OutputStreamWriter

7.3.3. 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, …​)

7.3.4. Copie d’un fichier texte

/**
 * Copie un fichier caractère par caractère.
 */
private static void textFileCopy(String inFilename, String outFilename) throws IOException {
    try (
            FileReader in = new FileReader(inFilename);
            FileWriter out = new FileWriter(outFilename)
    ) {
        int c;
        while ((c = in.read()) != END_OF_STREAM) {
            out.write(c);
        }
    }
}

7.3.5. Copie d’un fichier texte (avec buffer)

/**
 * Copie un fichier en utilisant un buffer.
 */
private static void bufferedTextFileCopy(String inFilename, String outFilename) throws IOException {
    Path inPath = Paths.get(inFilename);
    Path outPath = Paths.get(outFilename);
    try (
            BufferedReader in = Files.newBufferedReader(inPath);
            BufferedWriter out = Files.newBufferedWriter(outPath)
    ) {
        String line;
        while ((line = in.readLine()) != null) {
            out.write(line);
            out.newLine();
        }
    }
}

7.3.6. Copie d’un fichier texte (avec readAllLines)

/**
 * Copie un fichier texte.
 * Le fichier doit être de taille raisonnable car il est chargé en totalité en mémoire.
 */
private static void simpleTextFileCopy(String inFilename, String outFilename) throws IOException {
    Path inPath = Paths.get(inFilename);
    Path outPath = Paths.get(outFilename);
    List<String> lines = Files.readAllLines(inPath);
    Files.write(outPath, lines);
}

7.3.7. Copie d’un fichier binaire

/**
 * Copie un fichier octet par octet.
 */
private static void binaryFileCopy(String inFilename, String outFilename) throws IOException {
    try (
            FileInputStream in = new FileInputStream(inFilename);
            FileOutputStream out = new FileOutputStream(outFilename)
    ) {
        int c;
        while ((c = in.read()) != END_OF_STREAM) {
            out.write(c);
        }
    }
}

7.3.8. 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

7.3.9. Principaux flux de filtrage

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

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

    • BufferedInputStream et BufferedOutputStream pour des I/O avec buffer

    • PrintStream pour l’affichage des données

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

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

7.3.10. 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);

7.3.11. Ecriture des types primitifs

public static void writeDateToFile(String filename, int numberOfItems, double[] prices, int[] units, String[] descs) throws IOException {
    try (DataOutputStream out =
                 new DataOutputStream(
                         new BufferedOutputStream(
                                 new FileOutputStream(filename)))
    ) {
        for (int i = 0; i < numberOfItems; i++) {
            out.writeDouble(prices[i]);
            out.writeInt(units[i]);
            out.writeUTF(descs[i]);
        }
    }
}

7.3.12. Lecture des types primitifs

public static void readDateFromFile(String filename, int numberOfItems, double[] prices, int[] units, String[] descs) throws IOException {
    try (DataInputStream out =
                 new DataInputStream(
                         new BufferedInputStream(
                                 new FileInputStream(filename)))
    ) {
        for (int i = 0; i < numberOfItems; i++) {
            prices[i] = out.readDouble();
            units[i] = out.readInt();
            descs[i] = out.readUTF();
        }
    }
}

7.3.13. Entrée et sortie standards en Java

La classe System fournit des flux pour :

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

  • la sortie standard (attribut out de type InputStream)

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

7.3.14. Flux d’affichage PrintStream (ou PrintWriter)

  • PrintStream format( /* …​ */) affiche une chaîne selon un format

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

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

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

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

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

    • interrogeable avec la méthode boolean checkError()

7.3.15. Lire à partir de l’entrée standard

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

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

    • permet de découper un flux en token

7.3.16. Accéder à l’entrée et la sortie standard (avec Scanner)

System.out.println("Votre nom et votre âge ? ");

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

System.out.format("Votre nom est %s et vous avez %5d ans.%n", nom, age);

7.3.17. Accéder à l’entrée et la sortie standard (avec BufferedReader)

System.out.println("Votre nom et votre age ? ");

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

System.out.format("Votre nom est %s et vous avez %5d ans.%n", nom, age);

7.3.18. 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.4. Sérialisation en Java

7.4.1. Sérialisation

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

  • ObjectOutputStream implémente les interfaces DataOutput et ObjectOutput

  • ObjectInputStream implémente les interfaces DataInput et ObjectInput

7.4.2. Ecrire des objets dans un flux

private static void writeObjectsToFile(Student[] students, String filename) throws IOException {
    try (ObjectOutputStream oos =
                 new ObjectOutputStream(
                         new FileOutputStream(filename))
    ) {
        oos.writeObject(LocalDate.now());
        oos.writeObject(students);
    }
}

7.4.3. Lire des objets à partir d’un flux

private static LocalDate readObjectsFromFile(Student[] students, String filename) throws IOException, ClassNotFoundException {
    try (ObjectInputStream ois =
                 new ObjectInputStream(
                         new FileInputStream(filename))
    ) {
        LocalDate date = (LocalDate) ois.readObject();
        students = (Student[]) ois.readObject();
        return date;
    }
}

7.4.4. 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

7.4.5. Une classe sérialisable

package fr.uvsq.info.poo.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.6. 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.7. Contrôler la sérialisation

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

    • defaultWriteObject de ObjectOutputStream

    • defaultReadObject de ObjectInputStream

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

    • la classe de l’objet

    • la signature de la classe

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

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

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

7.5. Exercices

7.5.1. Utilisation des flux de caractères

Le but de cette exercice est de réaliser quelques outils de manipulation de fichiers textes.

  1. Écrire un programme prenant en argument un nom de fichier et affichant le nombre de lignes de ce fichier. Quelle classe du package java.io vous sera utile pour cette tâche ?

  2. Ècrire un programme prenant en argument une chaîne de caractères et un nom de fichier et qui affiche les lignes (avec leur numéro) du fichier contenant la chaîne. Quelle classe du package java.io vous sera utile pour cette tâche ?

7.5.2. Implémentation d’un flux de filtrage

Cette exercice consiste à implémenter une commande grep simplifiée. Parmi les nombreuses options de cette commande, vous implémenterez le sous-ensemble suivant (cf. page de manuel) : --help, -e|--regexp, -f|--file, -i|--ignore-case, -n|--line-number.

Vous utiliserez la bibliothèque

  • Apache Commons CLI pour la manipulation des arguments de ligne de commande,

  • de manipulation d’expressions rationnelles du JDK (cf. Tutoriel).

Questions
  1. Réalisez le flux de filtrage GrepReader qui lit un flux en retournant uniquement les lignes correspondant au motif recherché

  2. Réaliser la classe principale GrepApp

8. Gestion des collections

8.1. Introduction

8.1.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.1.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 permettent de manipuler des collections indépendamment de leurs implémentations

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

    • des algorithmes effectuent des traitements par l’intermédiaire des interfaces

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

8.1.4. Motivations

axes
  • \$i xx j\$ versions différentes du même algorithme pour supporter tous les conteneurs

    pour k algorithmes, \$i xx j xx k\$ programmes

  • La généricité permet de paramétrer par rapport aux types

    réduit à \$j xx k\$ programmes

  • Abstraction de la notion de collection (un algo. fonctionne sur toute collection)

    réduit à \$j + k\$ programmes

8.1.5. Intérêt d’une bibliothèque pour les collections 1/2

  • Réduit l’effort de programmation

    • fournit des SDD et des algorithmes permettant de se concentrer sur les parties importantes d’un problème sans s’occuper de détails d’implémentation

  • Améliore la qualité et les performances du programme

    • fournit des SDD et des algorithmes de haute qualité et efficaces.

    • il est aussi possible de remplacer simplement une SDD par une autre

  • Permet l’interopérabilité d’API

    • Les API peuvent échanger des collections

8.1.6. Intérêt d’une bibliothèque pour les collections 2/2

  • Réduit l’effort d’apprentissage et d’utilisation

    • permet de se concentrer sur une seule API

  • Réduit l’effort de conception de nouvelles API

    • les nouvelles API s’appuient sur les collections existantes

  • Améliore la réutilisabilité du logiciel

    • une nouvelle SDD ou un nouvel algorithme s’intégrant dans la bibliothèque est immédiatement utilisable avec le reste

8.1.9. 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.2. Interfaces

8.2.1. Les interfaces

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

  • Les interfaces permettent de manipuler les collections indépendamment des différentes implémentations

    • représente les types de structures de données

    • 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.2.2. La hiérarchie de Collection

collection hierarchie
  • Iterable autorise l’utilisation du foreach

  • D’autres interfaces existent mais sont liées à la concurrence

8.2.3. La hiérarchie de Map

map hierarchie
  • La hiérarchie de Map représentent des tableaux associatifs (dictionnaires)

8.2.4. L’interface Collection 1/2

  • 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

8.2.5. L’interface Collection 2/2

public interface Collection<E> extends Iterable<E> {
  // Opérations simples
  int size();
  boolean isEmpty();
  boolean contains(Object element);
  boolean add(E element);    // Optionnel
  boolean remove(Object element); // Optionnel
  boolean equals(Object o);
  int hashCode();
  Iterator<E> iterator();

  // Opérations de groupe
  boolean containsAll(Collection<?> c);
  boolean addAll(Collection<? extends E> c);    // Optionnel
  boolean removeAll(Collection<?> c); // Optionnel
  default boolean removeIf(Predicate<? super E> filter);
  boolean retainAll(Collection<?> c); // Optionnel
  void clear();    // Optionnel

  // Conversions
  Object[] toArray();
  <T> T[] toArray(T[] a);

  // Itération et Streams
  Iterator<E> iterator();
  default Spliterator<E> spliterator();
  default Stream<E> stream();
  default Stream<E> parallelStream();
}

8.2.6. Parcourir des collections

Trois techniques permettent de parcourir des collections

  • Les Streams

  • La boucle for-each

  • Les itérateurs

8.2.7. Les Streams

String joined = elements.stream()
    .filter(e -> e.getColor() == Color.RED)
    .map(Object::toString)
    .collect(Collectors.joining(", "));
  • Plus de détails dans le chapitre suivant

8.2.8. 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

8.2.9. Itérateur

  • Un itérateur permet de parcourir une collection

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

  • Un itérateur peut être vu comme un marqueur se trouvant entre deux éléments

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());
    }
}
  • 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

8.2.10. Parcours d’une collection avec un 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.2.11. L’interface Set

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

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

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

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

  • Sémantique des méthodes de groupe

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

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

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

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

8.2.12. L’interface List 1/2

  • 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

8.2.13. L’interface List 2/2

public interface List<E> extends Collection<E> {
  // Accès par position
  E get(int index);
  E set(int index, E element);            // Optionnel
  void add(int index, E element);              // Optionnel
  E remove(int index);                         // Optionnel
  boolean addAll(int index, Collection<? extends E> c); // Optionnel

  // 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);
  int lastIndexOf(Object o);

  // Itération
  ListIterator<E> listIterator();
  ListIterator<E> listIterator(int index);

  // Vue en sous-liste
  List<E> subList(int from, int to);
}

8.2.14. Itérateur de liste

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

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

    int nextIndex();
    int previousIndex();

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

8.2.15. 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.2.16. L’interface DeQueue

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

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

Exception

Valeur spéciale

Exception

Valeur spéciale

Insertion

addFirst(e)

offerFirst(e)

addLast(e)

offerLast(e)

Suppression

removeFirst()

pollFirst()

removeLast()

pollLast()

Accès

getFirst()

peekFirst()

getLast()

peekLast()

8.2.17. L’interface Map 1/2

  • 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

8.2.18. L’interface Map 2/2

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);
  }

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

8.2.19. Parcourir une Map

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

    keySet

    représente l'ensemble des clés

    values

    représente la collection des valeurs

    entrySet

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

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

8.2.20. Construire l’histogramme d’une image

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

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

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

8.2.21. Ordonner des objets

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

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

8.2.22. L’interface Comparable

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

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

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

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

8.2.23. Définir un ordre naturel sur des personnes

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

8.2.24. Ordonner des objets selon un ordre spécifique

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

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

8.2.25. L’interface Comparator

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

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

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

8.2.26. Définir un ordre spécifique sur des personnes

package fr.uvsq.info.poo.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();
    }
}

8.2.27. 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.2.28. L’interface SortedSet 1/2

  • Cette interface permet de maintenir les éléments d’une 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

8.2.29. L’interface SortedSet 2/2

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.2.30. L’interface SortedMap 1/2

  • 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)

8.2.31. L’interface SortedMap 2/2

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.3. Implémentations

8.3.1. 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.3.2. Implémentations généralistes

Interface

Hashage

Tableau dyn.

Arbre équ.

Liste chaînée

Hash. + chaîn.

Set

HashSet

TreeSet

LinkedHashSet

List

ArrayList

LinkedList

Queue

ArrayDeque

LinkedList

DeQueue

ArrayDeque

LinkedList

Map

HashMap

TreeMap

LinkedHashMap

  • Les implémentations principales sont en gras

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

  • Queue possède aussi pour implémentation PriorityQueue

8.3.3. Caractéristiques des implémentations généralistes

  • Toutes les implémentations implémentent toutes les méthodes optionnelles

  • Toutes sont sérialisables

  • Toutes supportent l’opération clone

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

8.3.4. Utilisation des implémentations généralistes

  1. Choisir un type d’implémentation

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

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

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

Set<Integer> unEnsemble = new HashSet<>();
List<Integer> uneList = new ArrayList<>();
Map<String, String> uneMap = new HashMap<>();

8.3.5. 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

8.3.6. 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

8.3.7. Les implémentations de List

  • ArrayList est plus rapide

    • permet un accès par position en temps constant

    • pas d’allocation à chaque ajout

    • on peut passer une capacité au constructeur de ArrayList

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

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

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

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

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

8.3.8. 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.3.9. 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"

    • non modifiables pour supprimer toute possibilité de modification d’une collection

    • vérifiant le type dynamiquement

List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());
Collection<String> collec = Collections.unmodifiableCollection(uneCollection);
Set<String> set = Collections.checkedSet(new HashSet<String>(), String.class);

8.3.10. Les implémentations "simples"

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

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

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

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

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

8.3.11. Utiliser des implémentations "simples"

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

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

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

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

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

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

8.3.12. Écrire une implémentation

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

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

  • Processus pour écrire son implémentation

    1. choisir une implémentation abstraite appropriée

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

    3. tester l’implémentation obtenue

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

8.3.13. Principales implémentations abstraites

  • AbstractCollection pour une collection quelconque

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

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

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

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

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

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

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

  • AbstractMap pour un tableau associatif

    • la vue entrySet doit être fournie

8.4. Algorithmes

8.4.1. 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.4.2. 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);

8.5. Mise en œuvre

8.5.1. Exercice sur les listes

  • Créer une liste (basée sur ArrayList) contenant les 10 premiers entiers

  • Mélanger aléatoirement les éléments

    • utiliser la méthode de classe void shuffle(List<?> list) de la classe Collections

  • Afficher la liste à l’endroit puis à l’envers

  • Trier la liste selon l’ordre naturel (croissant) puis selon l’ordre inverse (décroissant)

    • utiliser les méthodes de classe void sort(List<?> list) et void sort(List<?> list, Comparator<? super T> c) de la classe Collections

  • Que doit-on changer pour utiliser une LinkedList à la place d’une ArrayList ?

8.5.2. Créer une liste contenant les 10 premiers entiers

List<Integer> uneListe = new ArrayList<>();
for (int i = 0; i < MAX; ++i) uneListe.add(i);

8.5.3. Mélanger aléatoirement les éléments

Collections.shuffle(uneListe);

8.5.4. Afficher la liste à l’endroit puis à l’envers

System.out.println(uneListe);
for (ListIterator<Integer> it = uneListe.listIterator(uneListe.size());
     it.hasPrevious(); ) {
    System.out.print(it.previous());
    System.out.print(", ");
}

8.5.5. Trier la liste selon l’ordre naturel (croissant) puis selon l’ordre inverse (décroissant)

Collections.sort(uneListe);
uneListe.sort((i1, i2) -> i2 - i1);
uneListe.sort(Collections.reverseOrder());

8.5.6. Que doit-on changer pour utiliser une LinkedList à la place d’une ArrayList ?

// Remplacer
List<Integer> uneListe = new ArrayList<>();
// Par
List<Integer> uneListe = new LinkedList<>();
//
// Le reste du programme ne change pas !
//

8.5.7. Exercice sur les tableaux associatifs

  • Créer un tableau associatif (HashMap) contenant les 10 premiers entiers associés à leur cube

  • Afficher les valeurs des cubes triées par ordre croissant

  • Afficher les nombres pairs et leur cube

8.5.8. Créer un tableau associatif contenant les 10 premiers entiers associés à leur cube

Map<Integer, Integer> uneMap = new HashMap<>();
for (int i = 0; i < MAX; ++i) uneMap.put(i, i * i * i);

8.5.9. Afficher les valeurs des cubes triées par ordre croissant

List<Integer> lesCubes = new ArrayList<>(uneMap.values());
Collections.sort(lesCubes);

8.5.10. Afficher les nombres pairs et leur cube

for (Map.Entry<Integer, Integer> elt : uneMap.entrySet()) {
    if (elt.getKey() % 2 == 0) {
        System.out.println(elt.getKey() + " -> " + elt.getValue());
    }
}

8.6. Exercices

8.6.1. Utilisation des collections

L’objet de cet exercice est de simuler l’interrogation d’un DNS (Domain Name Server). Un DNS convertit une adresse IP (192.168.0.1 par exemple) en un nom qualifié de machine (machine.domaine.local par exemple) et inversement. Un nom qualifié comporte le nom de la machine (avant le premier '.') et un nom de domaine (après le premier '.').

L’interface proposera une ligne de commande à partir de laquelle les commandes suivantes devront être interprétées:

  • nom.qualifié.machine : l’adresse IP de la machine est affichée;

  • adr.es.se.ip : le nom qualifié de cette machine est affiché;

  • ls [-a] domaine : la liste des machines du domaine domaine sera affichée triée selon le nom des machines ou selon les adresses IP (si -a est présent).

  • La base de données du serveur sera conservée dans un fichier texte (une ligne par machine au format “un_nom_de_machine une.adresse.IP”) chargé au lancement du programme.

  • Le nom du fichier devra être stocké dans un fichier de propriétés (cf. Tutoriel sur les propriétés).

  1. Réaliser les classes AdresseIP, NomMachine et DnsItem qui représentent respectivement une adresse IP, un nom qualifié de machine et une entrée du DNS.

  2. Réaliser la classe Dns qui proposera les opérations suivantes:

    • un constructeur qui chargera la base de données,

    • deux méthodes getItem qui retourneront une instance de DnsItem soit à partir d’une adresse IP, soit à partir d’un nom de machine,

    • une méthode getItems qui retournera une liste d’items à partir d’un nom de domaine.

  3. Réaliser la classe DnsTUI qui se chargera des interactions avec l’utilisateur. Cette classe fournira une méthode nextCommande qui analysera le texte saisi par l’utilisateur et retournera un objet implémentant l’interface Commande (cf. question suivante) et une méthode affiche qui affichera un résultat.

  4. Les commandes DNS seront implémentées à l’aide du modèle de conception Commande.

    • créer l’interface Commande comportant une seule méthode execute,

    • créer une classe implémentant cette interface pour chaque action (rechercher une IP, rechercher un nom, rechercher les machines d’un domaine, quitter l’application).

  5. Réaliser la classe principale DnsApp. La méthode run de cette classe interagira avec l’interface utilisateur pour récupérer la prochaine commande, l’exécutera puis affichera la résultat.

8.6.2. Écriture d’algorithmes

Le but de cet exercice est d’écrire des algorithmes pouvant s’appliquer à toute collection respectant l’interface List.

  1. Ecrire un algorithme bubbleSort implémentant le tri à bulle. Deux versions de l’algorithme seront proposées: la première basée sur l’interface Comparable, la deuxième sur l’interface Comparator pour les comparaisons.

  2. Ecrire un algorithme copyIf créant une copie d’une collection contenant uniquement les éléments vérifiant une condition passée en paramètre

8.6.3. Exercice de synthèse

Le but de cet exercice est de réaliser un logiciel de dessin 2D. On se limitera ici à un affichage textuel, i.e. seule une description des figures sera affichée.

Le logiciel devra offrir les fonctionnalités suivantes:

  • manipulation (affichage et déplacement) des formes comme des rectangles et des cercles,

  • regroupement d’objets afin de leur faire subir un traitement global (par exemple, déplacer ensemble un cercle et un rectangle),

  • sauvegarde/chargement d’un dessin à l’aide de la sérialisation.

  • Les erreurs devront être gérées en utilisant les exceptions,

  • les ensembles de formes avec la librairie de collections.

  1. Proposer une hiérarchie de classe modélisant ce problème et un découpage en module du logiciel

  2. Réaliser une implémentation du logiciel de dessin

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

9.1. Introduction

9.1.1. Programmation impérative

  • En programmation impérative, un programme est

    • une séquence d’instructions

    • qui modifie l’état du programme

  • Repose sur la séquence d’instructions, l'affectation, les structures de contrôle

  • Permet les effets de bord

    • modifications de zones "partagées"

  • Suit le paradigme utilisé au niveau du processeur (langage machine)

  • Basée sur la machine de Turing et l'architecture de von Neumann

  • De nombreux langages supportent ce paradigme

    • C, Java, Python, …​

9.1.2. Programmation fonctionnelle

  • En programmation fonctionnelle, un programme est

    • un ensemble de fonctions (au sens mathématique) emboîtées

    • sans effets de bord

  • Repose sur une approche déclarative, l’évaluation de fonctions

  • Basée sur le \$lambda\$-calcul

  • De plus en plus de langages supportent ce paradigme

    • Haskell, F#, ML, Clojure, Lisp, …​

    • Java, Scala, Python, …​

9.1.3. Fonctions

Fonction pure

fonction sans effet de bord

Fonction d’ordre supérieur

une fonction peut être passée en paramètre ou retournée par une fonctione

Fonction de première classe

les fonctions sont traitées comme les autres données

Fonction lambda

fonction anonyme créée "à la volée"

Fermeture

fonction lambda avec son contexte

9.1.4. Transparence référentielle

  • Remplacer une expression par sa valeur ne change pas le résultat

Le programme suivant ne respecte pas la transparence référentielle
int globalValue = 2;
int inc(int k) {
  globalValue += k;
  return globalValue;
}
int result = inc(1) + inc(1)); // result = 3 + 4 = 7
globalValue = 2;
result = 2 * inc(1); // result = 2 * 3 = 6

9.1.5. Avantages

  • Certains programmes s’expriment plus simplement

  • Permet un raisonnement sur le programme (preuve de programmes)

  • Facilite la programmation parallèle et concurrente

    • tire parti des processeurs multi-cœurs

9.2. Programmation fonctionnelle en Java

9.2.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.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 telle interface

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

9.2.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.2.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

9.2.5. Référence de méthode (exemples)

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.2.6. Parcourir une collection (de l’itératif au fonctionnel) 1/5

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.2.7. Parcourir une collection (de l’itératif au fonctionnel) 2/5

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.2.8. Parcourir une collection (de l’itératif au fonctionnel) 3/5

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

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

9.2.9. Parcourir une collection (de l’itératif au fonctionnel) 4/5

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.2.10. Parcourir une collection (de l’itératif au fonctionnel) 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);
  • Le plus lisible

9.3. La bibliothèque streams

9.3.1. 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.3.2. 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.3.3. 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

9.3.4. Un exemple de stream

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

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

9.3.5. L’interface Stream

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

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

9.3.6. Création d’un flux

  • À partir d’une collection

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

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

  • À partir d’un intervalle

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

  • À partir de valeurs

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

  • À partir des méthodes de classe de Stream

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

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

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

9.3.7. Quelques opérations intermédiaires

Opération Description

filter

retourne les éléments respectant un prédicat

map

applique une fonction à chaque élément

flatMap

désimbrique des flux

limit

tronque un flux

skip

ignore les premiers éléments

distinct

élimine les doublons (avec état)

sorted

retourne un flux trié (avec état)

9.3.8. Opérations intermédiaires (exemples)

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.3.9. Quelques opérations terminales

Opération Description

reduce

applique une réduction avec une fonction d’accumulation

count

compte les éléments

sum, …​

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

collect

réalise une réduction par modification

allMatch

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

forEach

exécute une action pour chaque élément

9.3.10. Opérations terminales (exemple) 1/3

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

System.out.println(
    numbers.stream()
           .filter(e -> e % 2 == 0)
           .map(e -> e * 2.0)
           .reduce(0.0, (carry, e) -> carry + e));
Calculer une somme avec sum et DoubleStream
System.out.println(
    numbers.stream()
           .filter(e -> e % 2 == 0)
           .mapToDouble(e -> e * 2.0)
           .sum());

9.3.11. Opérations terminales (exemple) 2/3

Les doubles des nombres pairs dans une liste avec collect
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 1, 2, 3, 4, 5);

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

9.3.12. Opérations terminales (exemple) 3/3

Un dictionnaire des personnes regroupées par nom
List<Person> persons = //...

Map<Person, List<Person>> personsByName =
    persons.stream()
           .collect(Collectors.groupingBy(Person::getName));
System.out.println(personsByName);
Un dictionnaire des noms de personnes regroupés par sexe
Map<Person.Gender, List<String>> namesByGender =
    persons.stream()
           .collect(Collectors.groupingBy(
               Person::getGender,
               Collectors.mapping(
                   Person::getName,
                   Collectors.toList())));

9.3.13. Flux infinis

  • Les méthodes de classe Stream.generate et Stream.iterate créent un flux infini

  • Ce type de flux peut exister grâce à l'évaluation paresseuse

    • l’application d’opérations élémentaires ne provoque pas la traversée du pipeline

    • seules les opérations terminales déclenchent le traitement

Un dictionnaire des noms de personnes regroupés par sexe
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.

9.4. Exercices

9.4.1. Requêtes en utilisant les streams

Le but de cette exercice est d’exprimer un ensemble de requêtes en utilisant la bibliothèque stream.

Un employé possède les attributs suivants : nom (String), age (int), sexe (enum), salaire (BigDecimal), date d’embauche (LocalDate) et service de rattachement (Service). Un service comporte un nom (String) et une adresse (String).

  1. Implémentez la classe Employe et l’énumération Service

  2. Créez un jeu de données de test

  3. Implémentez les requêtes suivantes (faites afficher le résultat)

    1. les employés (avec toutes leurs caractéristiques)

    2. les employés de moins de 30 ans

    3. le nom des hommes

    4. le nom et le salaire trié par salaire décroissant

    5. la moyenne des salaires

    6. les employés regroupés selon leur sexe

    7. la moyenne des salaires par sexe

    8. le nom et la date d’embauche par services

10. Conclusion

10.1. Bilan

10.1.1. Principaux concepts abordés

  • Objet et messages

  • Type et classe, métaclasse, généricité

  • Sous-type, héritage, polymorphisme, classe abstraite, héritage multiple et à répétition

  • Relations entre classes (dépendance, association/agrégation/composition, héritage)

  • Modules

10.1.2. Autres notions abordées

  • Gestion d’erreurs et exceptions

  • Entrées/sorties et persistance

  • Gestion des collections

  • Bibliothèques stream et notion de programmation fonctionnelle

10.2. Conception orientée-objet

10.2.1. Qu’est-ce que la conception orientée-objet

  • Lors de son exécution, un système OO est un ensemble d’objets qui interagissent

  • La conception orientée-objet (COO) consiste donc à créer un modèle qui respecte les concepts objet

10.2.2. Difficultés de la conception orientée-objet

  • Les concepts objets sont nombreux et complexes (attribut, méthode, objet, classe, héritage, …​)

    • ⇒ plusieurs solutions sont en général envisageables

  • Identifier la bonne solution est difficile

  • Plusieurs symptômes voire métriques de qualité peuvent guider les choix

  • Programmer en Java ou en C# n’est pas concevoir objet !

  • Seule une analyse objet conduit à une solution objet, i.e. qui respecte les concepts objet

  • Le langage de programmation est un moyen d’implémentation qui ne garantit pas le respect des concepts objet

10.2.3. Principes, patterns et idiomes

  • Un principe donne des directives générales

    • souvent indépendant des paradigmes et des langages

    • par exemple KISS, YAGNI, DRY, Law of Demeter/Tell, don’t ask, …​

  • Un pattern propose une solution reconnue à un problème récurrent

    • en général indépendant des langages

    • par exemple le design pattern Composite

  • Un idiome est une construction spécifique d’un langage pour implémenter une situation courante

    • uneListe.forEach(System.out::println); (afficher une liste en Java 8)

10.2.4. KISS (Keep It Simple, Stupid)

  • "Simplicity should be a key goal in design and unnecessary complexity should be avoided", Kelly Johnson, ingénieur chez Lockheed

  • Le code le plus simple est aussi le plus simple à maintenir

10.2.5. YAGNI (You Aren’t Gonna Need It)

  • "Do the Simplest Thing That Could Possibly Work"

  • Ne pas ajouter une fonctionnalité avant que cela soit nécessaire

  • Principe issu d’eXtreme Programming

  • Nécessite de s’appuyer sur du refactoring pour être efficace

10.2.6. DRY (Don’t Repeat Yourself)

  • "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system", The Pragmatic Programmer, Andrew Hunt and David Thomas

  • Chaque fonctionnalité doit être réalisée à un seul endroit du code

  • Une modification d’un élément ne nécessite pas de changer un autre élément non relié logiquement

10.2.7. Law of Demeter

Law of Demeter or principle of least knowledge
  • Each unit should have only limited knowledge about other units: only units "closely" related to the current unit.

  • Each unit should only talk to its friends; don’t talk to strangers.

  • Only talk to your immediate friends.

  • Une méthode d’un objet devrait invoquer uniquement les méthodes de

    • l’objet lui-même,

    • ses paramètres,

    • tout objet qu’elle instancie,

    • ses composants directs.

  • Un objet connaît le minimum de la structure de ses voisins

  • Cela limite les dépendances avec les autres objets

10.2.8. Exemple de la Loi de Demeter 1/5

Un client paye (version incorrecte)
Wallet theWallet = theCustomer.getWallet();
double totalMoney = theWallet.getTotalMoney();
if (totalMoney > AMOUNT_TO_PAY_IN_EUROS) {
    theWallet.subtractMoney(AMOUNT_TO_PAY_IN_EUROS);
}
  • Cet exemple est extrait de The Paperboy, The Wallet, and The Law Of Demeter, David Bock

  • Le créancier connaît la structure du client et manipule lui-même le portefeuille

  • Il dispose de plus d’informations que nécessaire

  • La validité du portefeuille n’est pas garantie

10.2.9. Exemple de la Loi de Demeter 2/5

La classe Customer (version incorrecte)
public class Customer {
  private String name;
  private Wallet wallet;

  public Customer(String name, double fortune) {
    this.name = name;
    wallet = new Wallet(fortune);
  }

  public Wallet getWallet() {
    return wallet;
  }
}

10.2.10. Exemple de la Loi de Demeter 3/5

La classe Wallet (version incorrecte)
public class Wallet {
  private double totalMoney;

  public Wallet(double fortune) {
    totalMoney = fortune;
  }

  public double getTotalMoney() {
    return totalMoney;
  }

  public void subtractMoney(double amountToPayInEuros) {
    totalMoney -= amountToPayInEuros;
  }
}

10.2.11. Exemple de la Loi de Demeter 4/5

Un client paye (version corrigée)
double paidAmount = theCustomer.getPayment(AMOUNT_TO_PAY_IN_EUROS);
  • Le créancier doit demander le paiement

  • La classe Wallet est isolée (diminue le couplage)

  • La méthode getPayment encapsule la logique du paiement (améliore la cohésion)

  • La classe Customer est plus complexe

    • mais la complexité a été transférée depuis le code de l’application

10.2.12. Exemple de la Loi de Demeter 5/5

La classe Customer (version corrigée)
public class Customer {
  private String name;
  private Wallet wallet;

  public Customer(String name, double fortune) {
    this.name = name;
    wallet = new Wallet(fortune);
  }

  public double getPayment(double amountToPayInEuros) {
    double totalMoney = wallet.getTotalMoney();
    double paidAmount = 0.0;
    if (totalMoney > amountToPayInEuros) {
      paidAmount = amountToPayInEuros;
      wallet.subtractMoney(paidAmount);
    }
   return paidAmount;
  }
}
  • La classe Customer ne publie plus la méthode getWallet()

10.3. Pour aller plus loin…​

10.3.1. Lisez !

pour les langages, attention aux versions (Java >= 8)
Ayez l’esprit critique (toutes les pages ne se valent pas)

10.3.2. Pratiquez !

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

  • …​