Objectifs
-
Structures de données « dynamiques ».
-
Listes, tables de hachage, arbres…
- Et leurs algorithmes traditionnels, (recherches par exemple).
- Perfectionnement en programmation.
-
Programmer c’est comprendre (vraiment).
- Programmation des structures de données récursives.
- Choix des structures de données, (exemples « realistes »)
Organisation du cours
-
9 blocs, soit 9 vendredis.
-
Le matin, amphi, de 10h30 à 12h00.
- L’après-midi, TP.
- Évaluation.
-
TP noté, — le cinquième, 9 Dec, non le 7 Dec.
- Contrôle classant, à la fin, 25 Jan.
- Note de module :
Quelques éléments de Java
-
Qu’est-ce au juste qu’une valeur ?
- Qu’est-ce au juste qu’une variable ?
-
Création et initialisation.
- Appel de méthode
- Portée
- Allocation dynamique
-
Les tableaux.
- Les paires.
- Les listes.
Les valeurs
-
Les scalaires.
-
Les booléens :
boolean
(true
et false
).
- Les entiers :
byte
(modulo 28),
char
et short
(modulo 216), int
(modulo 232),
long
(modulo 264).
- Les flottants :
float
(simple précision), double
(double précision).
- Les objets : tout le reste ! Les objets appartiennent à des
classes qui sont plus ou moins leur type.
-
Les un peu spéciaux :
String
, les tableaux… - Les objets de classses de la librairie : par ex.
System.out
de la classe PrintStreamWriter
.
- Ceux que l’on fait soi-même (en définissant leur classe et par
new
).
Déclaration: avec et sans initialisation
int x = 1 ;
-
«
int x
»
est la déclaration : une variable (qui contient un int
) est créée.
- «
= 1
» est l’initialisation : la valeur initiale de la
variable est 1
.
Ne pas confondre déclaration avec initialisation et
affectation. Même si la syntaxe est très semblable.
int x = 1 ; // Création avec initialisation.
x = 2 ; // Changer le contenu
int y ; // Création tout court
Remarque : Quelle est la valeur initiale de la variable y
?
On ne peut pas le savoir !
Variables locales
Les variables locales sont :
-
Déclarées dans le corps des méthodes,
- Ou bien ce sont les paramètres des méthodes.
Le compilateur vérifie « l’initialisation » des variables locales.
class T {
static int f(int x) {
int y ;
if (x != 0) y = 1 ;
return x + y ;
}
}
# javac T.java
T.java:5: variable y might not have been initialized
Les variables sont « des cases »
Deux déclarations de variable.
int x=1, y ; | |
Une affectation.
y = x ; | |
On remarque que :
-
À gauche du
=
, une variable → une case.
- À droite du
=
, une variable → le contenu
d’une case.
Appel de méthode
Le principe est le suivant :
-
Chaque appel de methode crée ses propres variables.
- Les variables sont initialisés par l’appel.
static int f(int x) {
x = x + 2 ;
return x ;
}
public static void main (String [] arg) {
int x = 2 ;
int r = f(x) ;
System.out.println("f(" + x + ") = " + r) ;
}
Résultat : f(2) = 4
Avec des boites
static int f(int x) {
x = x + 2 ;
return x ;
}
Au début de l’appel de méthode :
Juste avant le return
:
Portée
La portée d’une variable est la zone du code où on peut l’utiliser,
autrement dit, la zone de visibilité.
Dans l’exemple précédent, les portées des deux
variables x étaient clairement limitées aux corps
des méthodes main
et f
.
La portée est structurée selon les blocs, en gros délimités par
{
…}
.
Quelques règles importantes :
-
La portée s’étend de la déclaration de la variable à la fin
du bloc de déclaration.
- Dans un même bloc, il ne peut pas y avoir deux variables de même
nom.
- L’usage d’une variable fait référence à la déclaration la plus
proche vers le haut du programme (liaison lexicale).
Exemple de portée I
Que penser de :
static int f() {
for (int i = 0 ; i < 10 ; i++) { … }
return i ;
}
Ce code est incorrect, car
dans l’instruction return i
, la variable i
ne fait
référence à aucune déclaration visible.
Note :
On peu considérer que la boucle for
est plus ou moins
équivalente à :
{ int i = 0 ; // NB. un bloc est ouvert
while (i < 10) { …
i++ ;
}
}
Portée, II
Que se passe-t-il si on appelle f(4)
?
static int f(int x) {
{ int x = 2 ;
System.out.print("x = " + x) ;
}
System.out.println(", x = " + x) ;
}
Ce programme est correct. Il affiche :
x = 2, x = 4
Il y a en fait deux variables,
x et x.
On peut très bien changer x (ou
x) en y,
sans rien changer au fond.
Les variables (locales) sont un peu comme les variables muettes des
mathématiques.
Un tableau avec les boites
Un tableau de taille n est (plus ou moins) une grosse case
composée de n cases « normales ».
int [] t = new int[6] ;
Mais la valeur du tableau est une flèche (référence, pointeur,
adresse) vers la grosse case.
Note : Les cases du tableau sont initialisés par défaut.
Valeur par défaut
Les cases des tableaux ont une valeur par défaut.
-
Scalaires : une espèce de zéro.
-
boolean
: c’est… false
.
- Autres scalaires : c’est bien zéro.
- Objets :
null
.
Notons que null
est un objet un peu spécial.
Il appartient à toutes les classes, mais on ne peut rien faire avec,
sauf :
-
L’affecter, par ex.
String s = null
- Le comparer, par ex.
if (s != null)
…
La deuxième dimension
Une matrice à 3 lignes et 2 colonnes.
int [] [] t = new int [3][2] ;
Est en fait un tableau de trois tableaux de deux entiers.
Et null dans tout ça ?
Logiquement, les tableaux sont initialisés par défaut à null
.
int [] t // = null ;
La valeur null
remplace une flèche, mais ne pointe nulle part.
L’initialisation par défaut vaut aussi pour les cases de tableau.
int [][] t = new int[3][] ; // tableau de tableaux
Soit : un tableau de trois null
.
Les tableaux sont alloués « dynamiquement »
Cela veut dire que leur place est calculée/allouée lors de
l’exécution.
static int [] f(int n, int x0) {
int [] r = new int [n] ;
for (int i = 0 ; i < n ; i++) { r[i] = x0 ; }
return r ;
}
En outre, l’allocation est effectuée dans le « tas ».
-
Le tableau existe encore après le retour de la méthode
f
(contrairement à la variable r
).
- Il n’y a pas lieu de libérer explicitement
la place occupée lorsque le tableau devient inutile (GC).
Les valeurs des tableaux sont des flèches
int [] t = {1, 2, 3, 4, 5, 6} ; // Initialisation explicite
int [] r = t ;
Dans r = t
c’est la flèche qui est copiée.
De sorte que ici t[2]
et r[2]
désignent la même case
(la troisième).
t[2] = 0 ; // ranger 0 dans la case désignée
System.out.println(r[2]) ; // Affiche '0'
À comparer avec
La copie du contenu de variables de type scalaire.
int x = 3, y = x ;
x = 0 ; System.out.println(y) ; // Affiche '3'
Car ici x
et y
sont deux cases différentes.
Appel de méthode
Les valeurs sont bien copiées, mais ce sont des flèches.
static void f(int [] r) { r[0] = 0 ; }
int [] t = { 1, 2 } ;
System.out.println(t[0]) ; // Affiche '1'
f(t) ;
System.out.println(t[0]) ; // Affiche '0'
Égalité « physique »
L’égalité ==
est l’égalité des valeurs.
-
Pour les scalaires aucun problème !
- Pour les objets, attention !
Les valeurs sont les flèches.
int [] t = {1, 2, 3} , r = {1, 2, 3} ; // Deux variables.
int [] u = t ;
System.out.println((t == r) + ", " + (t == u)) ;
Le résultat est false, true
, car
Égalité « structurelle »
Pour savoir si deux tableaux sont identiques (et non plus exactement
le même). On utilise
o1.equals(o2)
.
int [] t = {1, 2, 3} ;
boolean b = t.equals(new int [] {1, 2, 3}) // tableau « anonyme »
System.out.println(b) ;
Affiche true
.
Résumé :
-
L’égalité physique
==
expose l’implémentation.
- L’égalité structurelle est plus « mathématique ».
Les chaînes sont des objets…
Mais des objets un peu particuliers.
class Coucou {
public static void main(String [] arg) {
System.out.println("coucou" == "coucou") ;
System.out.println(arg[0] == arg[1]) ;
}
}
La commande « java Coucou coucou coucou
» affichera :
true
false
Un conseil donc, utiliser equals, même sur les chaînes.
Affichage
Nous savons afficher des scalaires et des chaînes.
System.out.println(1 + " " + true + …) ;
Mais savons nous afficher des « objets » ?
int [] t = {1, 2} ;
System.out.println(t) ;
Affiche : [I@a16869
, c’est la flèche (l’adresse) qui est
affichée !
Pour afficher un tableau, il faut écrire une boucle :
static void print(PrintStream out, int [] t) {
for (int i = 0 ; i < t.length ; i++) {
out.print(" " + t[i]) ;
}
} // Appel : print(System.err, new [] {1, 2}) ;
Fabriquons nos propres structures de données
Par exemple une paire (de deux int
):
class Pair {
int x, y ; // Variables d'instance ou champs
Pair (int x0, int y0) { // Constructeur
x = x0 ; y = y0 ;
}
}
La classe ci dessus est un patron pour créer des objets, par :
Pair p = new Pair (1, 2) ;
Important : L’ecriture avec constructeur est meilleure que :
Pair p = new Pair () ; // Constructeur par défaut
p.x = 1 ; p.y = 2 ;
Les valeurs des objets sont des références
Et tout ce qui s’appliquait au tableaux reste vrai.
Pair p = new Pair (1, 2) ;
Pair q = p ;
q.x = 0 ;
System.out.println(p.x) ; // Affiche '0'
| ⇒ | |
Comment afficher les paires
Ou début de programmation objet.
-
Si
out
est un PrintStream
,
et o un objet, le code out.print(o)
,
revient à afficher la chaîne renvoyée par l’appel de méthode
o.toString()
.
- Par défaut,
o.toString()
affiche la valeur de o (une flèche).
out.print(new Pair(1,2))
⇒ Pair@6f0472
- Mais on peut redéfinir la méthode
toString
(dans la classe Pair
).
public String toString() { // pas de static
return "[x=" + x + ",y=" + y + "]" ;
}
Dès lors,
out.print(new Pair(1, 2))
⇒ [x=1,y=2]
Résumé sur les classes
Une classe C « pour faire des objets o » comprend :
-
Un (ou des ou pas du tout) constructeur homonyme au nom de la
classe. On crée un objet o par :
new C(…)
- Des (ou une ou pas du tout) variables x, que l’on réfère
par :
o.x
- Des (ou une ou pas du tout) méthodes f, que l’on appelle par :
o.f(…)
Plus compliqué :
-
Pour le moment :
static
nulle part.
- L’objet peut posseder des méthodes pré-existantes, qui peuvent
être redéfinies.
Surcharge (Overloading)
Il est possible de définir plusieurs constructeurs,
à condition que leurs arguments soient de types
distincts.
class Pair {
int x, y ;
Pair() { // Constructeur de l'origine
x = 0 ; y = 0 ; // Autant insister (init. par défaut)
}
Pair (int x0, int y0) {
x = x0 ; y = y0 ;
}
}
-
Cela fonctionne aussi pour les méthodes.
- Tout ce passe comme si le type des arguments faisait partie du
nom des constructeurs/méthodes.
Visibilité des variables d’instance
La règle des blocs semble rester valable :
class Pair {
int x, y ; // Déclaration
Pair (int x0, int y0) {
x = x0 ; y = y0 ;
}
public String toString() {
return "(" + x + ", " + y + ")" ;
}
}
Visibilité des variables d’instance II
Mais en fait, c’est un peu différent, la portée des variables
d’instance s’étend sur l’ensemble de l’objet.
class Pair {
Pair (int x0, int y0) {
x = x0 ; y = y0 ;
}
…
int x, y ; // Déclaration
}
En outre, on a aussi droit à p.x
à peu près n’importe où…
(à condition de ne pas spécifier private int x
)
Visibilité des variables d’instance III
En fait la notation des variables d’instance comme
des variables normales est une commodité.
La référence x dans le constructeur ou dans une
méthode est une abréviation pour this.x
.
Où this
est l’objet construit ou dont on a appelé la méthode.
Cela permet le style :
class Pair {
int x, y ; // Déclaration
Pair (int x, int y) {
this.x = x ; this.y = y ;
}
…
}
La liste
Le tableau même dynamique est encore un peu rigide.
-
Typiquement, il faut connaître la taille du tableau à l’avance.
Mais considérons une « liste » de courses :
Une analogie plus exacte, est un « carnet de courses » : un article
par page !
Soyons plus précis
Une liste est :
-
Soit vide,
- Soit un premier article et le reste de la liste.
Le plus simple est de procéder ainsi :
List moi = new List("patates", new List("thon", null)) ;
List elle = new List("laitue", new List("boulgour", null)) ;
La liste dans la mémoire
Une valeur de type List
est
-
Une référence vers une cellule de
List
(pointeur, addresse).
- Ou bien la référence
null
qui ne pointe nulle part.
Une cellule de liste comporte :
-
L’élément en tête de liste (champ
val
).
- Et un le reste de la liste (champ
next
de type List
).
List moi = new List("patates", new List("thon", null)) ;
Intermède
On veut afficher les listes : peut-on y arriver en redéfinissant
toString
?
Non ! Pourquoi ?
Parce que null
est une liste valide, et que null.toString()
ne fonctionne pas.
List pasFaim = null ;
System.out.println(pasFaim) ;
Affiche null, car la méthode println
est du genre.
void println(… p) {
if (p == null) {
printString("null") ;
} else {
printString(p.toString()) ;
}
}
Afficher une liste
Il faut employer une méthode statique.
static void print(List p) {
for (List q = p ; q != null ; q = q.next) {
System.out.println(q.val) ;
}
}
Remarques Ce genre de boucle for
est la façon
« idiomatique » de parcourir une liste.
Ici, la variable q
n’est pas utile :
static void print(List p) {
for ( ; p != null ; p = p.next) {
System.out.println(p.val) ;
}
}
(Car on modifie p
, variable locale, pas la liste).
Quoi de neuf ?
C’est comme la paire, sauf…
class List {
…
List next ;
}
… que la liste est une structure de donnée récursive.
-
Les listes de courses sont solution de l’équation :
- La programation sur les listes est « naturellement » récursive.
Programmation naturellement récursive
Fabriquer une nouvelle liste de courses avec deux listes de courses.
List courses = List.append(moi, elle) ;
On peut exprimer append par des équations (récursives), en
notant ∅ la liste vide et (a; L) une liste non-vide.
A(∅,M) | = | M |
A((a; L), M) | = | a; A(L, M) |
|
static List append(List p, List q) {
if (p == null) {
return q ;
} else {
return new List(p.val, append(p.next, q)) ;
}
}
Premier appel
Deuxième appel
Troisième appel
Retour du troisième appel
Retour du deuxième appel
Retour du premier appel
Situation finale
List courses = List.append(elle, moi) ;
La liste courses
est"laitue"
; "blougour"
;
"patates"
; "thon"
Important : La liste elle
n’a pas changé.
Structure persistante.
Moins important : Les cellules de elle
sont copiées.
Et si je change d’avis
moi.val = "carottes" ;
Situation :
Conclusion : moi
devient
"carotte"
; "thon"
et courses
devient "laitue"
; "boulgour"
;
"carotte"
; "thon"
.
Et si elle change d’avis ?
elle.val = "scarole" ;
Situation :
Conclusion : courses
ne change pas.
Conclusion
La mutation est incompatible avec les structure persistantes.
J’aime les chose délicates
Concaténer en place
La fonction nappend concatène les listes « en place »
Elle s’écrit facilement,
en remplaçant les allocations de cellules
de append par des mutations du champ next.
static List nappend(List xs, List ys) {
if (xs == null) {
return ys ;
} else {
xs.next = nappend(xs.next, ys) ;
return xs ;
}
}
Premier appel
Deuxième appel
Troisième appel
Retour du troisième appel
Retour du deuxième appel
Retour du premier appel
Résultat (des courses en place)
Danger principal
La liste pointée par moi a « changé ».
Car une de ses cellules a été « mutée ».
Un second problème
Écrivons
List courses = List.nappend(moi, moi) ;
-
Une liste circulaire,
l’interprétation récursive devient douteuse (plus de
null
).
- Les structures mutables sont délicates.
Programation itérative
Deux façons d’aborder la programmation.
-
« Qu’est-ce que je veux calculer ? ».
-
Conception plus simple, et plus sûre.
- On néglige le coût en temps calcul à l’exécution (pas toujours
un problème).
- « Comment je calcule ? ».
-
Conception plus délicate, car forcément plus détaillée.
- Maitrîse du coût.
En gros, programmation récursive vs. programmation avec des boucles.
Programmation itérative
Comment nappend
fonctionne-elle ?
-
Trouver la dernière cellule de
xs
.
- Faire pointer son champ
next
vers ys
.
- Ne pas oublier la liste vide.
static List nappend2(List xs, List ys) {
if (xs == null) return ys ;
List zs = xs ;
for ( ; zs.next != null ; zs = zs.next) ;
zs.next = ys ;
return xs ;
}
Et sans modifier la liste d’origine ?
Comment obtenir une concaténation itérative et
non-destructive simplement :
static List append2(List xs, List ys) {
List zs = copy(xs) ; // Copier xs qui va être modifiée
return nappend2(zs, yz) ;
}
(En pratique, on ne devrait presque jamais écrire un truc pareil. Si le besoin
s’en fait sentir, penser aux listes persistentes.)
En trichant
Il est facile d’écrire un copy
récursif.
List copy(List xs) {
if (xs == null) {
return null ;
} else {
return new List (xs.val, copy(xs.next)) ;
}
}
Ne pas tricher c’est écrire copy
en style itératif.
Qu’est-ce que ça fait ?
-
Parcourir la liste
xs
,
- En copiant ses cellules dans une nouvelle liste.
Premier essai
-
Parcourir la liste
xs
- En la recopiant.
- Ne pas oublier la liste vide (c’est bon !).
static List copy2(List xs) {
List r = null ;
for ( ; xs != null ; xs = xs.next) {
r = new List (xs.val, r) ;
}
return r ;
}
Première itération
Exécution du corps
Deuxième itération (fin)
Troisième itération (fin)
Et c’est fini
Copier dans l’ordre
Copier dans l’ordre
Programmation itérative de copy
-
Il faut ajouter la nouvelle cellule à la fin
de la liste résultat (variable
lst
— dernière cellule).
- Et il faut que cela soit vrai tout le temps,
y compris au début.
- Et attention à la liste vide
static List copy2(List xs) {
if (xs == null) return ys ;
List r = new List (xs.val,null) ;
List lst = r ;
for ( xs = xs.next ; xs != null ; xs = xs.next) {
lst.next = new List (xs.val, null) ;
lst = lst.next ;
}
return r ;
}
Programmation itérative de append
Comment append
fonctionne-elle ?
-
Copier (la liste référencée par)
xs
dans l’ordre.
- En remplaçant le
null
de la fin par ys
.
static List append2(List xs, List ys) {
if (xs == null) return ys ;
List r = new List (xs.val,null) ;
List lst = r ;
for ( xs = xs.next ; xs.next != null ; xs = xs.next) {
lst.next = new List (xs.val, null) ;
lst = lst.next ;
}
lst.next = ys ;
return r ;
}
Les structures de données dynamiques
Nature des structures de données :
-
Persistantes (ou fonctionelles, ou non-mutable).
- Mutables (ou impératives, ou en place).
Le choix persistant/mutable est un choix de fonctionalité. Il doit
être conscient et influence l’ensemble du programme.
Style de programmation :
-
Récursif.
- Itératif (avec des boucles).
Le choix récursif/itératif est un choix d’efficacité,
compromis facilité d’écriture/efficacité du programme.
Résumé
On peut définir quatre catégories.
Selon la nature des structures de données et le style de programmation.
| Persistant | Mutable |
Récursif | apppend | nappend |
Itératif | append2 | append2 |
-
Simplicité de conception : recursif et persistent.
- Itératif souvent plus efficace (mais est-ce bien utile ?),
parfois naturel (inverser une liste).
- Structures mutables : parfois plus efficace (dépend de l’efficacité
de GC), parfois voulu.
Ce document a été traduit de LATEX par HEVEA