Archives de l’auteur : pedago

Programmation par contraintes : partie C

Programmation par contraintes

En M2 Bases de données et IA et M2 Image et IA

Par Olivier Bailleux, Maître de conférences HDR, Université de bourgogne

Partie C : Le filtrage par cohérence de domaines

Objectif

Être capable de restaurer la cohérence de domaines sur un réseau de contraintes. Comprendre en profondeur en quoi consiste la restauration de cohérence de domaines sur une seule contrainte et sur un réseau de contraintes.

Prérequis

Partie A.

C1 : Cohérence de domaine

Ce que nous allons voir ici est une forme simple de raisonnement automatique permettant de réaliser des déductions dont les conclusions consistent à retirer des valeurs des domaines de certaines variables pour simplifier un réseau de contraintes. Le système déductif utilisé est élémentaire. Il travaille avec une seule contrainte à la fois. Les informations exploitées pour faire la déduction sont une contrainte et les contenus des domaines des variables reliées par cette contrainte. Le résultat de la déduction, i.e., sa conclusion sont les contenus des mêmes domaines dans lesquels certaines valeurs ont été (si applicable) retirées.

image1

Notion de support

Soit q une contrainte, V(q) l’ensemble des variables reliées par q, v une variable appartenant à V(q) et x un élément du domaine de v. On appelle support de l’assignation v=x toute assignation complète de V(q) qui étend {v=x} et satisfait q.

Dans l’exemple ci-dessous, A=2 a un seul support : {A=2, B=1, C=3, D=4}.

image2

Cohérence de domaine

Dans un réseau de contraintes, la cohérence de domaines est respectée si et seulement si pour toute variable v et toute valeur x du domaine de v, v=x a un support pour toutes les contraintes dans lesquelles la variable v intervient.

Par exemple, le réseau constitué de la seule contrainte ci-dessous ne respecte pas la cohérence de domaine car C=1 n’a pas de support.

image3

Si on assigne la valeur 1 à C, alors quelles que soient les valeurs des autres variables, la contrainte ne peut être satisfaite.

Exercices de découverte

C-dec-10

Soit la contrainte $A < B$ où les variables $A$ et $B$ ont toutes les deux pour domaine ${1,2,3}$. Quels sont les supports de $A=1$ ?

"Solution"

${A=1,B=2}$ et ${A=1,B=3}$.

C-dec-10

Soit la contrainte $A < B$ où les variables $A$ et $B$ ont toutes les deux pour domaine ${1,2,3}$. Y a-t-il une valeur du domaine de A n’ayant aucun support pour cette contrainte ? Si oui laquelle et pourquoi ?

"Solution"

$A = 3$. Parce que si $A$ vaut 3, il n’existe aucune valeur dans le domaine de $B$ permettant de satisfaire la contrainte.

C2 : Restaurer la cohérence de domaine

Restaurer la cohérence de domaines consiste à retirer de chaque domaine toutes les valeurs qui n’ont pas de support pour au moins une contrainte du réseau. Par exemple, dans le réseau ci-dessous qui ne comporte qu’une seule contrainte, C=1, C=2, D=1, D=2, D=3 n’ont pas de support.

image4

Vous pouvez vérifier que dans aucune des assignations complètes de A, B, C, D permettant de satisfaire la contrainte alldiff, on a C=1 ou C=2 ou D=1 ou D=2 ou D=3. Donc ces valeurs peuvent être retirées des domaines concernés et on obtient le résultat suivant.

image5

La contrainte alldiff est « filtrée », et comme c’est la seule contrainte du réseau, le réseau respecte maintenant la cohérence de domaines : chaque valeur de chaque domaine de chaque variable a un support pour chaque contrainte concernée par cette variable.


Voici un autre exemple. Nous allons restaurer la cohérence de domaine sur un réseau de contraintes représenté sous forme énumérative. Comme toutes les contraintes sont binaires, c’est-à-dire relient deux variables, le réseau peut se représenter graphiquement, ce qui permet de visualiser plus facilement les étapes. Voici le réseau initial.

image6

Voici les différentes étapes du filtrage.

image7

image8

image9

image10

Ici, le filtrage s’est terminé lorsqu’un domaine est devenu vide. Lorsque cela arrive, on peut conclure que le réseau de départ était incohérent. Mais la réciproque n’est pas vraie. Restaurer la cohérence de domaines sur un réseau incohérent n’aboutit pas toujours à vider un domaine. Le filtrage est une simplification d’un réseau de contraintes, mais ne suffit pas toujours à déterminer si ce réseau est incohérent ni à trouver une solution.


Comment restaurer la cohérence de domaines ?

Pour filtrer à la main un réseau de contraintes, il faut « visiter » toutes ses contraintes, une à la fois, puis visiter chaque domaine des variables reliées par cette contrainte, un à la fois, puis visiter chaque valeur du domaine visité et regarder si cette valeur a au moins un support pour la contrainte concernée. Si la réponse est non, on retire la valeur du domaine, sinon on laisse la valeur dans le domaine.

Il est parfois nécessaire de visiter plusieurs fois certaines contraintes et certains domaines car le fait de retirer des valeurs d’un domaine peut faire disparaitre des supports pour des contraintes reliées à la variable concernée, ce qui peut permettre de nouvelles réductions de domaine qui n’étaient pas possibles au départ. On arrête le processus lorsque pour chaque contrainte, toutes les valeurs de tous les domaines des variables concernées ont des supports. La bonne nouvelle, c’est que le résultat est le même quel que soit l’ordre dans lequel on visite les contraintes et les domaines.

Exercices de découverte

C-dec-20

Soit le réseau constitué d’une seule contrainte $A < B$ où les variables $A$ et $B$ ont toutes les deux pour domaine ${1,2,3}$. Restaurez la cohérence de domaine.

"Solution"

image-20210914115037182

C-dec-21

Soit le réseau constitué des deux contraintes $A < B$ et $B < C$ où les variables $A$, $B$ et $C$ ont toutes pour domaine ${1,2,3}$. Restaurez la cohérence de domaine.

"Solution"

Voici trois étapes permettant de réaliser complètement le filtrage.

image-20210914120351352

On filtre d’abord la première contrainte exactement comme dans l’exercice précédent. Ensuite on filtre la deuxième, et on constate que le filtrage de la première a retiré le support de $C=2$ qui n’aurait pas été filtré si la contrainte avait été traitée en premier (mais qui aurait fini par être retiré plus tard). Enfin, on revient sur la première contrainte pour constaté que $A=2$ n’a plus de support et doit donc être viré.

C-dec-22

Soit le réseau constitué des trois contraintes $A \neq B$, $B \neq C$ et $A \neq C$ où les variables $A$, $B$ et $C$ ont toutes pour domaine ${1,2}$. Restaurez la cohérence de domaine.

image-20210914121938704

"Solution"

Aucune valeur ne doit être retirée. Le réseau est déjà domaine cohérent. En effet, la restauration de la cohérence de domaine se fait en examinant chaque contrainte séparément. Et si on regarde chaque contrainte séparément, on constante que toutes les valeurs de son domaine ont un support. Évidemment, notre cerveau voit immédiatement qu’il n’y a pas de solution, mais ce n’est pas ce que demande l’énoncé de l’exercice ni ce que ferait un algorithme de restauration de cohérence de domaine.

Rappelez-vous-en le jour de l’examen pour ne pas risquer un hors sujet.

C3 : Quelques algorithmes de restauration de DC- cohérence

Les connaissances présentées dans cette section ne seront pas évaluées.

De nombreux algorithmes de restauration de cohérence de domaines ont été publiés depuis les années 1970. Avant de donner quelques exemples, voici un rappel des notations utilisées pour parler de complexité algorithmique :

Il y a trois notations importantes pour parler de complexité algorithmique :

  • $f(x) = O(g(x))$ signifie qu’il existe une constante $K$ telle que, au delà d’une certaine valeur de $x$, $f(x)$ est majorée par $K g(x)$.
  • $f(x) = \Omega(g(x))$ signifie qu’il existe une constante $K$ telle que, au delà d’une certaine valeur de $x$, $f(x)$ est minorée par $K g(x)$.
  • $f(x) = \Theta(g(x))$ signifie que $f(x) = O(g(x))$ et $f(x) = \Omega(g(x))$.

Dans la suite, nous parlerons de complexité en temps dans le pire des cas des algorithmes présentés. Pour bien comprendre cette notion, vous pouvez vous référer à cette vidéo.

Algorithme naïf AC1

On réitère une procédure de filtrage tant que cette procédure retire au moins une valeur d’au moins un domaine. La procédure est la suivante : pour chaque variable V et chaque contrainte q concernée par V, on retire du domaine de V toutes les valeurs qui n’ont pas de support pour q.

Dans le cas où les contraintes sont binaires (i.e. relient 2 variables) et sont définies en extension, en notant $n$ le nombre de variables, $c$ le nombre de contraintes et $d$ la taille du plus grand domaine, cet algorithme a une complexité temporelle $\Theta(n c d^3)$.

Pourquoi cette complexité ? Il faut jusqu’à $d^2$ itérations pour rechercher toutes les valeurs d’un domaine qui n’ont pas de support pour une contrainte particulière. La procédure de filtrage, qui doit faire cela deux fois par contrainte, a donc une complexité $\Theta(c d^2)$. Comme cette procédure peut n’enlever qu’une seule des $d$ valeurs initiales dans un seul des $n$ domaines, il faut dans certains cas l’appeler $n d$ fois, d’où une complexité en $\Theta(n c d^3)$.

Algorithme AC3 (1977)

On maintient dans un ensemble T les couples (Variable, Contrainte) à traiter. Cet ensemble contient au départ tous les couples (v, q) tels que v est une des variables reliées par q. Tant que cet ensemble n’est pas vide, on réitère les opérations suivantes :

  1. Retirer un couple (v, q) de l’ensemble T.

  2. Filtrer (v, q) en retirant du domaine de v les valeurs non supportées par q.

  3. Si au moins une valeur a été supprimée du domaine de v, alors ajouter dans T tous les couples (u, p) tels que p est une contrainte différente de q, reliant les variables u et v.

Dans le cas d’un réseau de contraintes binaires spécifiées en extension, la complexité temporelle dans le pire des cas d’AC3 est $\Theta(c d^3)$.

Pourquoi cette complexité ? Chaque contrainte est examinée au plus $d$ fois, il y a donc au plus $\text{c~d}$ filtrages de contrainte dont chacun coûte, dans le pire des cas, $\Theta\left( d^{2} \right)$ opérations.

Bref panorama des algorithmes de filtrage pour contraintes binaires

Cette liste d’algorithmes a pour seul vocation de montrer le nombre des contributions et d’améliorations apportées en matière de filtrage de contraintes binaires spécifiées sous forme énumérative : AC4 (1984), AC6(1994), AC7 (1999), AC2001 (2001), AC3d (2002).

AC2001, aussi appelé AC3.1, est une amélioration tardive (21 ans plus tard) de AC3 qui a une complexité temporelle dans le pire des cas $\Theta( c d^{2} )$, et il est impossible de faire mieux. Sa complexité spatiale dans le pire des cas est $\Theta(c d)$.

Cas de la contrainte alldiff

Un algorithme célèbre datant de 1996 (Jean-Charles Regin, [Generalized arc consistency for global cardinality constraint](https://www.researchgate.net/profile/Jean-Charles_Regin/publication/221604096_Generalized_Arc_Consistency_for_Global_Cardinality_Constraint/links/0deec51a6fc6160677000000.pdf, AAAI 96) permet de restaurer la cohérence de domaine sur cette contrainte en temps $\Theta(d^{2} n)$, ou d est la taille du plus grand domaine et n le nombre de variables reliées par la contrainte. Il s’applique à une famille plus générale de contraintes appelées « contraintes globales de cardinalité » et fait appel à des résultats de théorie des graphes relatifs à la notion de flot.

Cas de la contrainte somme

Cette contrainte relie n variables $v_{1},\ \ldots,\ v_{n}$ ayant chacune un domaine contenant des entiers et impose que $v_{1} + \ldots + v_{n} = k$, où k est un entier. On ne connait pas d’algorithme de complexité polynomiale dans le pire des cas pour restauration de la cohérence de domaine sur cette contrainte. Cette contrainte ne peut être filtrée en temps polynomial que si la célèbre hypothèse $P \neq NP$ est fausse.

Exercices d’assimilation

C-ass-00

Restaurez la consistance de domaine sur le réseau de contrainte ci-dessous.

image11

Obtiendrait-on le même résultat en remplaçant les AllDiff par les contraintes binaires $A \neq B,\ A \neq C,B \neq C,\ A \neq D,\ A \neq E,\ D \neq E$ ?

"Indice"

Commençons par la première contrainte (pourquoi pas ?).

image18

Deux valeurs doivent être retirées A=1 et A=2, parce que si A était assignée avec ces valeurs, il serait impossible de satisfaire alldiff(A, B, C). Il faut maintenant visiter la deuxième contrainte.

"Solution"

Le fait que la seule valeur restante dans le domaine de A soit 3 induit que cette valeur n’est plus utilisable pour D et E. Donc on la retire.

image23

Mais cela induit que D=1, par exemple, n’a plus de support pour la deuxième contrainte. On le retire, ce qui vide complètement le domaine de D.

image24

Le filtrage est terminé. Un domaine vide nous permet de conclure que le réseau initial était incohérent.

C-ass02

On modélise le problème des 4 reines avec 4 variables A, B, C, D représentant les colonnes d’un échiquier de 4 x 4 et ayant dans leurs domaines les numéros de lignes, {1, 2, 3, 4}. Il y a 6 contraintes binaires exprimées en extension. Par exemple, une des contraintes relie A et B et n’autorise que les valeurs de A et B désignant des positions qui ne sont ni sur la même ligne, ni sur une même diagonale.

Par exemple, $Q_{\text{AB}} = {(1,3),(1,4),(2,4),(3,1),(4,1),(4,2)}$.

Voici quelques exemples de positions de reines qui respectent la contrainte reliant les variables A et B qui concernent les 2 premières colonnes.

image12

On suppose qu’une reine est déjà en place en position 2 sur la premièrecolonne. Restaurez la cohérence des domaines des trois variables B, C et D en mettant une croix dans les cases correspondant aux valeurs retirées.

image13

"Indice1"

Commençons par la contrainte entre A et B.

image19A étant assignée à 2, les cases correspondant aux autres valeurs du domaine de A ont été barrées. On voit facilement les valeurs de B qui doivent être retirées. Elles correspondent aux cases menacées par la reine située sur la première colonne. Du coup, il ne reste que 4 dans le domaine de B, ce qui revient à assigner B=4. Pour la suite, merci de visiter la contrainte entre A et C pour que les indices suivants soient pertinents.

"Indice2"

Voici le résultat après filtrage de la contrainte entre A et C et de celle entre A et D.

image25

Merci de continuer avec les contraintes entre B et C et entre B et D.

"Indice3"

Voici le résultat du filtrage des contraintes entre B et C et entre B et D.

image31

Tentez maintenant de terminer le filtrage.

"Solution"

La valeur D=1 doit être supprimée parce qu’elle n’a plus de support pour la contrainte entre C et D. On arrive à une situation où il ne reste plus qu’une valeur dans chaque domaine, et ces valeurs constituent une solution.

image35

C-ass03

Soit le réseau de contraintes suivant :

Variables : P et L avec domaine 0..8,

Contraintes :

  • Q1 : P + L = 8

  • Q2 : P + 2L = 11

Restaurez la cohérence de domaine.

"Indice1"

Il faut commencer par une des contraintes. La première, P + L = 8, ne permet aucun filtrage. Donc on s’intéresse à la deuxième, P + 2L = 11.

On voit assez vite qu’aucune valeur paire de P ne permet de satisfaire P + 2L = 11. Ce qui induit les filtrages suivants dans le domaine de P.

image20

Continuez à filtrer la même contrainte en vous intéressant au domaine de L.

"Indice2"

Voici les valeurs retirées du domaine de L au regard de la contrainte P + 2L = 11.

image26

On arrive à un stade où toutes les valeurs des deux domaines ont des supports pour la contrainte P + 2L = 11. Mais le filtrage n’est pas terminé. Intéressez-vous maintenant à la contrainte P + L = 8.

"Indice3"

Voici les valeurs retirées du domaine de P au regard de la contrainte P + L = 8.

image32

Tentez de continuer le filtrage sur le domaine de L, puis revenez si nécessaire à la contrainte P + 2L = 11.

[spoiler title="Solution"]

On termine le filtrage de la contrainte P + L = 8.

image36

Puis on revient sur la contrainte P + 2L = 11.

image37

Le filtrage se termine avec une solution. Le problème que vous venez de résoudre est célèbre : Dans une basse-cour, il y a des poules et des lapins. Au total, on compte 8 têtes et 22 pattes. Combien y-a-t-il de poules (P) et de lapins (L) ? La réponse est 5 poules et 3 lapins.

C-ass04

Soit le réseau de contraintes suivant :

Variables : $X_{0},\ X_{1},\ X_{2},\ X_{3}\ \in { 0, 1 }, Y \in { 10,\ 11,\ 14}$

Contrainte : $X_{0} + \ {2X}{1} + \ {4X}{2}, + {\ 8X}_{3} = Y$.

Restaurez la cohérence de domaine.

"Indice"

Il y a une seule contrainte, mais à première vue elle parait difficile à filtrer. Toutefois si on remarque que cette contrainte exprime que les valeurs des variables $X_{0},\ X_{1},\ X_{2},\ X_{3}$ sont les bits de la décomposition en base 2 de l’entier représenté par $Y$, les choses se simplifient. Chaque entier représentable sur 4 bits en base 2 admet une unique représentation en base 2.

Observons les représentations en base 2 des entiers du domaine de la variable $Y$.

image21

A vous de continuer le raisonnement. On cherche des valeurs des domaines des variables qui, quelles que soient les valeurs des autres variables, ne peuvent permettre de satisfaire la contrainte $X_{0} + \ {2X}{1} + \ {4X}{2}, + {\ 8X}_{3} = Y$.

"Solution"

La table donnée en premier indice peut nous être très utile. Elle nous montre que dans toutes les assignations des variables qui satisfont la contrainte, les valeurs $X_{3} = 0$ et $X_{1} = 0$ n’apparaissent jamais.

image27

Le résultat du filtrage est :

image28

On voit que chaque valeur de chaque domaine apparait dans au moins une des assignations des variables qui satisfont la contrainte, ce qui est précisément ce qui caractérise la cohérence de domaine.

C-ass05

Soit le réseau de contraintes suivant.

image14

  1. En considérant qu’il y a entre chaque paire de nœuds reliés par une arête une contrainte de différence binaire, restaurez la cohérence de domaine.

  2. Au lieu de modéliser ce problème à l’aide de contraintes binaires, on choisit maintenant d’utiliser une contrainte AllDiff pour chacun des triangles du graphe. Restaurez la cohérence de domaine.

image15

"Indice1"

Concernant la première question, la réponse est simple et immédiate. On ne peut filtrer une contrainte binaire de différence que si l’un des domaines concernés est un singleton dont l’élément est dans l’autre domaine, comme par exemple {2} et {2, 5}. Si les deux domaines ont plus d’un élément, chaque valeur a nécessairement un support.

Par contre, des filtrages sont possibles avec la version spécifiée à l’aide de contraintes alldiff.

"Indice2"

Il faut trouver une première contrainte qui filtre. Par exemple celle-ci.

image29

La suppression de B=3 va entrainer d’autres filtrages. A vous de les trouver.

"Solution"

En visitant successivement les contraintes, on arrive rapidement à cette solution.

image33

Java en LP : partie C

Partie C : Classes et objets

Objectif

Être capable d’implémenter une application comportant plusieurs classes, y compris des classes abstraites, reliées par héritage, ainsi que des interfaces, et des itérateurs, en maîtrisant l’implémentation et le comportement des constructeurs, de méthodes redéfinies avec liaisons dynamiques. Être capable de prévoir le comportement d’un tel programme, y compris les objets et données créés dans la mémoire de l’ordinateur (pile, tas, zone des données statiques).

Prérequis

Parties A et B


C1 : Exemple introductif

On appelle classe dérivée d’une classe S toute classe D qui hérite de S dans le sens suivant :

  • Toutes les variables de classe et attributs de S appartiennent implicitement à D.

  • Toutes les méthodes de S appartiennent à D, mais peuvent être redéfinies dans D.

  • D peut posséder des variables d’instance, de classe, des méthodes d’instance, de classe, spécifiques qui n’appartiennent pas à S.

Les redéfinitions d’attributs, variables de classe, méthodes de classes ne seront pas abordées.

Nous allons découvrir ces notions avec un exemple qui commence par la définition d’une classe Personne, qui représente un individu, et d’une classe Document, qui représente un document ayant un nom et un ou plusieurs auteurs (qui sont des instances de Personne).

Pour l’instant il n’y a pas d’héritage, mais seulement un lien de composition entre Document et Personne.

public class Personne
{
    private String nom;
    private Date dateNaiss;

    public Personne(String nom)
    {
        // DateNaiss est initialisé implicitement à null
        this.nom=nom;
    }

    public Personne(String nom, Date naiss)
    {
        this(nom); // Appel du constructeur à 1 paramètre
        this.dateNaiss =  naiss;
    }

    public String toString()
    {
        String s = nom;
        if(dateNaiss!=null)
        {
            s = s + "né le " + dateNaiss;
        }
        return s;
    }
}
public class Document
{
    private String nom;
    private Personne[] auteurs;

    public Document(String n, Personne[] a)
    {
        this.nom=n; auteurs = a;
    }

    public int nbAuteurs()
    {
        return auteurs.length;
    }

    public String toString()
    {
        String da;
        if(nbAuteurs()==0) da="inconnu";
        else
        {
            da="";
            for (int i = 0; i < nbAuteurs(); i++)
                da = da + auteurs[i] + " ";
        }
        return nom + "\nAuteur(s) : "+da;
    }
}

Les deux classes peuvent être représentées par ce schéma appelé diagramme de classes.

1

La notion de composition correspond à l’auxiliaire avoir : un document a un ou plusieurs auteurs. A présent nous allons introduire une classe Livre qui représente un cas particulier de document : un livre est un document. Cet auxiliaire être traduit la notion d’héritage.

public class Livre extends Document
{
    private long isbn;
    private int annee;

    public Livre(String nom, Personne[] aut, long isbn, int apub)
    {
        super(nom, aut); // Appel du constructeur de Document
        this.isbn=isbn;
        annee=apub;
    }

    public long getIsbn()
    {
        return isbn;
    }

    public String toString()
    {
        String s = "Livre : "+super.toString(); // Appel toString de document
        return s+"\nISBN : "+isbn+" ("+annee+")";
    }
}

Le mot-clé extends traduit l’héritage. La classe Livre est dérivée de Document. Document est la superclasse de Livre.

Le code de tout constructeur de la classe dérivée doit commencer par l’appel d’un constructeur de sa superclasse à l’aide du mot clé super.

La méthode toString existe déjà dans la superclasse et elle est ici redéfinie. La méthode toString de la superclasse est appelée depuis la classe dérivée grâce à la syntaxe super.toString().

Voici le diagramme de classe complet. Toute instance de Livre est une instance de Document. Toute instance de Document a des auteurs qui sont des instances de Personne. Donc, évidemment, toute instance de Livre a des auteurs…

image-20210909153314999

Il y a quelques règles à connaitre en matière d’héritage et de constructeurs :

  1. Si une classe n’a pas de constructeur explicite alors Java lui ajoute un constructeur implicite qui appelle le constructeur par défaut de sa superclasse, et initialise ses variables d’instance avec des valeurs par défaut.
  2. Si une classe a au moins un constructeur explicite alors Java ne produit pas de constructeur par défaut implicite pour cette classe, mais on peut bien sûr lui donner un constructeur par défaut explicite.
  3. Si le code d’un constructeur explicite d’une classe dérivée ne commence pas par super(…) alors ce constructeur appelle implicitement le constructeur par défaut de sa superclasse. Et si la superclasse n’a pas de constructeur par défaut, le programme ne compile pas !
  4. Si le code d’un constructeur explicite d’une classe dérivée appelle explicitement un constructeur de sa superclasse, son code doit commencer par super(…).

Pour mémoire, rappelons le sens de quelques termes importants.

  • implicite : produit automatiquement pas le compilateur, non visible dans le code.
  • explicite : écrit par le programmeur.
  • constructeur par défaut : sans paramètres.
  • valeur par défaut : placée automatiquement dans une variable qui n’est pas initialisée explicitement.

Exercices de découverte

C-dec-10

Complétez ces lignes de code permettant de créer une instance de Document représentant un document ayant un seul auteur nommé N. Wirth et ayant pour titre "Algorithm + Data Structure = Program", puis d’afficher la description de ce document produite par la méthode toString.

Personne[] aut = {/* création d'une instance de Personne */};
Document doc = new /* À compléter */;
System.out.println(/* À compléter */);

L’instance de Personne doit être créée avec le constructeur à un paramètre, qui ne nécessite pas la date de naissance.

Vous pouvez utiliser un ordinateur pour rechercher la bonne syntaxe par essai et erreurs.

"Solution"

Personne[] aut = {new Personne("N. Wirth")};
Document doc = new Document("Algorithm + Data Structure = Program", aut);
System.out.println(doc);

C-dec-11

Complétez cette ligne de code permettant de créer une instance de Livre représentant un document ayant un seul auteur nommé N. Wirth et ayant pour titre "Algorithm + Data Structure = Program", un numéro isbn égal à 130224189, ayant été publié en 1976.

Personne[] aut = {new Personne("N. Wirth")};
Livre w = new Livre (// À compléter //);
"Solution"

Livre w = new Livre
        ("Algorithm + Data Structure = Program",
          aut,  130224189, 1976
        );

C2 : Héritage et types

En Java, chaque variable et chaque paramètre a un type. Nous avons vu qu’il existe des types primitifs : int, boolean, double, long. Chaque nom de classe représente aussi un type. Mais du fait de l’héritage, une instance de classe peut avoir plusieurs types !

Voici un premier exemple d’utilisation de notre classe Livre

Livre w = new Livre(
        "Patience dans l'azur",
        new Personne[]{new Personne("Hubert Reeves")},
        2020099179, 1981);

System.out.println(w.toString());
System.out.println(w.getIsbn());

…qui produit l’affichage :

Livre : Patience dans l'azur
Auteur(s) : Hubert Reeves 
ISBN : 2020099179 (1981)
2020099179

Maintenant, il est aussi possible d’écrire…

Document w = new Livre(
        "Patience dans l'azur",
        new Personne[]{new Personne("Hubert Reeves")},
        2020099179, 1981);

System.out.println(w.toString());

…car toute instance de Livre est aussi par héritage une instance de Document. Mais… Il n’est plus possible d’appeler la méthode getIsbn.

System.out.println(w.getIsbn()); // Ne compile pas

Parce que c’est une méthode de Livre qui n’existe pas dans Document, et que la variable w est de type Document. Pas de problème avec la méthode toString qui est redéfinie dans la classe Livre mais existe dans Document et, cerise sur le gâteau, c’est la méthode toString de Livre qui sera exécutée et non celle de Document.

Ce petit miracle s’appelle la liaison dynamique et nous y reviendrons plus tard. Observons ce qui se produit en mémoire.

image-20210910122934576

Exercices de découverte

C-dec-20

Donnez les lignes de code permettant, après la déclaration précédente,

  • d’afficher le nombre d’auteurs du livre,
  • d’afficher le numéro isbn du livre,
  • d’afficher la description de ce document produite par la méthode toString de la classe Livre.
"Solution"

System.out.println(w.nbAuteurs());
System.out.println(w.getIsbn());
System.out.println(w.toString());

C-dec-21

On écrit les lignes suivantes.

Personne[] aut = {new Personne("N. Wirth")};
Livre w = new Livre
        ("Algorithm + Data Structure = Program",
          aut,  130224189, 1976
        );
Document d = w;                      // ligne A
System.out.println(d);               // ligne B
System.out.println(d.nbAuteurs());   // ligne C
System.out.println(d.getIsbn());     // ligne D

Toutes les lignes passent à la compilation sauf la dernière, qui comporte une erreur. Expliquez pourquoi.

"Solution"

Toute instance de Livre est aussi, par héritage, une instance de Document, et c’est pourquoi sa référence peut être mise dans une variable de type Document. Les méthodes nbAuteurs et toString étant définies dans la classe Document, elles sont accessibles via la variable d.

Par contre, la méthode getIsbn n’est pas définie dans la classe Document et donc pas accessible via une variable de type Document, quand bien même cette variable contient une instance de Livre.

C-dec-21

Quand on exécute le code de l’exercice précédent (en supprimant la dernière ligne puisqu’elle est incorrecte), on obtient l’affichage suivant :

Livre : Algorithm + Data Structure = Program
Auteur(s) : N. Wirth 
ISBN : 130224189 (1976)
1

Bien que la variable d soit de type Document, c’est la méthode toString de la classe Livre qui a été exécutée. Tentez de trouver une explication.

"Solution"

La méthode toString est définie dans Document et redéfinie dans Livre. Comme elle est définie dans Document, elle est donc accessible via la variable d de type Document. Mais comme cette variable pointe une instance de Livre (une classe dérivée de Document), c’est la méthode toString de Livre qui est exécutée. Si la méthode n’avait pas été redéfinie dans Livre, c’est celle de Document qui aurait été exécutée.

Ce comportement n’est pas évident à prévoir. Il résulte d’un choix faite par les inventeurs de la programmation orientée objet. Si vous avez eu du mal à deviner la réponse, refaites les exercices de découverte dans quelques jours.

C3 : Compléments sur l’héritage et la structuration des programmes

Notion de package

Un package regroupe un ensemble de classes. Le fichier dans lequel est définie une classe d’un package p doit comporter, au début, la déclaration :

package p;

Si une classe C utilise une classe U située dans un package q différent de celui où elle se trouve, une des déclarations suivantes doit apparaître avant la définition de C :

import q.U; // Importe la classe U du package q
import q.*; // Importe toutes les classes du package q

Droits d’accès

Jusqu’à maintenant, nous avons utilisé deux droits d’accès : public et private. Il en existe deux autres : protected et « par défaut ». Ce dernier s’applique lorsque le droit d’accès à un élément n’est pas précisé.

  • public : L’élément est accessible depuis toutes les classes du programme.
  • private : L’élément est accessible seulement depuis la classe où il est défini ou déclaré.
  • protected : L’élément est accessible depuis les classes dérivées de la classe où il est défini et déclaré et depuis toutes les classes du même package.
  • par défaut : L’élément est accessible seulement depuis les classes du package de la classe où il est défini ou déclaré.

Les classes ont elles-mêmes des droits d’accès. Une classe directement définie dans un fichier source .java, comme nous l’avons fait jusqu’à présent, peut avoir le droit public ou « par défaut ». Une classe public est utilisable dans tout le programme où elle est définie, une classe avec droit d’accès par défaut n’est utilisable que depuis le package où elle est définie.

Hiérarchie de classe

Une classe déclarée final ne peut avoir de classe dérivée. Mais en dehors de cette restriction, toute classe dérivée peut elle-même avoir des classes dérivées. D’ailleurs toutes les classes descendent d’une classe définie par Java nommée Object. Par contre Java ne permet pas l’héritage multiple : chaque classe (excepté Object) a une seule superclasse.

image-20210910130821540

Or il se trouve que la classe Object possède nativement certaines méthodes, dont notamment la méthode toString. On peut donc utiliser cette méthode héritée depuis n’importe quelle classe, même si elle n’y est pas redéfinie.

Exercices de découverte

C-dec-30

On ajoute la méthode suivante à la classe Document.

private void test()
{
    System.out.println("Exécution de test dans la classe Document");
}

On ajoute la méthode suivante à la classe Livre.

public void test()
{
    System.out.println("Exécution de test dans la classe Livre");
}

Le programme compile. Que peut-on en déduire en matière de possibilité de redéfinition d’une méthode private dans une classe dérivée ?

"Solution"

Non seulement on peut redéfinir une méthode private , mais on peut rendre public sa redéfinition. En fait cela se généralise : on peut toujours redéfinir une méthode avec des droits d’accès moins restrictifs.

C-dec-31

On ajoute la méthode suivante à la classe Document.

public void test()
{
    System.out.println("Exécution de test dans la classe Document");
}

On ajoute la méthode suivante à la classe Livre.

private void test()
{
    System.out.println("Exécution de test dans la classe Livre");
}

Le compilateur indique une erreur. Que peut-on en déduire ?

"Solution"

Une méthode public , ne peut être redéfinie avec un droit d’accès private. En fait cela se généralise : on ne peut redéfinir une méthode avec des droits d’accès plus restrictif que ceux dont elle dispose dans la superclasse.

C-dec-32

On ajoute la méthode suivante à la classe Document.

private void test()
{
    System.out.println("Exécution de test dans la classe Document");
}

On ajoute la méthode suivante à la classe Livre.

private void test()
{
    super.test();
    System.out.println("Exécution de test dans la classe Livre");
}

Le programme va-t-il compiler sans erreur ? Si oui, que se produira-t-il à l’exécution de test avec une instance de Livre ? Si non, expliquez l’erreur.

"Solution"

Il y a une erreur car la méthode test de Document est privée, donc elle ne peut être appelée par aucune méthode de Livre.

C4 : Interface

Une interface contient des signatures de méthodes et des commentaires expliquant ce que sont censées faire ces méthodes.

Par exemple, l’interface List<T> est implémentée par les classes ArrayList<T> et LinkedList<T>.

Autre exemple : il existe une interface prédéfinie Comparable<T> dans laquelle est déclarée une méthode int compareTo(T e) permettant de comparer deux objets de type T (l’objet courant et l’objet e) en respectant une relation d’ordre sur les objets de type T. Cette méthode doit retourner un entier négatif si this < e, un entier positif si this > e, et en cas d’égalité elle doit retourner 0.

Une interface définit un type. Par exemple, on peut déclarer une variable de type List.

Une interface ne contient pas de code et ne possède pas de constructeur.

Une classe peut implémenter une ou plusieurs interfaces. Elle doit alors implémenter toutes les méthodes déclarées dans cette interface (en respectant les spécifications données dans les commentaires de l’interface).

Voici un exemple d’utilisation de l’interface Comparable. Cette interface est déjà prédéfinie dans le langage Java, sous la forme suivante :

public interface Comparable<T>
{
	public int compareTo(T e);
}

Pour rendre une classe comparable, on fait en sorte qu’elle implémente cette interface :

public class Date implements Comparable<Date>
{
    // ...
	
    public int compareTo(Date d)
    {
        if(d.annee!=annee) 
            return this.annee-d.annee;

        else if(d.mois!=mois) 
            return this.mois-d.mois;

        else 
            return this.jour-d.jour;
    }
}

A présent, nous pouvons créer une liste de dates et utiliser la méthode statique sort de la classe prédéfinie Collections pour trier cette liste sans avoir à coder un algorithme de tri.

    public static void test()
    {
        List<Date> w = new ArrayList<>();
        w.add(new Date(25,1,1962));
        w.add(new Date(26,1,1962));
        w.add(new Date(25,2,1961));

        Collections.sort(w);

        System.out.println(w);
    }

L’affichage produit par l’exécution nous permet de vérifier que le tri a bien été exécuté.

[25/2/1961, 25/1/1962, 26/1/1962]

L’appel Collections.sort(w) fonctionne si et seulement si w désigne une instance d’une classe qui implémente l’interface List représentant une liste dont les éléments sont des instances d’une classe qui implémente l’interface Comparable. C’est un peu compliqué, mais très puissant puisque cela évite d’avoir à programmer une méthode de tri lorsque les conditions sont réunies.

Exercices de découverte

C-dec-40

Relisez le code de la classe Personne. Si on décide que cette classe doit implémenter l’interface Comparable, que doit-on ajouter à cette classe ? À quoi cela pourrait servir ? Proposez un exemple d’utilisation.

"Solution"

On doit ajouter dans la classe Personne une méthode compareTo permettant de comparer l’instance courante avec une autre instance. Cette comparaison pourrait porter sur les noms, en étant basée sur l’ordre alphabétique, mais pourrait aussi être basée sur les dates de naissances, à condition de disposer d’un moyen de comparer des instances de Date. (Il serait alors judicieux que la classe Date implémente aussi l’interface Comarable.)

Cela permettrait, par exemple de trier des listes d’instances de Personne en utilisant la méthode Collection.sort.

C5 : Classes et méthodes abstraites

Jusqu’à maintenant, nous avons parlé de classes, dans lesquelles toute méthode non héritée doit être implémentée (codée), et d’interfaces, dans lesquelles des méthodes sont déclarées, mais pas implémentées.

Une classe abstraite est une sorte d’intermédiaire entre ces deux concepts : certaines méthodes peuvent être implémentées alors que d’autres, appelées méthodes abstraites, sont juste déclarées. Une classe abstraite peut comporter des variables d’instances et des variables de classe.

Une classe abstraite ne peut pas être instanciée.

Une classe abstraite n’a d’intérêt que si elle a au moins une classe dérivée « concrète », qui implémente ses méthodes abstraites.

Fait remarquable : Une méthode concrète peut très bien appeler une méthode abstraite ! Le code exécuté est celui de l’implémentation de cette méthode dans une classe dérivée qui est déterminée lors de l’exécution du programme. On parle alors de liaison dynamique.

Pour bien comprendre le principe de la liaison dynamique, nous allons développer un exemple dans lequel une classe abstraite Animal a deux classes dérivées concrètes : Chat et Elephant.

image-20210910151842924

La classe abstraite Animal a une variable d’instance nom et un constructeur qui initialise cette variable. Elle a une méthode abstraite reactionDanger et une méthode concrète dangerDetecte qui appelle la méthode abstraite reactionDanger.

La méthode abstraite reactionDanger devra être implantée dans les classes Chat et Elephant.

public abstract class Animal
{
    private String nom;

    public Animal(String nom)
    {
        this.nom = nom;
    }

    public abstract void reactionDanger(); // Méthode abstraite

    public void dangerDetecte() // Méthode concrète
    {
        System.out.println(nom + " a détecté un danger.");
        reactionDanger(); // Appel d'une méthode abstraite
    }
}

On voit dans cet exemple que la méthode concrète dangerDetecte de la classe Animal appelle une méthode abstraite reactionDanger de cette même classe. Lors de l’exécution, c’est une implémentation concrète de reactionDanger définie dans une classe dérivée qui sera appelée. C’est typiquement un cas de liaison dynamique.

Voici deux exemples de classes qui dérivent de la classe abstraite Animal.

public class Chat extends Animal
{
    public Chat(String n)
    {
        super("Le chat "+n);
    }

    public void reactionDanger()
    {
        System.out.println("Il miaule et prend la fuite");
    }
}
public class Elephant extends Animal
{
    public Elephant(String n)
    {
        super("L’éléphant "+n);
    }

    public void reactionDanger()
    {
        System.out.println("Il barrit et il charge !");
    }
}

Voici un exemple d’utilisation des classes que nous venons de définir.

Animal felix = new Chat("Felix");        // ligne A
Animal jumbo = new Elephant("Jumbo");    // ligne B
felix.dangerDetecte();                   // ligne C
jumbo.dangerDetecte();                   // ligne D

L’exécution de la ligne C produit l’affichage "Le chat Felix a détecté un danger. Il miaule et prend la fuite. "

L’exécution de la ligne D produit l’affichage "L’éléphant Jumbo a détecté un danger. Il barrit et il charge ! "

Dans les deux cas, la méthode appelée, dangerDetecte, est héritée de la superclasse Animal et appelle la méthode reactionDanger, abstraite dans la classe Animal, mais implémentée dans les classes Chat et Elephant.


Les classes abstraites permettent notamment d’éviter la duplication de code : on y implémente des algorithmes qui sont utilisés ou utilisables par plusieurs classes dérivées concrètes.

Par exemple, la classe abstraite AbstractList<T> de Java a parmi ses classes dérivées les classes ArrayList<T> et (indirectement) LinkedList<T>. Elle implante des algorithmes qui sont les mêmes pour ces deux classes, comme par exemple la méthode…

public boolean addAll(int i, Collection<? extends T> c) 

…qui insère dans la liste courante, à partir de la position i, tous les éléments de c. Cette méthode addAll utilise la méthode…

public void add(int i, T e) 

…qui devra être implémentée dans toute classe concrète dérivant de AbstractList.

Voici une vue partielle de la hiérarchie des classe java dédiées à la représentation des listes.

image-20210910154953409

C-dec-50

Que se passe-t-il si on écrit, par exemple dans une méthode main, la ligne suivante ?

Animal w = new Animal("Flipper le dauphin");
"Solution"

Il y a une erreur car on ne peut pas créer d’instance d’une classe abstraite.

C-dec-51

On ajoute une méthode enrouleTrompe à la classe Elephant.

public void enrouleTrompe() {...}

Que se passe-t-il si on écrit, par exemple dans une méthode main, les lignes suivantes ?

Animal jumbo = new Elephant("Jumbo");
jumbo.enrouleTrompe();
"Solution"

Il y a une erreur car même si enrouleTrompe est une méthode d’instance de la classe Elephant, elle n’est pas définie dans Animal donc pas accessible via la variable jumbo.

C6 : Les itérateurs

Un itérateur est un objet (!) permettant de parcourir facilement tous les objets d’une collection, et en particulier d’une liste. En Java, toute classe implémentant l’interface prédéfinie Iterable<T> permet d’utiliser des itérateurs. C’est le cas notamment des classes ArrayList et LinkedList, mais aussi des tableaux.

L’interface Iterable<T> est indissociable d’une autre interface prédéfinie nommée Iterator<T>.

public interface Iterator<T>
{
    boolean hasNext(); // vrai s'il y a un élément suivant
    T next();          // élément suivant
    void remove();     // suppression de l’élément suivant
}
public interface Iterable<T>
{
    Iterator<T> iterator()
    // ...
}

Pour comprendre l’intérêt et la manière d’utiliser ces concepts, considérons l’exemple d’une instance de ArrayList<Integer>.

ArrayList<Integer> w = new ArrayList<>();

w.add(15); w.add(12); w.add(33);

Iterator<Integer> scan = w.iterator(); // ligne A

while(scan.hasNext())
{
    System.out.println(scan.next());
}

En ligne A, La méthode iterator retourne un itérateur, c’est à dire une instance d’une classe qui implémente l’interface Iterator.

Dans la boucle while, Les méthodes hasNext et next de l’itérateur permettent de parcourir la liste et de récupérer ses éléments.

Cette boucle peut être réécrite avec une syntaxe plus lisible qui masque l’utilisation d’un itérateur et qui est la manière la plus courante de procéder.

  for(int x : w)
  {
      System.out.println(x);
  }

C-dec-60

Soit une classe C représentant une collection d’objets (liste, ensemble…). Soit w une instance de C. À quelle condition peut-on utiliser la syntaxe suivante :

for(x : w) {...}
"Solution"

Il faut que la classe C implémente l’interface Iterable<...> (directement ou parce qu’elle dérive d’une classe qui implémente cette interface).

C7 : Classes internes

Les compétences relatives aux notions introduites ici ne seront pas évaluée et sont présentées à titre informatif.

Les classes internes anonymes

Il arrive parfois qu’on ait besoin de créer une classe qui dérive d’une classe existante ou qui implémente une interface, et que l’on ait besoin que d’une seule instance de cette classe. Puisqu’une seule instance est requise, il n’est pas nécessaire de donner un nom à la classe et il est possible de créer la classe et son unique instance en une seule ligne de programme. Voici un exemple dont l’unique intérêt est la simplicité.

abstract class Oiseau
{
    abstract void voler();
}
public class Main
{
    public static void main(String[] args)
    {
        Oiseau bird = new Oiseau()
        {
            void voler()
            {
                System.out.println("Flap flap");
            }
        };

        bird.voler();
    } 
}

La classe Oiseau est abstraite donc on ne peut pas créer directement avec new une instance de cette classe.

La variable bird de ma méthode main reçoit une référence d’une instance d’une classe dérivée de Oiseau, qui n’a pas de nom et dans laquelle la méthode voler est implémentée. Cette classe dérivée n’aura qu’une seule instance.

Les classes internes statiques

On peut définir une ou plusieurs classes dans une classe comme dans l’exemple suivant :

public class Test
{
    public static abstract class Animal
    {
        private String nom;

        public Animal(String nom) {this.nom = nom;}
        public abstract void reactionDanger();

        public void dangerDetecte()
        {
            System.out.println(nom + " a detecte un danger.");
            reactionDanger();
        }
    }

    public static class Chat extends Animal
    {
        public Chat(String n) {super("Le chat "+n);}

        public void reactionDanger()
        {
            System.out.println("Il miaule et prend la fuite");
        }
    }
}

Pour instancier la classe Chat en dehors de la classe Test, on doit utiliser la syntaxe illustrée par cet exemple.

   Test.Animal felix = new Test.Chat("Felix");
   felix.reactionDanger();

Les classes membres

Le principe est le même que pour les classes internes statiques, mais chaque instance de la classe interne est liée à une instance de la classe enveloppante, et à accès aux variables d’instance de la classe enveloppante. Dans cet exemple, la classe Liste représente une liste chaînée dont les éléments de type double, sont contenus dans des maillons, qui sont des instances d’une classe interne Maillon.

public class Liste
{
    Maillon deb;

    private class Maillon
    {
        Maillon suiv;
        double val;

        Maillon(double val)
        {
           this.val=val;
           suiv=deb;
           deb=this;
        }
    }
  
    public void ajoute(double x)
    {
        Maillon m = new Maillon(x);
    }

    public void print()
    {
        Maillon cur=deb;
        while(cur!=null)
        {
            System.out.println(cur.val);
            cur = cur.suiv;
        }
    }
}

Le constructeur de Maillon a accès aux attributs de la classe Liste.

La méthode ajoute ajoute un élément en début de liste. La méthode print affiche les éléments de la liste.

La classe Maillon étant private, elle ne peut être exploitée que dans la classe Liste.

Exercices d’assimilation

C-ass-00

Réalisez une classe Video qui représente un cas particulier de Document. Les instances de Video représentent des vidéos qui, en plus de tous les attributs d’un document, ont une durée. Cette durée doit être l’un des paramètres du constructeur de la classe Video. La classe Video doit redéfinir la méthode toString de sorte que cette méthode retourne la description retournée par la méthode toString de Document, complétée avec la durée. La classe Video doit être dotée en outre d’une méthode getDuree qui retourne la durée de la vidéo représentée par l’instance courante.

"Indice1"

Voici un squelette de la classe Video

public class Video extends Document
 {
   private double duree;
 
   public Video(String n, Personne[] a, double duree)
   {
 			// À compléter
   }
 
   public double getDuree()
   {
  		// À compléter
   }
 
   public String toString()
   {
      // À compléter
   }
 }

"Indice2"

Voici le constructeur de la classe Video, qui doit appeler (en première ligne) celui de la superclasse (c’est-à-dire Document).

public Video(String n, Personne[] a, double duree)
 {
   super(n, a);
   this.duree = duree;
 }

"Solution"

Voici le reste de la classe Video, avec pour particularité l’appel de de la méthode toString de la superclasse par celle de la classe Video.

public double getDuree()
 {
   return this.duree;
 }
 
 public String toString()
 {
   return super.toString() + " durée : " + this.duree;
 }

C-ass-01

Réalisez une classe Roman qui représente un cas particulier de Livre. Les instances de Roman représentent des romans qui, en plus de tous les attributs d’un livre, ont un genre, représenté par une chaîne de caractère. Ce genre doit être l’un des paramètres du constructeur de la classe Roman. La classe Roman doit redéfinir la méthode toString de sorte que cette méthode retourne la description retournée par la méthode toString de Livre, complétée avec le genre.

"Indice1"

Voici un squelette de la classe Roman

public class Roman extends Livre
 {
   private String genre;
 
   public Roman(String nom, Personne[] aut, long isbn, int apub, String genre)
   {
       // À compléter
   }
 
   public String toString()
   {
       // À compléter
   }
 }

"Indice2"

Voici le constructeur de la classe Roman, qui doit appeler (en première ligne) celui de la superclasse (c’est-à-dire Livre).

public Roman(String nom, Personne[] aut, long isbn, int apub, String genre)
 {
   super(nom, aut, isbn, apub);
   this.genre = genre;
 }

"Solution"

Voici la méthode toString de la classe Roman, qui appelle la méthode toString de sa superclasse Livre.

public String toString()
 {
   return super.toString() + " genre : " + this.genre;
 }

C-ass-02

Exercice créé par Marie-Laure Mugnier, Université de Montpellier

Les Pokémons sont des gentils animaux qui sont passionnés par la programmation objet en général et par le polymorphisme en particulier. Il existe quatre grandes catégories de Pokémons :

Les Pokémons sportifs : Ces Pokémons sont caractérisés par un nom, un poids (en kg), un nombre de pattes, une taille (en mètres) et une fréquence cardiaque mesurée en nombre de pulsations à la minute. Ces Pokémons se déplacent sur la terre à une certaine vitesse que l’on peut calculer grâce à la formule suivante : vitesse = nombre de pattes * taille * 3.

Les Pokémons casaniers : Ces Pokémons sont caractérisés par un nom, un poids (en kg), un nombre de pattes, une taille (en mètres) et le nombre d’heures par jour où ils regardent la télévision. Ces Pokémons se déplacent également sur la terre à une certaine vitesse que l’on peut calculer grâce à la formule suivante : vitesse = nombre de pattes * taille * 3.

Les Pokémons des mers : Ces Pokémons sont caractérisés par un nom, un poids (en kg) et un nombre de nageoires. Ces Pokémons ne se déplacent que dans la mer à une vitesse que l’on peut calculer grâce à la formule suivante : vitesse = poids / 25 * nombre de nageoires.

Les Pokémons de croisière : Ces Pokémons sont caractérisés par un nom, un poids (en kg) et un nombre de nageoires. Ces Pokémons ne se déplacent que dans la mer à une vitesse que l’on peut calculer grâce à la formule suivante : vitesse =(poids / 25 * nombre de nageoires) / 2.

Pour chacune de ces quatre catégories de Pokémons, on désire disposer d’une méthode toString qui retourne (dans une chaine de caractères) les caractéristiques du Pokémon.

Programmez ces classes en Java. Les attributs sont privés. Pour éviter de devoir créer autant de fichiers que de classes, vous pouvez ne déclarer qu’une classe "public", celle qui contient la méthode main de l’application.

Le but est de minimiser les duplications de code en utilisant le polymorphisme (méthodes abstraites, redéfinition, héritage, liaison dynamique).

Voici un exemple de méthode main permettant de tester votre solution.

public static void main(String[] args)
 {
   Pokemon p1 = new PokSportif("Tim", 1.2, 4, 0.3, 70);
   System.out.println(p1.toString());
 
   Pokemon p2 = new PokMaison("Tom", 1.8, 4, 0.5, 5);
   System.out.println(p2.toString());
 
   Pokemon p3 = new PokMer("Tum", 2.4, 2);
   System.out.println(p3.toString());
 
   Pokemon p4 = new PokMer("Tem", 3.3, 2);
   System.out.println(p4.toString());
 }

L’affichage attendu lors de l’exécution de la méthode de test est le suivant.

Tim : Pokemon sportif de fréquence cardiaque 70
4 pattes, taille = 0.3 m
vitesse : 3.59, poids : 1.2

Tom : Pokemon domestique regardant la télé 5 heures par jour
4 pattes, taille = 0.5 m
vitesse : 6.0, poids : 1.8

Tum : Pokemon de mer
2 nageoires
vitesse : 0.19, poids : 2.4

Tem : Pokemon de mer
2 nageoires
vitesse : 0.26, poids : 3.3
"Indice1"

On peut utiliser l’architecture suivante avec 3 classes abstraites et 4 concrètes.

image-20210910210105543

"Indice2"

Commençons par la classe abstraite Pokemon au sommet de notre hiérarchie.

abstract class Pokemon
{
   String nom;
   double poids;
 
   public Pokemon(String nom, double poids)
   {
     this.nom = nom;
     this.poids = poids;
   }
 
   public abstract double vitesse();
 
   public abstract String description();
 
   public String toString()
   {
     return nom + " : " + description() + "\nvitesse : " 
            + vitesse() + ", poids : " + poids;
   }
}

On met dans cette classe tout ce qui est commun à tous les Pokémons. Les attributs ne sont pas privés mais ont un accès par défaut (package) pour éviter d’alourdir le code avec des accesseurs. Le constructeur sert uniquement à être appelé par les constructeurs des classes dérivées (on ne peut pas créer une instance de classe abstraite). La méthode concrète toString construit une description de n’importe quel Pokémon en faisant appel au méthodes abstraites vitesse et description qui seront définies dans les classes dérivées.

Il vous reste à implémenter les 2 autres classes abstraites et les 4 classes concrètes dans le même esprit.

"Indice3"

Intéressons-nous à la classe abstraite PokTer

abstract static class PokTer extends Pokemon
{
   private int pattes;
   private double taille;
 
   public PokTer(String nom, double poids, int nb_pattes, double taille)
   {
     super(nom, poids);
     this.pattes = nb_pattes;
     this.taille = taille;
   }
 
   public double vitesse()
   {
     return pattes * taille * 3;
   }
 
   public String description()
   {
     return pattes + " pattes, taille = " + taille + " m";
   }
}

Cette classe regroupe tout ce qui est commun aux Pokémons sportifs et aux Pokémons casaniers un nombre de pattes, une taille, le calcul de la vitesse qui est réalisé par une méthode concrète dont les classes dérivées hériteront, et une partie de la description qui sera réutilisée dans les classes dérivées. Je vous conseille de développer maintenant la classe concrète PokSportif et de faire des essais sur machine sans attendre d’avoir complètement terminé l’exercice.

"Indice4"

Voici le détail de la classe PokSportif.

static class PokSportif extends PokTer
{
   private int frequence;

   public PokSportif(String nom, double poids, int nb_pattes, double taille, int frequ)
   {
     super(nom, poids, nb_pattes, taille);
     this.frequence = frequ;
   }

   public String description()
   {
     return "Pokemon sportif de fréquence cardiaque " + frequence + "\n"
         \+ super.description();
   }
}

On voit apparaitre l’attribut fréquence qui est spécifique aux Pokémons sportifs. Le point important est que la méthode description redéfinie ici est celle qui sera effectivement exécutée, mais qu’elle appelle la méthode description de la superclasse PokTer qui crée la partie de la description commune aux Pokémons sportifs et aux Pokémons de maisons (casaniers).

Vous avez maintenant tous les éléments techniques pour terminer l’exercice, d’abord en implémentant l’autre type de Pokémon terrestre, puis la branche des Pokémons aquatiques.

"Indice5"

Voici le détail de la classe PokMaison.

static class PokMaison extends PokTer
 {
   private int htv;
 
   public PokMaison(String nom, double poids, int nb_pattes, double taille, int htv)
   {
     super(nom, poids, nb_pattes, taille);
     this.htv = htv;
   }
 
   public String description()
   {
     return "Pokemon domestique regardant la télé " + htv + 
            " heures par jour\n" + super.description();
   }
 }

Et la classe abstraite qui mutualise les éléments communs des Pokémons aquatiques.

static class PokAqua extends Pokemon
 {
   private int nageoires;

   public PokAqua(String nom, double poids, int nageoires)
   {
      super(nom, poids);
      this.nageoires = nageoires;
   }

   public double vitesse()
   {
     return *round*((poids / 25) * nageoires);
   }

   public String description()
   {
     return nageoires + " nageoires";
   }
 }

On retrouve ici les principales techniques mises en œuvre précédemment. On peut d’ailleurs imaginer d’autres variantes des classes déjà écrites qui seraient tout à fait acceptables. Le point intéressant est que la vitesse des Pokémons des mers et celle des Pokémons de croisière ses calcules de manière assez similaire. Une grosse partie du calcul est réalisé par la méthode vitesse de la classe abstraite PokAqua qui sera directement héritée par la classe PokMer et sera redéfinie dans PokCrois. Les deux classes manquantes sont données au prochain épisode.

"Solution"

Voici le détail de la classe PokMer.

static class PokMer extends PokAqua
{
   public PokMer(String nom, double poids, int nageoires)
   {
     super(nom, poids, nageoires);
   }

   public String description()
   {
     return "Pokemon de mer\n » + super.description();
   }
}
static class PokCrois extends PokAqua
{
   public PokCrois(String nom, double poids, int nageoires)
   {
     super(nom, poids, nageoires);
   }

   public double vitesse()
   {
     return super.vitesse() / 2.0;
   }

   public String description()
   {
     return "Pokemon de mer\n" + super.description();
   }
}

Ceci termine l’exercice. Il est important que vous compreniez bien ce qui se passe lorsque la méthode toString est invoquée avec un objet de type PokCrois. Cette méthode est définie dans la classe Pokemon au sommet de la hiérarchie. Elle fait appel :

  • à la méthode vitesse de la classe PokCrois qui fait appel à la méthode vitesse de la classe PokAqua,

  • à la méthode description de la classe PokCrois, qui elle-même appelle la méthode description de PokAqua.

Cet enchaînement d’appels n’est pas si évident à suivre ! Il faut prendre le temps d’y réfléchir. Le principe général est qu’en cas de redéfinitions, c’est la variante de la méthode la plus « proche » de l’objet concerné qui est appelée.

Exercices de consolidation

C-cons-00

Vous devez développer une petite hiérarchie de classes permettant de représenter des ensembles d’entiers de différentes manières.

image-20210910181135430

L’interface IntSet déclare trois méthodes permettant respectivement d’ajouter un élément dans l’ensemble courant, de tester si un entier appartient l’ensemble courant, et de tester si l’ensemble courant est inclus dans un autre ensemble désigné en paramètre. Cette interface dérive de Iterator<Integer> pour que toutes les classes implémentant cette interface dispose d’itérateurs permettant d’énumérer leurs éléments.

La classe abstraite AintSet permet l’implémentation de la méthode subsetOf qui utilise les méthodes abstraites isIn et iterator.

Quant aux deux classes BoolIntSet et ListIntSet, elles implémentent deux représentations différentes d’un ensemble d’entiers : une représentation par un ArrayList d’entiers et une représentation par tableau de Booléens.

image-20210910180941252

"Indice1"

Commençons par déclarer l’interface IntSet.

public interface IntSet extends Iterable<Integer>
{
   public void add(int e);
   public boolean isIn(int e);
   public boolean subsetOf(IntSet s);
}

"Indice2"

Intéressons-nous maintenant à la classe abstraite AintSet.

public abstract class AintSet implements IntSet
{
   public boolean subsetOf(IntSet s)
   {
       Iterator<Integer> iter = iterator();
       while(iter.hasNext())
       {
         int e = iter.next();
         if(!s.isIn(e))
         {
             return false;
         }
       }
       return true;
   }
}

Il faut bien comprendre que les méthodes abstraites déclarées dans l’interface IntSet et celles héritées par cette interface sont héritées par AintSet et sont donc implicitement présentes dans cette classe. C’est ce qui permet de mettre ici l’implémentation concrète de la méthode subsetOf dont le code est commun aux deux représentations possibles des ensembles. Cette méthode utilise un itérateur, dont la disponibilité est garantie par le fait que AintSet implémente indirectement (via IntSet), l’interface Iterable<Integer>. L’algorithme implémenté dans la méthode subsetOf consiste à parcourir, grâce à un itérateur, tous les éléments de l’ensemble courant et de vérifier grâce à un appel à isIn si chacun d’eux appartient à l’ensemble désigné par le paramètre s.

La méthode isIn est abstraite, mais ce sera une de ses implémentations concrètes dans une classe dérivée qui sera exécutée en pratique. C’est toute la force du concept de liaison dynamique. Il vous reste maintenant à implémenter les deux classes dérivées BoolIntSet et ListIntSet. Commencez par ListIntSet, qui sera traitée par le prochain indice. Ne le consultez pas tout de suite !

"Indice3"

public class ListIntSet extends AintSet
{
   private ArrayList<Integer> data;
  
   class IterIntSet implements Iterator<Integer>
   {
     private int cursor;
 
     public IterIntSet(){this.cursor = 0;}
 
     public boolean hasNext()
     {
       if(cursor<data.size()) return true;
       else return false;
     }
 
     public Integer next()
     {
       this.cursor++;
       return data.get(cursor-1);
     }
   }

  // À compléter
}

La classe ListIntSet implémente concrètement un ensemble sous la forme d’une liste d’entiers. Elle doit également implémenter une classe interne de type Iterator<Integer>. Voici une partie du code de cette classe.

Les éléments de l’ensemble représenté seront stockés dans la liste désignée par l’attribut data. Cet attribut est accessible depuis la classe interne IterIntSet qui implémente l’itérateur.

Cette classe IterIntSet a son propre attribut cursor, de type int, qui représente une sorte de curseur permettant de parcourir la liste des éléments de l’ensemble.

Vous pouvez comprendre les détails du fonctionnement en examinant attentivement le code des différentes méthodes.

Il y a toutefois une autre approche possible, plus « professionnelle », qui consiste à utiliser un itérateur de la classe ArrayList<Integer>, qui elle-même implémente Iterable<Integer> . Vous pouvez essayer et tester votre solution sur machine.

Il vous reste à compléter cette classe avec les définitions du constructeur, des méthodes add, isIn et iterator.

"Indice4"

Voici le constructeur et les méthodes add et iterator de la classe ListIntSet.

public ListIntSet() {this.data = new ArrayList<>();}
 
public void add(int e) {this.data.add(e);}

public Iterator<Integer> iterator(){return new IterIntSet();}

Et la méthode isIn. Elle pourrait utiliser l’itérateur. Le choix qui a été fait ici est de parcourir la liste des éléments avec une boucle for.

public boolean isIn(int e)
{
   for(int x : this.data)
   {
     if( x== e) return true;
   }
   return false;
}

Il vous reste à implémenter la classe BoolIntSet.

"Solution"

Voici le détail de la classe BoolIntSet qui termine l’exercice. Prenez le temps de bien analyser son contenu.

public class BoolIntSet extends AintSet
{
   private boolean[] data;
  
   // Implémenter l'itérateur comme classe membre lui permet
   // l'accès à l'attribut privé data
   class IterIntSet implements Iterator<Integer>
   {
       private int cursor;

       public IterIntSet() {this.cursor = -1;}

       public boolean hasNext()
       {
           cursor++;
           while(cursor<data.length)
           {
               if(data[cursor]) return true;
               else cursor++;
           }
           return false;
       }

       public Integer next() {return cursor;}
   }
 
   public BoolIntSet() {this.data = new boolean[256];}
 
   public void add(int e) {this.data[e] = true;}
 
   public boolean isIn(int e){return this.data[e];}
 
   public Iterator<Integer> iterator(){return new IterIntSet();}
}

public BoolIntSet() { this.data = new boolean[256]; }

public void add(int e) { this.data[e] = true; }

public boolean isIn(int e) { return this.data[e]; }

public Iterator<Integer> iterator() { return new IterIntSet(); } }

Cet exercice est difficile à maîtriser car il comporte des subtilités algorithmiques et des notions assez avancées de programmation objet.

Programmation par contraintes : partie B

Programmation par contraintes

En M2 Bases de données et IA et M2 Image et IA

Par Olivier Bailleux, Maître de conférences HDR, Université de bourgogne

Partie B : Modélisation d’un problème

Objectif

Être capable de modéliser sous la forme d’un réseau de contraintes un problème combinatoire à vérifiabilité polynomiale exprimé en langage naturel et /ou en langage mathématique.

Prérequis

Partie A.

Introduction

La modélisation est une activité de conception, donc une activité créative, comme c’est le cas de l’invention d’un algorithme qui résout un problème, bien que ce ne soit pas du tout la même chose. Modéliser un problème à l’aide de contraintes est donc tout autant un art qu’une technique. Il n’existe pas de recette et c’est surtout la pratique, l’expérience, qui vous rendra apte à traiter de nouveaux problèmes. Il y a tout de même une démarche intellectuelle que je vais illustrer par un exemple.

Il faut déjà bien comprendre que qu’on attend de vous lorsqu’on vous demande de modéliser un problème, et en particulier que vous devez livrer un réseau de contraintes, c’est-à-dire :

  • des variables,
  • des domaines,
  • et des contraintes !

B1 : Un exemple

Nous allons modéliser pas à pas le problème du sac à dos. Voici son énoncé.

L’énoncé du problème

On dispose d’un sac à dos dans lequel on souhaite placer des objets à sélectionner parmi une liste de N d’objets. Chaque objet de la liste a un poids (en Kilogrammes) et une valeur (en Euro) et peut être ou non sélectionné pour être placé dans le sac. Le nombre d’objets placés dans le sac n’est pas limité, mais la somme de leurs poids ne doit pas excéder W, et la somme de leur valeur doit être au moins égale à V.

Une instance de problème est donc définie par une liste de poids $w_{1},\ldots w_{N}$, une liste de prix $v_{1},\ldots v_{N}$, une charge maximale $W$ et une valeur minimale $V$.

image1

Bien comprendre ce qui est attendu

Une des choses essentielles à bien comprendre est que dans l’énoncé du problème, il n’y pas de variables. N, $w_{1},\ldots w_{N}$, $v_{1},\ldots v_{N}$, W et V sont, à l’échelle d’une instance du problème, des constantes. Ce sont des données parfaitement connues au moment de résoudre le problème.

Le solveur qui va résoudre le problème ne doit pas décider quel poids ou quelle valeur a chaque objet, ni la capacité du sac, ni la valeur minimum à transporter. Tous cela est déjà connu au moment de résoudre le problème.

Ce que le solveur doit décider, c’est quels objets mettre dans le sac et lesquels ne pas mettre dans le sac. Et c’est cela que les valeurs des variables doivent nous dire dans une solution du problème qui sera, si applicable, trouvée par le solveur.

Il faut donc que toute solution puisse être représentée par une assignation des variables qui nous dise, pour chaque objet, s’il va aller ou non dans le sac.

Les variables

Un réseau de contraintes s’articule autour de variables. Il faut donc commencer par proposer des variables dont les assignations sont susceptibles de représenter une solution.

Puisque, dans une solution, chaque objet a deux états possibles : pris ou pas pris, on peut modéliser le problème à l’aide de N variables $X_{1},\ldots,X_{n}$ à domaine {0,1}.

On donne un sens aux assignations : $X_{i} = 1$ signifie que l’objet $i$, (de poids $w_{i}$ et de valeur $v_{i}$) est dans le sac. Bien sûr, $X_{i} = 0$ signifie qu’il n’est pas dans le sac.

Voici une assignation qui représente une solution de notre exemple d’instance.

image2

Ce choix de variables à domaines ${ 0,1}$ sera un bon choix si on arrive à exprimer les contraintes qui traduisent les exigences exprimées dans l’énoncé du problème.

Les contraintes

Nous avons donc choisi d’utiliser N variables Booléennes pour modéliser une instance du problème du sac à dos avec N objets. Il nous reste à trouver les contraintes qui spécifient si une assignation particulière de ces variables est une solution.

Or, d’après l’énoncé, il y a deux propriétés à vérifier :

  1. La somme des poids des objets mis dans le sac ne doit pas dépasser W.

  2. La somme des valeurs des objets mis dans le sac doit être au moins égale à V.

On peut exprimer ces propriétés sous la forme de deux contraintes arithmétiques sur les variables :

$$Q1\ :\sum_{i = 1}^{N}{w_{i}X_{i}}\ \leq W$$

$$Q2\ :\sum_{i = 1}^{N}{v_{i}X_{i}}\ \geq V$$

En effet, on voit facilement que pour une assignation particulière des variables, $\sum_{i = 1}^{N}{w_{i}X_{i}}$ représente la somme des poids des objets qui sont dans le sac, et que $\sum_{i = 1}^{N}{v_{i}X_{i}}$ représente la valeur totale de ces objets.

Les contraintes Q1 et Q2 sont appelée contraintes arithmétiques linéaires. Examinons le réseau de contraintes obtenu avec notre instance d’exemple.

Variables : $X_{1},X_{2},X_{3},X_{4},X_{5}$ avec domaines {0,1}

Contraintes :

  • Q1 : $0.05 X_{1} + 1.2 X_{2} + 0.9 X_{3} + 0.1X_{4} + 0.9X_{5} \leq 2$
  • Q2 : $5.9 X_{1} + 749 X_{2} + 5 X_{3} + 0.5 X_{4} + 79 X_{5} \geq 755$

Le problème est modélisé. Cet exemple nous a juste donné une idée de la démarche de modélisation, mais les choix des variables, des domaines, et la spécification des contraintes sont font au cas par cas et nécessitent une certaine créativité et un peu d’expérience.

Exercices de découverte

B-dec-10

Donnez des critères simples permettant d’identifier facilement certaines instances du problème n’ayant aucune solution.

"Solution"

Si la somme des valeurs de tous les objets est inférieure à $V$ ou si l’objet le plus léger a un poids supérieur à $W$ alors on peut conclure immédiatement que le problème n’a pas de solution.

Exercices d’assimilation

Pour acquérir une première expérience de la modélisation en programmation par contraintes, il n’y a pas trente-six mille moyens. Il faut faire traiter des exemples divers et variés, en prenant le temps de réfléchir à chaque cas. L’erreur fatale serait de regarder trop vite les solutions des exercices d’entraînement qui vont vous être proposés. C’est comme si vous tentiez d’apprendre à jouer du piano juste en observant un pianiste en action.

B-ass-00

Aguirre, Brunissendre, Clodomir et Déothéria travaillent dans une entreprise où 4 machines doivent être exploitées. Chaque machine requiert une personne qualifiée, et une personne ne peut s’occuper que d’une seule machine. Voici les compétences des Employés.

Personnes compétentes
Machine 1 Aguirre, Brunissendre, Clodomir, Déothéria
Machine 2 Aguirre, Brunissendre, Clodomir, Déothéria
Machine 3 Clodomir, Déothéria
Machine 4 Déothéria

On ne vous demande pas une solution, mais un réseau de contraintes dont des solutions représentent des solutions de cette instance de problème. Votre modélisation doit être généralisable à toute autre instance de ce problème avec un nombre quelconque de personnes et autant de machines.

La première question que vous devez vous poser est : quelles variables et quels domaines ? Nous avons 4 personnes et 4 machines. Réfléchissez d’abord aux possibilités avant de lire la suite.

"Indice

On pourrait utiliser 4 variables, chacune associée à une personne. Ces variables pourraient être nommées A, B, C, D pour Aguirre, Brunissendre, Clodomir et Déothéria.

image6

"Indice

Si le domaine d’une variable associée à une personne est l’ensemble des numéros des machines sur lesquelles cette personne est habilitée à travailler, alors toute assignation des variables revient à attribuer à chaque personne une machine qu’elle peut gérer. Et ceci sans avoir encore écrit la moindre contrainte.

Variables :

  • $A$ avec domaine {1, 2}

  • $B$ avec domaine {1, 2}

  • $C$ avec domaine {1, 2, 3}

  • $D$ avec domaine {1, 2, 3, 4}

Mais alors, la modélisation est terminée ? En fait non. S’il n’y a aucune contrainte, toutes les assignations sont des solutions, y compris par exemple ${ A = 1,\ B = 1,\ C = 1,\ D = 1}$. Voyez-vous ce qui cloche ? La modélisation recherchée doit non seulement permettre d’assigner une machine à chaque personne, mais elle doit aussi faire en sorte que toutes les machines soient assignées et qu’une même personne ne puisse être assignée à plusieurs machines. Pour exprimer cela, il nous faut des contraintes. Essayez de trouver lesquelles.

"Solution"

On peut simplement exprimer que chacune des variables doit avoir une valeur différente des autres. Ainsi, deux personnes ne peuvent pas travailler sur une même machine, et donc les 4 machines sont occupées.

Contraintes :

$A \neq B$, $A \neq C$,$\text{\ A} \neq B,$ $A \neq D$, $B \neq C$, $B \neq D$, $C \neq D$.

Il existe une contrainte particulière qui exprime cela de manière plus concise : $\mathbf{\text{alldiff}}(A,B,C,D)$. Cette contrainte se généralise à un nombre quelconque de variables et est satisfaite, comme son nom l’indique, si et seulement si les valeurs des variables reliées sont toutes différentes. Il n’y a rien d’autre à ajouter, notre problème est modélisé.

Cette modélisation fonctionne aussi s’il y a plus d’employés que de machines. Elle garantit alors que toutes les machines sont utilisées (si la distribution des compétences le permet) et qu’une et une seule machine est assignée à chaque personne.

B-ass-01

Soit le réseau de contraintes suivant.

Variables :

  • A et B avec domaine {1,2}

  • C avec domaine {1,2,3}

Contrainte :

  1. AllDiff(A,B,C)

Donnez une spécification en extension de la contrainte.

"Indice

Cet exercice est très facile. La seule difficulté est de comprendre l’énoncé. La contrainte est spécifiée ici en compréhension. On demande une reformulation en extension, c’est-à-dire par énumération des assignations des variables A, B, C qui satisfont cette contrainte. La présentation est condensée sous la forme d’un ensemble de triplets.

Sur A, B, C : {…}

"Indice

Comme ce réseau ne comporte qu’une seule contrainte, la représentation en extension de cette contrainte revient à donner les solutions du réseau.

"Solution"

Sur A, B, C : {(1, 2, 3), (2, 1, 3)}

B-ass-02

Proposez une modélisation du problème des sacs à dos. Ce problème est une généralisation du problème du sac à dos que nous avons modélisé précédemment dans laquelle on N objets ayant chacun un poids et une valeur, mais avec K sacs ayant chacun la même charge maximale W.

Chaque objet peut être placé dans au plus un sac, et la somme des valeurs de tous les objets emportés (placés dans un sac) doit être au moins égale à une valeur V donnée.

"Indice

Il faut bien sûr relire la description de la modélisation du problème avec un seul sac à dos, dans lequel, pour chaque objet, une variable Booléenne indique s’il est ou non dans le sac. Ici, on a plusieurs sacs, et pour chaque objet $i$ et chaque sac $j$, soit l’objet $i$ est dans le sac $j$, soit il n’y est pas. Il est de fait tout à fait possible que certains objets ne soient dans aucun sacs.

"Indice

On a N objets pouvant chacun être dans un des K sac ou dans aucun sac. Une première idée pourrait être d’associer à chaque objet $i$ une variable $Y_{i}$ de domaine ${ 0,\ 1,\ \ldots,\ K}$ avec le sens suivant : $Y_{i} = 0$ si l’objet $i$ n’est dans aucun sac, et $Y_{i} = j$, pour $j \in 1..K$, si l’objet $i$ est dans le sac $j$.

Cette idée n’est pas du tout mauvaise à priori. Elle serait même plutôt bonne si on envisageait d’écrire un algorithme pour résoudre le problème. Mais ce n’est pas du tout le cas. On cherche une modélisation avec des contraintes qui spécifie le problème et non un algorithme qui le résout. Or avec ce choix de variables, comment écririez-vous les contraintes relatives aux poids et valeurs des objets embarqués ? Ce serait lourd et difficile. Il est beaucoup plus facile de travailler uniquement avec des variables Booléennes.

"Indice

On définit $NK$ variables $X_{\text{ij}},\ i \in 1..N,\ j \in 1..K$ à domaines {0,1}. Le sens des assignations de ces variables est le suivant : $X_{\text{ij}} = 1$ indique que l’objet $i$ est dans le sac $j$, $X_{\text{ij}} = 0$ indique que l’objet $i$ n’est pas dans le sac $j$. Il vous reste à écrire les contraintes.

"Indice

Pour exprimer que le coût total des objets embarqués est au moins égal à $V$, on écrit :

$$\Sigma_{i = 1}^{N}\Sigma_{j = 1}^{K}{v_{i}X}_{\text{ij}} \geq V$$

Prenez le temps de bien comprendre cette contrainte. S’il vous faut 10 minutes, prenez 10 minutes ; s’il vous faut 20 minutes, prenez-en 20. Vous n’êtes pas obligé de tout comprendre instantanément, mais si vous brulez les étapes sans vraiment comprendre en profondeur les solutions, vous ne progresserez pas. Pour bien comprendre pourquoi on fait la somme de toutes les variables multipliées par les valeurs des objets associés, prenons un exemple avec 4 objets et 2 sacs. On sait que chaque objet doit être dans au plus un sac, ce qui devra être spécifié par d’autres contraintes que celle qui nous intéresse ici. Imaginons une assignation qui respecte cette condition comme par exemple celle illustrée ci-dessous.

image9

Maintenant, reprenons la matrice des variables en multipliant les valeurs assignées aux variables (0 ou 1) par les prix des objets concernés. Nous obtenons ceci :

image10

La somme des termes représente bien le cumul des prix des objets embarqués. La prochaine étape consiste à trouver les contraintes qui expriment que la charge maximale de chaque sac n’est pas dépassée. Il vous faudra donc spécifier une contrainte par sac.

"Indice

On pose une contrainte de charge pour chaque sac. La contrainte de charge pour le sac $j$ est :

$$\Sigma_{i = 1}^{N}w_{i}X_{\text{ij}} \leq W$$

Si on reprend notre petit exemple avec la même assignation, chaque contrainte relie les variables d’une même ligne correspondant à un sac $j$ et le terme $\Sigma_{i = 1}^{N}w_{i}X_{\text{ij}}$ représente le cumul des poids des objets qui sont dans ce sac.

image11

Mais ce n’est pas terminé. Il manque encore des contraintes. Jusqu’à maintenant, nous avons considéré comme implicite qu’un objet ne peut être placé dans plusieurs sac. Mais il faut spécifier cette interdiction par des contraintes. Sans cela, un solveur à qui on demanderait de résoudre notre problème pourrait nous trouver des solutions qui satisfont toutes les contraintes mais dans lesquelles certains objets seraient présents dans plusieurs sacs. À vous de trouver les contraintes manquantes.

"Solution"

On doit ajouter une nouvelle contrainte pour chaque objet, spécifiant que cet objet peut se trouver dans au plus un sac. Pour chaque objet $i$, on peut écrire :

$$\Sigma_{j = 1}^{k}X_{\text{ij}} \leq 1$$

Ce qui « autorise » l’objet à être présent dans aucun sac ou dans un seul sac.

B-ass-03

Dans le cadre de cet exercice, on appelle contrainte arithmétique linéaire normalisée une contrainte de la forme $a_{1}x_{1} + … + a_{n}x_{n} \leq b$, où $a_{1},\ .\ .\ .,\ a_{n},\ b$ sont des entiers relatifs (i.e. positifs, négatifs ou nuls) et $x_{1},\ .\ .\ .\ ,\ x_{n}$ des variables à domaine {0, 1}.

Soient $y_{1},\ y_{2},\ y_{3}$ des variables à domaine {0, 1}. Modélisez à l’aide d’une ou plusieurs contraintes arithmétiques linéaires normalisées chacune des propriétés suivantes :

  1. au moins une des variables $y_{1},\ y_{2},\ y_{3}$ a la valeur 1;

  2. aucune des variables $y_{1},\ y_{2},\ y_{3}$ n’a la valeur 1;

  3. toutes les variables $y_{1},\ y_{2},\ y_{3}$ ont la valeur 1;

  4. au moins une des variables $y_{1},\ y_{2},\ y_{3}$ a la valeur 0.

"Indice

Commençons par une contrainte facile à spécifier telle que la numéro 2. Aucune des variables n’a la valeur 1, ça signifie qu’elles ont toutes la valeur 0. On peut alors écrire :

$y_{1} + y_{2} + y_{3} \leq 0$

On pourrait remplacer le $\leq$ par $=$, mais nous devons respecter la forme exacte $a_{1}x_{1} + \ … + \ a_{n}x_{n}\ \leq \ b$ imposée par l’énoncé.

"Indice

Passons à la contrainte numéro 4. Au moins une des variables a la valeur 0. Cela suppose que leur somme ne peut atteindre la valeur 3. On peut donc écrire :

$y_{1} + y_{2} + y_{3} \leq 2$

"Indice

Passons à la contrainte numéro 3. Comment dire que toutes les variables ont la valeur 1 ? Cela revient à dire que leur somme vaut 3. Pour l’exprimer avec $\leq$, on peut écrire :

$- y_{1} – y_{2} – y_{3} \leq – 3$

On utilise des coefficients -1 pour les variables et une valeur $b = – 1$, ce qui est autorisé par l’énoncé, contrairement aux opérateurs de comparaison $=$ ou $\geq$.

"Solution"

Il reste une dernière contrainte, la numéro 2, qui dit qu’au moins une des variables doit avoir la valeur 1. La somme des variables doit donc être au moins égale à 1. En changeant les signes, on a :

$- y_{1} – y_{2} – y_{3} \leq – 1$

B-ass-04

Soient les variables a1, a2, b1, b2, c1, c2 à domaine {0,1} représentant les couleurs des trois arêtes d’un triangle avec la convention suivante : a1=1 (respectivement b1=1, c1=1) signifie que l’arête A (respectivement B, C) est rouge, a2=1 (respectivement b2=1, c2=1) signifie que l’arête A (respectivement B, C) est bleue.

Donnez une spécification en compréhension de toutes les contraintes nécessaires pour spécifier que la coloration est cohérente, i.e. chaque arête a exactement une couleur, et que le triangle n’est pas monochrome, i.e. que les arêtes ne sont pas toutes de la même couleur.

image3

"Indice"

Commençons par la cohérence des coloriages. On doit avoir $a_{1} \neq a_{2}$, $b_{1} \neq b_{2}$, $c_{1} \neq c_{2}$. Mais on pourrait l’exprimer différemment, par exemple $a_{1} + a_{2} = 1$ etc.

"Solution"

Il faut à présent spécifier que le triangle ne doit pas être monochrome. La contrainte $a_{1} + b_{1} + c_{1} \neq 3$ empêche le triangle d’être complètement rouge. La contrainte $a_{2} + b_{2} + c_{2} \neq 3$ l’empêche d’être complètement bleu. Ces deux contraintes sont suffisantes, mais il y a d’autres manières d’exprimer la même chose.

B-ass-05

On appelle $\text{conv}(X,\ \ B_{0},\ \ B_{1},\ \ldots,B_{n – 1})$ la contrainte reliant une variable $X$ de domaine $0..2^{n – 1}$ et $n$ variables $B_{0}, \ldots,B_{n – 1}$ de domaines {0,1} qui est satisfaite si et seulement si les valeurs $B_{0}, \ldots,B_{n – 1}$ représentent les bits de la représentation de $X$ en base 2. Par exemple, l’assignation $X = 12, B_{3} = 1, B_{2} = 1, B_{1} = 0, B_{0} = 0$ satisfait la contrainte $\text{conv}(X, B_{0}, B_{1}, B_{2}, B_{3})$ parce que 12 a pour représentation 1 1 0 0 en base 2.

Donnez une spécification en compréhension de $\text{conv}(X, B_{0}, B_{1}, B_{2}, B_{3})$ en utilisant exclusivement des opérateurs arithmétiques +, *, = et des constantes.

Donnez une spécification énumérative de $\text{conv}(X, B_{0}, B_{1})$.

En utilisant la contrainte conv et des contraintes arithmétiques de votre choix (limitées aux opérateurs +, =, <), proposez une modélisation du problème suivant : Trouver un entier X compris entre 5 et 9 (inclus) dont la représentation en base 2 comporte exactement 3 bits ayant la valeur 1.

"Indice

Il faut commencer par donner comme contrainte la formule qui exprime la décomposition d’un entier en base 2 :

$$X = \ B_{0} + \ 2B_{1} + 4B_{2} + 8B_{3}$$

"Indice

Pour la spécification en compréhension, il faut bien comprendre que ce qui est attendu est la liste de toutes les assignations des variables $X,\ B_{0},\ B_{1}$ qui satisfont la contrainte :

Sur $X,\ B_{0},\ B_{1}$, ${(0,0,0),\ (1,1,0),\ (2,0,1),\ (3,1,1)}$.

"Indice

Nous abordons la dernière question de l’exercice, on cherche à spécifier qu’un certain entier $X$ est compris entre 5 et 9, ce qui peut s’écrire en introduisant une variable $X$ de domaine {5, 6, 7, 8, 9}. Aucune contrainte n’est nécessaire. Par contre, pour exprimer que la représentation de X en base 2 comporte 3 bits à 1, on doit introduire 4 variables supplémentaires, $B_{0},\ B_{1},\ B_{2},\ B_{3}$ avec domaines {0, 1}. Essayez de continuer seul avant de consulter l’indice suivant.

"Solution"

Nous avons donc actuellement le réseau suivant :

Variables :

  • $X$ avec domaine {5, 6, 7, 8, 9}

  • $B_{0}, B_{1}, B_{2},B_{3}$ avec domaine {0, 1}

Il nous manque les contraintes :

Contraintes :

  • $\text{conv}(X, B_{0}, B_{1}, B_{2}, B_{3})$

  • $B_{0} + B_{1} + B_{2} + B_{3} = 3$

B-ass-06

On appelle règle toute suite strictement croissante d’entiers commençant par la valeur 0. Chacun de ces entiers est appelé une marque. On dit qu’un entier d est une distance mesurable par une règle si et seulement s’il existe deux marques a, b de cette règle (non nécessairement consécutives) telles que b − a = d. Par exemple, les distances mesurables par la règle ⟨ 0, 1, 3, 6 ⟩ sont les valeurs 1, 2, 3, 5 et 6. La longueur d’une règle est la valeur de sa dernière marque (c’est donc aussi la plus grande distance mesurable avec cette règle).

On appelle règle de Golomb toute règle dont chaque distance mesurable ne peut être mesurée que d’une seule manière possible. Par exemple, la règle ⟨ 0, 1, 3, 6 ⟩ n’est pas une règle de Golomb car elle permet de mesurer la distance 3 de deux manières différentes : soit en utilisant les marques 0 et 3, soit en utilisant les marques 3 et 6. En revanche, ⟨ 0, 1, 4, 6 ⟩ est une règle de Golomb ayant pour distances mesurables les valeurs 1, 2, 3, 4, 5 et 6. Vous pouvez vérifier qu’il n’y a bien qu’une seule paire de marques permettant de mesurer directement chacune de ces distances.

On souhaite déterminer s’il existe une règle de Golomb de longueur n avec k marques. Proposez une modélisation sous la forme d’un problème de satisfaction de contraintes, en précisant (en fonctions des valeurs n et k) quelles sont les variables, leurs domaines, et les contraintes qui les relient.

"Indice

D’abord les variables. La règle recherchée possède k marques qui peuvent être représentées par des variables $X_{0},\ \ldots X_{k – 1}.$ Par convention, la première marque vaut 0 et la dernière représente la longueur de la règle. Les autres ont des valeurs comprises entre 1 et $n – 1$. Donc :

Variables :

  • $X_{0}$ avec domaine {0}

  • $X_{k – 1}$ avec domaine {n}

  • $X_{1}$ à $X_{k – 2}$ avec domaines 1..n-1.

"Indice

On peut introduire de nombreuses contraintes pour dire que toute distance entre deux graduations est différente de toute autre.

Pour tout $i,\ j,\ k,\ m$, tels que $(i,j) \neq (k,m),\ i < j,\ k < m$, on ajoute la contrainte :

$$X_{j} – X_{i} \neq X_{m} – X_{k}$$

Par exemple pour 5 marques (k=5) on aurait 90 contraintes, dont certaines redondantes. Même en spécifiant un peu plus finement les contraintes, on en aura toujours $O(n^{4})$, ce qui n’est pas « interdit », mais guère satisfaisant. On peut limiter le nombre de contraintes en introduisant des variables supplémentaires associées aux distances.

Pour tout $i \in 0..(k – 2),\ j \in (i + 1)\text{..}(k – 1)$, on introduit une variable $D_{\text{ij}}$ de domaine $1..n$. On doit alors spécifier deux types de contraintes :

  • des contraintes qui expriment que chaque variable $D_{\text{ij}}$ représente la distance entre les marques $i$ et $j$,

  • et des contraintes qui expriment que les variables $D_{\text{ij}}$ ont des valeurs toutes différentes.

image7

"Solution"

Nous avons donc les variables suivantes :

  • $X_{0}$ avec domaine {0}

  • $X_{k – 1}$ avec domaine {n}

  • $X_{1}$ à $X_{k – 2}$ avec domaines 1..n-1

  • $D_{\text{ij}},\ i \in 0..k – 2,\ j \in (i + 1)\text{..}(k – 1)$ avec domaines $1..n$

Les contraintes à ajouter sont les suivantes :

  • $D_{\text{ij}} = X_{j} – X_{i}$, $i \in 0..(k – 2),\ j \in (i + 1)\text{..}(k – 1)$

  • alldiff$\left( { D_{\text{ij}},\ i \in 0..(k – 2),\ j \in (i + 1)\text{..}(k – 1)} \right)$

La contrainte alldiff relie toutes les variables de distance.

Voici un schéma du réseau obtenu avec $k = 4$ :

image8

B-ass-07

On dispose de N salles numérotée de 1 à $N$. Ces salles disposent chacune d’équipements de différents types en quantité variable. Il y a $M$ types d’équipements possibles, numérotés de 1 à $M$. Les quantités des différents types d’équipements disponibles dans les salles sont représentés par des constantes notées $E_{\text{jk}}$. $E_{\text{jk}} = r$ signifie que la salle $j$ dispose de $r$ ressource de type $k$.

image5

On souhaite assigner $P$ personnes, numérotées de 1 à $P$, à ces salles. Une salle peut accueillir une ou plusieurs personnes à condition de disposer des équipements dont elles ont besoin. Les équipements dont ont besoin les personnes sont représentés par des constantes notée $B_{\text{ik}}$. $B_{\text{ik}} = r$ signifie que la personne $i$ a besoin de $r$ équipements de type $k$.

Le problème à modéliser est le suivant : Est-il possible d’assigner à chaque personne une salle qui réponde à ses besoins, et si oui quelle salle peut être assignée à chaque personne ?

"Indice

Il ne faut surtout pas confondre les constantes $E_{\text{jk}}$ et $B_{\text{ik}}$ avec les variables du modèle à produire. Les assignations de variables doivent permettre de dire quelle personne doit aller dans quelle salle. Le plus simple pour pouvoir exprimer ensuite les contraintes est d’utiliser des variables $X_{\text{ij}}$ à domaine {0,1}, où $i \in 1..P$ désigne une personne et $j \in 1..N$ désigne une salle. $X_{\text{ij}} = 1$ signifie que la personne $i$ va dans la salle $j$.

A vous maintenant de spécifier les contraintes.

"Indice

Il faut exprimer qu’une personne doit aller dans une salle et une seule. Pour ce faire, on pose la contrainte suivante pour chaque personne $i$ :

$$\Sigma_{j \in 1..N}X_{\text{ij}} = 1$$

Ensuite, il faut ajouter les contraintes permettant d’exprimer que les ressources de chaque salle sont suffisantes pour satisfaire les besoins des personnes qui s’y trouvent. A vous de jouer.

"Indice

Pour chaque salle $j$, $j \in 1..N$ et chaque ressource $k \in 1..M$, Il faut ajouter une contrainte qui exprime que les ressources de type $k$ de la salle $j$ sont suffisantes pour tous les occupants de cette salle. Au total, il y aura donc $\text{NM}$ contraintes. Tentez d’exprimer ces contraintes.

"Solution"

On doit avoir les contraintes suivantes :

Pour tout $j \in 1..N$ et tout $k \in 1..M$, $\Sigma_{i \in 1..P}B_{\text{ik}}X_{\text{ij}} \leq E_{\text{jk}}$.

Le problème est complètement modélisé.

Programmation par contraintes : partie A

Programmation par contraintes

En M2 Bases de données et IA et M2 Image et IA

Par Olivier Bailleux, Maître de conférences HDR, Université de bourgogne

Partie A : Satisfaction de contraintes

Objectif

Connaître et comprendre les notions de base de la programmation par contrainte avec variables à domaines finis.

Prérequis

Notions de mathématiques de niveau Lycée.

Introduction

La programmation par contraintes permet de résoudre de nombreux problèmes sans avoir à réaliser un programme spécifique pour chaque problème à résoudre. Le principe est de modéliser le problème à résoudre sous la forme d’un réseau de contraintes qui sera placé en entrée d’un solveur.

image1

Dans ce cours, nous nous limiterons aux contraintes sur des variables à domaines finis, qui permettent de modéliser des problèmes combinatoires.

image2

A1 : Notions de base

Présentation

Un réseau de contraintes est constitué de variables, de domaines associés à ces variables et de contraintes reliant certaines de ces variables. Les contraintes peuvent être décrites en compréhension, c’est à dire par des formules mathématiques, ou en extension, c’est à dire par énumération des valeurs conjointement admissibles des variables concernées.

Exemple

Énoncé en langage naturel

Peut-on colorier les sommets du graphe suivant avec trois couleurs en faisant en sorte que les sommets reliés par une arête aient toujours des couleurs différentes ? Et si oui donnez une solution.

image3

Modélisation en compréhension

Variables : $A,B,C,D,E,F$ ayant toutes pour domaine ${1,2,3}$.

Contraintes :

$A\neq B,A\neq C,B\neq C,C\neq D,D\neq E,D\neq F,E\neq F$

Modélisation en extension

Variables : $A,B,C,D,E,F$ ayant toutes pour domaine ${1,2,3}$.

Contraintes :

  • $q_1$ sur $A,B$ : ${(1,2),(2,1),(1,3),(3,1),(2,3),(3,2)}$
  • $q_2$ sur $A,C$ : ${(1,2),(2,1),(1,3),(3,1),(2,3),(3,2)}$
  • $q_3$ sur $B,C$ : ${(1,2),(2,1),(1,3),(3,1),(2,3),(3,2)}$
  • $q_4$ sur $C,D$ : ${(1,2),(2,1),(1,3),(3,1),(2,3),(3,2)}$
  • $q_5$ sur $D,E$ : ${(1,2),(2,1),(1,3),(3,1),(2,3),(3,2)}$
  • $q_6$ sur $D,F$ : ${(1,2),(2,1),(1,3),(3,1),(2,3),(3,2)}$
  • $q_7$ sur $E,F$ : ${(1,2),(2,1),(1,3),(3,1),(2,3),(3,2)}$
Une solution

image4

Exercices de découverte

A-dec-10

Donnez une représentation en extension de la contrainte sur les variables $A,B,C$ ayant toutes pour domaine ${0,1,2}$ dont la représentation en compréhension est $A+B=C$.

"Solutions"

Sur $A,B$ : ${(0,0,0),(0,1,1),(1,0,1),(0,2,2),(2,0,2),(1,1,2)}$

A2 : Quelques définitions

Assignation :

Une assignation d’une variable $V$ consiste à lui attribuer une valeur x de son domaine. On peut la noter $V=x$ ou $(V, x)$.

Une assignation complète d’un ensemble de variables est un ensemble d’assignations attribuant une valeur à chaque variable de cet ensemble.

Une assignation partielle d’un ensemble de variable est un ensemble d’assignations attribuant une valeur à certaines variables de cet ensemble.

Étendre, compatibilité :

Soit $V$ un ensemble de variables, $J$ une assignation partielle de $V$ et $I$ une assignation complète de $V$. On dit que $I$ étend $J$, ou est compatible avec $J$ si et seulement si toutes les variables assignées par $J$ sont assignées avec la même valeur dans $I$.

Satisfaire, falsifier :

Soit $V$ l’ensemble des variables reliées par une contrainte $Q$. On dit d’une assignation complète $I$ de $V$ qu’elle satisfait $Q$ si et seulement si les valeurs attribuées aux variables sont autorisées par $Q$. Dans le cas contraire, on dit que $I$ falsifie $Q$.

On dit d’une assignation partielle $J$ de $V$ qu’elle satisfait $Q$ si et seulement si toute assignation complète compatible avec $J$ satisfait $Q$. On dit qu’elle falsifie $Q$ si et seulement si toute assignation complète compatible avec $J$ falsifie $Q$.

Exemple

La contrainte ALLDIFF, appliquée à un ensemble de variables, est satisfaite si et seulement si toutes les variables concernées sont assignées à des valeurs différentes. Dans la figure ci-dessous, les domaines des variables sont représentés par des rectangles.

image5

Voici une assignation complète et une assignation partielle qui satisfont la contrainte.

image6

Et voici une assignation complète et une assignation partielle qui falsifient la contrainte.

image7

Réseau de contraintes cohérent :

Une solution d’un réseau de contraintes est une assignation complète de ses variables qui satisfait toutes ses contraintes.

Un réseau de contrainte est cohérent si et seulement s’il admet au moins une solution. Dans le cas contraire il est dit incohérent.

Exercices de découverte

A-dec-20

Soient les variables $A,B,C$ ayant toutes pour domaine ${0,1}$. Donnez une assignation complète qui étend l’assignation partielle ${A=1}$.

"Solution"

Par exemple ${A=1,B=0,C=0}$. Il y en a trois autres.

A-dec-21

Soient deux variables $A,B$ ayant le même domaine de $k$ éléments. Combien d’assignations complètes des variables satisfont la contrainte $A\neq B$ ?

"Solution"

$k^2 – k$

Récapitulatif

Vous venez de découvrir beaucoup de termes que vous n’avez pas encore complètement assimilé. Reprenez-les sur la figure suivante et tentez de retrouver le sens de chacun d’eux. Si vous n’y arrivez pas, revenez en arrière dans ce document pour relire les parties concernées.

image8

Exercices d’assimilation

A-ass-00

Voici un réseau de contraintes. Variables : $A,B,C$ de domaine ${1,2,3,4}$. Contraintes :

  • $q_1$ : $A+B\leq C$

  • $q_2$ : $A\neq B$

Ce réseau est-il cohérent ? Si oui, donnez ses solutions. Donnez une assignation partielle de ${A,B,C}$ attribuant une valeur uniquement à la variable $A$ qui falsifie $q_1$.

"Indice

${A=1,B=2,C=3}$ est une solution. Trouvez les autres.

"Indice

Voici les solutions avec $A=1$ : ${A=1,B=2,C=3}, {A=1,B=2,C=4}, {A=1,B=3,C=4}$ Il vous reste à trouver celles avec $A=2$, celles avec $A=3$ (il n’y en a pas avec $A=$4).

"Indice

Voici toutes les solutions. ${A=1,B=2,C=3}, {A=1,B=2,C=4}, {A=1,B=3,C=4}$ ${A=2,B=1,C=3}, {A=2,B=1,C=4}, {A=2,B=3,C=4}$ ${A=3,B=1,C=4}$ Il vous reste à trouver une valeur de $A$ qui ne permet pas de satisfaire $q_1$, quelles que soient les valeurs de $B$ et $C$.

"Solution"

${A=4}$ falsifie $q_1$.

A-ass-01

Voici un réseau de contraintes. variables : $A,B,C\in {1,2,3}$. contraintes :

  • $q_1$ sur $A,B$ : ${(1,1),(1,2),(2,3),(3,3)}$
  • $q_2$ sur $A,C$ : ${(2,2),(3,1)}$
  • $q_3$ sur $B,C$ : ${(2,1),(3,3)}$

Ce réseau est-il cohérent ? Si oui, donnez ses solutions.

"Indice

La représentation graphique ci-dessous, dans laquelle les couples de valeurs autorisés par les contraintes sont matérialisés par des lignes reliant les valeurs concernées, doit vous permettre de trouver plus rapidement les solutions, si applicable.

image9

"Indice

Dans la figure ci-dessus, toute solution (s’il y en a) apparait sous la forme d’un triangle dont les sommets sont les valeurs assignées aux variables.

"Solution"

Le réseau est incohérent.

A-ass-02

Voici un réseau de contraintes. Variables : $A,B,C,D,E\in {0,1}$. Contraintes :

  • $A\vee \neg B$
  • $B\vee \neg C$
  • $C\vee \neg D$
  • $D\vee \neg E$
  • $\neg A\vee B$
  • $\neg B\vee C$
  • $\neg C\vee D$
  • $\neg D\vee E$

Où le symbole $\vee$ est le connecteur logique « ou ». Ce réseau est-il cohérent ? Si oui, donnez ses solutions.

"Indice

Trouvez d’abord la solution pour laquelle la variable A vaut 0 en faisant des déductions basées sur les 4 premières contraintes. Puis ensuite trouvez la solution pour laquelle A vaut 1 en exploitant les 4 dernières contraintes.

"Indice

Pour $A=0$, la première contrainte ne peut être satisfaite qu’avec $B=0$. Donc pour satisfaire la deuxième contrainte, il faut $C=0$, pour la troisième, $D=0$, pour la quatrième $E=$0. Avec ces valeurs, les 4 dernières contraintes sont satisfaites. Tenter de trouver l’autre solution.

"Solution"

Les deux solutions sont ${A=0,B=0,C=0,D=0}$ et ${A=1,B=1,C=1,D=1}$.

Java en LP – Partie B

Partie B

Classes et objets

Objectif

Être capable d’implémenter une classe comportant des attributs (variable d’instance), des méthodes d’instance, des constructeurs, exploitant des données représentées par des types primitifs, des tableaux, des chaînes, des listes de type ArrayList, et des classes enveloppes (telles que Integer, Double…). Être capable d’utiliser une telle classe en maîtrisant les effets de bord induits par les attributs modifiables. Être capable de réaliser une application utilisant plusieurs classes par composition (sans héritage) et de prévoir le comportement d’un tel programme, y compris les objets et données créés dans la mémoire de l’ordinateur (pile, tas, zone des données statiques).

Prérequis

Partie A


B1 : Exemple introductif

Présentation

Nous allons maintenant aborder la « vraie » programmation orientée objet, c’est à dire la création d’instances de classes, donc d’objets, et l’utilisation de méthodes d’instances pour agir sur ces objets.

Voici un exemple introductif avec une classe Date qui modélise une date constituée d’un jour, d’un mois et d’une année. Java dispose déjà d’une classe Date, mais on peut utiliser un même nom pour des classes différentes, à condition qu’elles soient dans des packages différents.

public class Date
 {
   private int jour;
   private int mois;
   private int annee;

   public Date()
   {
     this.jour=1; this.mois=1; this.annee=1900;
   }

   public Date(int jour, int mois, int annee)
   {
     this.jour=jour; this.mois=mois; this.annee=annee;
   }

   public String toString()
   {
     return this.jour + "/" + this.mois + "/" + this.annee;
   }
 }

Détaillons les différents éléments de cette classe. Voici la déclaration de trois attributs aussi appelés variables d’instance.

private int jour;
private int mois;
private int annee;

Ces trois attributs sont privés. Ils ne sont accessibles que depuis les méthodes de la classe Date. Chaque instance de la classe, c’est-à-dire chaque objet créé d’après cette classe, dispose de ses propres exemplaires de ces attributs.

Ensuite nous avons la définition d’un premier constructeur.

public Date()
 {
   this.jour=1; this.mois=1; this.annee=1900;
 }

Un tel constructeur sans paramètre est appelé constructeur pas défaut. Comme tout constructeur, son rôle est d’initialiser une instance de la classe lors de sa construction lancée par l’opérateur new.

Voici maintenant un deuxième constructeur qui accepte 3 paramètres.

public Date(int jour, int mois, int annee)
 {
   this.jour=jour; this.mois=mois; this.annee=annee;
 }

Comme dans le constructeur par défaut, les attributs initialisés sont préfixés par this, qui est la référence de l’objet en cours de construction. Dans le premier constructeur, ce préfixe est facultatif. Mais ici il permet de distinguer les attributs des paramètres de mêmes noms. Si on écrivait…

public Date(int jour, int mois, int annee)
 {
   jour=jour; mois=mois; annee=annee;
 }

… ce constructeur n’aurait strictement aucun effet sur les attributs de la classe, ni sur autre chose d’ailleurs puis qu’il recopierait le contenu du paramètre jour dans lui-même, et pareillement pour mois et annee.

Nous avons ensuite une méthode d’instance.

public String toString()
 {
   return this.jour + "/" + this.mois + "/" + this.annee;
 }

Cette méthode n’a aucun paramètre et retourne une chaîne de caractère qui décrit l’objet courant, c’est-à-dire l’objet via lequel elle est appelée. Ici, le préfixe this est facultatif, mais il a le mérite de bien mettre en évidence les attributs.

Maintenant que nous avons une classe, nous pouvons l’utiliser pour créer des instances et les utiliser. Voici un exemple simple.

public class Main
 {
   public static void main(String[] args)
   {
     Date d1 = new Date(11,9,2017);
     Date d2 = new Date();
 
     System.out.println(d1.toString());
     System.out.println(d2.toString());
   }
 }

Les deux premières lignes de la méthode main font respectivement appel aux deux constructeurs de la classe Date.

    Date d1 = new Date(11,9,2017);
    Date d2 = new Date();

Les références des objets créés sont placées dans deux variables de méthode. Voici ce qui se passe en mémoire.

image1

Les deux lignes suivantes appellent la méthode toString avec chacune des deux instances et affiche les chaînes retournées.

    System.out.println(d1.toString());
    System.out.println(d2.toString());

Le résultat est l’affichage suivant…

1/9/2017
1/1/1900

Quelques précisions

Constructeur par défaut

Lorsqu’on ne définit aucun constructeur dans une classe, Java crée un constructeur par défaut qui initialise chaque variable d’instance avec une valeur par défaut : 0 pour un entier, 0.0 pour un flottant, null pour une référence… Dès lors qu’on définit un constructeur avec paramètre(s), aucun constructeur par défaut n’est produit de manière automatique. Si on en veut un, il faut le définir. Dans notre exemple, le constructeur par défaut ne sert pas à grand-chose d’autre qu’illustrer le concept.

Destruction d’une instance

Toute instance est détruite automatiquement par la machine virtuelle dès lors qu’aucune variable ne contient sa référence. Le programmeur n’a donc pas à se soucier du recyclage de la mémoire.

Éditer et compiler ses programmes

Dans une application qui contient plusieurs classes, les bytecodes de ces classes sont regroupés dans un fichier ayant l’extension «.jar». On peut les produire en ligne de commande, via une console ou avec un script, mais Il existe des logiciels appelés IDE qui facilitent l’édition, la compilation et la mise au point des programmes, comme par exemple : NetBeans, Eclipse, IntelliJ IDEA, BlueJ (à vocation pédagogique !)… Je vous conseille d’en utiliser un.

Exercices de découverte

B-dec-10

Quels éléments syntaxiques (notation) permettent de savoir si une variable est une variable de méthode, une variable d’instance (aussi appelée attribut) ou une variable de classe dans un code Java ?

"Solution"

Les variables de méthodes sont déclarées uniquement à l’intérieur d’une méthode. La déclaration d’une variable de classe comporte le mot clé static. Les variables déclarées à l’extérieur d’une méthode sans le mot clé staticsont des variables d’instance, aussi appelée attributs.

B-dec-11

Quels éléments syntaxiques (notation) permettent de savoir si une méthode est une méthode de classe ou une méthode d’instance ?

"Solution"

La définition d’une méthode de classe comporte le mot clé static, contrairement à celle d’une méthode d’instance.

B-dec-12

On considère la classe Date définie précédemment et la classe Main suivante.

public class Main
{
    public static void main(String[] args)
    {
        Date d = new Date();
        d.annee = 1905;
        System.out.println(d.toString());
    }
}

Il y a une erreur. Trouvez là sans utiliser d’IDE ni de compilateur Java.

"Solution"

L’attribut annee de la classe Date est private. la méthode main est dans une autre classe et ne peut donc pas accéder à cet attribut.

B-dec-13

Ajoutez dans la classe Date un constructeur acceptant un seul paramètre de type int et qui permet de créer une instance de Date représentant le premier janvier de l’année passée en paramètre.

"Solution"

public Date(int annee)
{
    this.jour = 1; this.mois = 1; this.annee = annee;
}


B2 : Accesseurs et bonnes pratiques

A l’instar de ce qui est fait dans notre classe Date, le fait de rendre private les variables d’instance est considéré comme une bonne pratique en programmation objet. Plutôt que de permettre aux « utilisateurs » de la classe d’accéder directement à ces variables, on préfère mettre à sa disposition des méthodes publiques appelées accesseurs.

Voici les accesseurs de la classe Date qui ont été produits automatiquement par mon IDE.

 int getJour()
 {
   return jour;
 }
 
 public int getAnnee()
 {
   return annee;
 }
 
 public int getMois()
 {
   return mois;
 }
 
 public void setAnnee(int annee)
 {
   this.annee = annee;
 }
 
 public void setMois(int mois)
 {
   this.mois = mois;
 }
 
 public void setJour(int jour)
 {
   this.jour = jour;
 }

Avec cette approche, on pourrait changer la représentation en mémoire d’une date, par exemple en utilisant un seul entier qui combinerait le jour, le mois et l’année avec une formule du genre (d = année x 10000 + mois x 100 + jours) sans avoir à changer quoi que ce soit dans les méthodes utilisant la classe Date. Il suffirait de modifier le code des accesseurs pour que tout fonctionne correctement.

Par exemple getMois() retournerait (d/100)%10000 et setMois(int m) effectuerait l’opération d = getAnnee()*10000 + m*100 + getJour().

Exercices de découverte

B-dec-20

Réalisez une méthode d’accès setDate permettant de modifier simultanément le jour, le mois et l’année de l’instance courante de la classe Date. Cette méthode doit avoir trois paramètre nommés jour, mois et annee.

"Solution"

public void setDate(int jour, int mois, int annee)
 {
   this.jour = jour; this.mois = mois; this.annee = annee;
 }

B-dec-21

Donnez les lignes de code permettant de créer une instance de Date avec le constructeur par défaut de la classe Date, puis de modifier cette instance à l’aide de la méthode setDate de façon à ce que la date représentée soit le 12/10/2005.

"Solution"

Date d = new Date();
d.setDate(12, 10, 2005);


B3 : Composition de classes

On parle de composition de classes lorsqu’une classe a des attributs ayant pour types d’autres classes. Par exemple on peut définir une classe Periode dont les attributs désignent deux instances de Date et une instance de String.

public class Periode
 {
   private Date debut;
   private Date fin;
   private String nom;
 

  // À compléter
 }

Chacun des deux attributs debut et fin peut contenir une référence d’une instance de Date. Ces instances ont vocation à représenter le début et la fin d’une période constituée d’une ou plusieurs journées. Le troisième attribut est de type String. Il a vocation à contenir la référence d’une instance de String, c’est-à-dire une chaîne de caractères qui donne un nom à la période représentée.

On peut ajouter dans notre classe le constructeur suivant…

public Periode(Date deb, Date fin, String nom)
 {
   this.debut = deb;
   this.fin = fin;
   this.nom = nom;
 }

…puis la méthode toString suivante…

public String toString()
 {
   return "[ " + nom + " " + debut + " - " + fin + " ]";
 }

…et une méthode permettant de tester la classe…

public static void test()
 {
   Date d1 = new Date(4, 9, 2018);
   Date d2 = new Date(19, 9, 2018);
   Periode p = new Periode(d1, d2, "Vacances");
   System.out.println(p.toString());
 }

Notez bien que test est une méthode de classe qui ne nécessite pas d’objet de type Periode pour être appelée, et qui créée elle-même un objet de ce type. Pour appeler cette méthode test, par exemple depuis une méthode main, on écrit simplement…

Periode.test();

L’affichage obtenu est…

[ Vacances 4/9/2018 - 19/9/2018 ]

Voici ce qui se passe dans la mémoire…

image3

Exercices de découverte

B-dec-30

On considère deux variantes d’un constructeur de la classe Periode. En pratique, il faudra en choisir une car on ne peut implémenter dans une même classe deux constructeurs ayant exactement les mêmes paramètres.

/// Version 1
public Periode(Periode m)
{
    this.debut = m.debut;
    this.fin = m.fin;
    this.nom = m.nom;
}
/// Version 2
public Periode(Periode m)
{
    this.debut = new Date(m.debut.getJour(), m.debut.getMois(), m.debut.getAnnee());
    this.fin = new Date(m.fin.getJour(), m.fin.getMois(), m.fin.getAnnee());
    this.nom = m.nom;
}

On considère les lignes de code suivantes.

Date d1 = new Date(4, 9, 2018);
Date d2 = new Date(19, 9, 2018);
Periode p = new Periode(d1, d2, "Vacances");
Periode q = new Periode(p);
d1.setJour(5);
System.out.println(p.toString());
System.out.println(q.toString());

Donnez les affichages obtenus dans le cas où la version 1 du constructeur à 1 paramètre est utilisée et dans le cas où la version 2 est utilisée.

"Solution"

Version 1 :

[Vacances 5/9/2018 - 19/9/2018]
[Vacances 5/9/2018 - 19/9/2018]

Version 2 :

[Vacances 5/9/2018 - 19/9/2018]
[Vacances 4/9/2018 - 19/9/2018]

Pour bien comprendre ce résultat, il faut dessiner la configuration en mémoire dans les deux scénarios. Avec la version 1 du constructeur, il n’y a au total que deux objets de type Date dans le tas. Avec la version 2, on a deux new Date(...) en plus et au total 4 objets de type Datedans le tas. Dans le premier cas, les attributs debut des instances de Periode désignées par p et q pointent sur une même instance de Date. Si on la modifie, cela se répercute sur les deux périodes. Dans le deuxième cas, les attributs debut des instances de Periode désignées par p et q pointent sur deux instances de Date distinctes. Modifier l’une d’elle n’a pas d’effet sur l’autre.


B4 : Composition par tableaux

Dans cet exemple, nous créons une classe Memo qui contient plusieurs dates stockées dans un tableau. Cette classe a surtout une vocation pédagogique : montrer comment gérer un tableau référencé par une variable d’instance et comment les instances de la classe concernée sont représentées en mémoire.

public class Memo
 {
   private Date[] tab;
   private int nDates;


   // À compléter
 }

Une instance de cette classe représente une liste de dates, chacune représentée par une instance de Date. Un tableau est utilisé à cette fin, dans un but plus pédagogique que pratique. Examinons d’abord le constructeur de la classe…

public Memo(int n)
 {
   tab = new Date[n]; nDates=0;
 }

Ce constructeur accepte en paramètre la capacité du mémo, c’est-à-dire le nombre maximum de dates qu’il va pouvoir contenir. Cela correspond à la taille du tableau désigné par l’attribut tab. L’attribut nDate indique le nombre de dates effectivement stockées dans le mémo. Cet attribut est donc initialisé à 0, puisqu’à sa création le mémo est vide. Incidemment, chaque cellule du tableau créé contient la référence null.

Pour ajouter une date à une instance de Memo, on adjoint à la classe une méthode add définie de la manière suivante…

public void add(Date d)
{
   if(nDates >= tab.length)
   {
     System.out.println("Tableau plein !");
   }
   else
   {
     tab[nDates] = d; nDates++;
   }
}

On passe en argument à add la référence d’une instance de Date à ajouter au mémo. Mais comme le tableau de stockage a une taille limitée, déterminée à sa création, on vérifie qu’il reste encore de la place en comparant le nombre de dates enregistrées avec la longueur (tab.length) du tableau de stockage.

Dans le cas où il reste de la place, la référence de l’instance de Date à ajouter est placée dans la première cellule libre du tableau, qui a pour indice la valeur de l’attribut nDate. Cette valeur est ensuite augmentée de 1 pour refléter le nouveau nombre de dates stockées dans le mémo.

Ajoutons à notre classe Memo une méthode d’affichage…

public void print()
{
   for(int i=0; i<nDates; i++)
   {
     System.out.println(tab[i].toString());
   }
}

Puis testons ce que nous avons programmé avec la méthode suivante, qui peut se trouver dans la classe Memo ou ailleurs.

public static void test()
{
   Memo m = new Memo(5);
   m.add(new Date(12,10,2017));
   m.add(new Date(14,12,2017));
   m.print();
}

Voici la situation en mémoire à la fin de l’exécution de cette méthode.

image4

Exercices de découverte

B-dec-40

Réalisez une méthode d’instance read de la classe Memo permettant de récupérer la référence de la date située à une position donnée en argument dans l’instance courante de Memo. Si m désigne une instance de Memoet i est un entier positif ou nul alors l’appel m.read(i) doit retourner…

  • la référence de la date située en position i dans le mémo s’il contient au moins i+1 dates,
  • la valeur null dans le cas contraire.

Les positions sont numérotées à partir de 0 comme il est d’usage en programmation.

"Solution"

public Date read(int i)
{
    if(i >= nDates) return null;
    else return this.tab[i];
}

B-dec-41

Réalisez un constructeur de la classe Memo permettant de créer un mémo contenant exactement deux dates.

public Memo(Date d1, Date d2)
{
    // À compléter
}
"Solution"

public Memo(Date d1, Date d2)
{
    tab = new Date[2]; nDates=2;
    tab[0] = d1;
    tab[1] = d2;
}


B5 : La classe standard ArrayList

Comme nous l’avons vu dans l’exemple de la classe memo, l’utilisation d’un tableau comporte une limitation assez embêtante : sa taille doit être fixée une fois pour toute lors de sa création. Pour éviter ce problème, on peut utiliser une classe nommée ArrayList permettant de faire des listes extensibles.

Cette classe est générique, dans le sens où elle peut être paramétrée par un type. ArrayList<T> représente une liste de références d’instances de T. Une telle liste est plus souple d’utilisation qu’un tableau car sa capacité n’est pas limitée (du moins elle n’est limitée que par les ressources mémoire allouées à la machine virtuelle java).

Exemple introductif

Examinons un exemple. Je veux créer une liste de dates (techniquement, d’instances de la classe Date). J’écris simplement…

ArrayList<Date> t = new ArrayList<>();

Une liste vide est créée dans le tas et sa référence est placée dans w. Cette liste à une capacité initiale de 10 emplacements (valeur par défaut allouée par le compilateur), mais cette capacité sera automatiquement augmentée si on y place plus de 10 éléments.

J’ajoute deux dates à ma liste…

t.add(new Date(1,1,1901));
t.add(new Date(2,2,1902));

Examinons ce qui s’est passé dans la mémoire.

image5

La représentation d’une instance d’ArrayList a été simplifiée (tout comme celle des instances de String que nous avons eu l’occasion de voir précédemment). En réalité, une instance d’ArrayList comporte plusieurs attributs dont un désigne un tableau qui pourra être remplacé par un autre de taille différente à chaque fois que c’est nécessaire. Mais puisque cette classe a été créée pour nous simplifier la vie, autant en simplifier la représentation.

Précisions sur la classe ArrayList<T>

L’essentiel à savoir concernant la classe ArrayList<T>, c’est que :

  • toute instance w contient w.size() éléments,
  • tout élément est soit une référence de type T, soit null.
  • la position de chaque élément est désignée par un indice,
  • le premier élément a pour indice 0.

Voici quelques méthodes de la classe ArrayList. Il y en a beaucoup d’autres, que vous pourrez trouver dans la documentation officielle à l’URL https://docs.oracle.com/javase/8/docs/api/

  • int size() : retourne le nombre d’éléments stockés.
  • boolean add(T e) : ajoute l’élément e en fin de liste. La valeur de retour est toujours true. Elle peut être ignorée.
  • void add(int i, T e) : ajoute l’élément e en position i et décale (si applicable) les éléments suivants d’une position. La plus grande valeur autorisée pour i est size().
  • T get(int i) : retourne l’élément situé en position i. La plus grande valeur autorisée pour i est size()-1.
  • T remove(int i) : retire l’élément situé en position i et le retourne. Les éléments suivants (si applicable) sont décalés d’une position pour boucher le « trou ».
  • int indexOf(Object e) : retourne l’indice de la position de la première occurrence de l’objet e dans la liste, ou -1 si cet objet n’est pas dans la liste. (Toute référence a le type Object par héritage.)
  • T set(int i, T e) : remplace l’élément situé en position i par e.
  • void clear(int i) : retire tous les éléments de la liste.

Un exemple d’utilisation

Soit une classe Personne qui représente une personne et dispose d’une méthode toString permettant d’afficher le nom et le prénom de la personne représentée.

public class Personne
{
  private String nom;
  private String prenom;
  
  public Personne(String nom, String prenom)
  {
    this.nom = nom; this.prenom = prenom;
  }
  
  publis String toString()
  {
    return nom + " " + prenom;
  }
}

Voici une classe Document dont chaque instance représente un document ayant un nom et des auteurs. Voici les attributs de la classe Document.

public class Document
 {
   private String nom;
   private ArrayList<Personne> auteurs;

   // À compléter
}

Pour initialiser ces attributs, il nous faut un constructeur. En voici un…

public Document(String n, Personne[] a)
{
   this.nom=n;
   auteurs = new ArrayList<Personne>();
   for(int i=0; i<a.length; i++)
   {
     auteurs.add(a[i]);
   }
}

Il permet de créer un document en une seule fois, à partir des données reçues en argument, à savoir la référence n d’une chaîne de caractères et un tableau a de références d’instances de la classe Personne. Cela suppose qu’au moment d’utiliser ce constructeur, on dispose de ces données qui ont été préalablement créées.

La première ligne…

  this.nom=n;

…place juste dans l’attribut nom la référence d’instance de String contenue dans n.

La ligne suivante…

  auteurs = new ArrayList<Personne>();

… appelle le constructeur par défaut de ArrayList<Personne> pour créer une liste vide à laquelle on pourra ajouter autant des références à des instances de Personne que désiré. L’opérateur new retourne la référence de la liste créée, qui est placée dans l’attribut auteurs.

Ensuite, une boucle for

  for(int i=0; i<a.length; i++)
   {
     auteurs.add(a[i]);
   }

… parcourt le tableau passé en argument et ajoute chacune des références qu’il contient à la liste des auteurs du document en cours de construction.

Nous allons ajouter une méthode permettant de récupérer le nombre d’auteurs d’un document…

public int nbAuteurs()
 {
   return auteurs.size();
 }

… qui fait bêtement appel à une méthode de la classe ArrayList retournant le nombre d’éléments de l’instance d’ArrayList concernée.

La méthode suivante, toString, permet de créer une chaîne de caractères donnant une description de l’instance courante de Document.

public String toString()
{
   String da = "";
   for (int i = 0; i < nbAuteurs(); i++)
   {
     da = da + auteurs.get(i) + " ";
   }
   return nom + "\nAuteur(s) : " + da;
}

Notez au passage le moyen utilisé pour récupérer les informations sur les auteurs mérite quelques explications. La ligne…

da = da + auteurs.get(i) + " ";

…récupère la référence désignant l’élément situé en position i dans la liste. Jusque-là rien d’étonnant. Mais ensuite cet élément, qui est de type Personne (entendez par là qu’il s’agit d’une référence d’une instance de Personne), est ajouté à la chaîne en cours de construction grâce à l’opérateur +. Mais attendez, une instance de Personne n’est pas une chaîne de caractère ! Comment Java fait-il pour ajouter une instance de Personne à une chaine de caractères ?

En réalité, dans un tel contexte java remplace implicitement auteurs.get(i) par auteurs.get(i).toString(). C’est une sorte de notation raccourcie qui permet d’ajouter un objet à une chaîne, et qui fonctionne bien si l’objet en question dispose d’une méthode toString correctement programmée. Dans le cas contraire, c’est une chaîne représentant la référence de l’objet concerné qui est ajoutée à la chaîne.

Bon, notre classe Document n’offre pas encore beaucoup de possibilités, mais elle permet de créer et d’afficher des objets de type Document, à condition toutefois de programmer une classe Personne disposant à minima d’un constructeur et d’une méthode toString. Une personne pourrait par exemple être décrite par deux attributs de type String représentant son nom et son prénom.

La création d’une instance de Document peut alors prendre la forme suivante…

Personne auteur1 = new Personne("Brian","Kernighan");
Personne auteur2 = new Personne("Dennis","Ritchie");
Document doc = new Document("Le langage C",new Personne[]{auteur1, auteur2});

La syntaxe…

Personne[]{auteur1, auteur2}

…permet de créer un tableau d’instances de Personne et de l’initialiser en même temps.

Exercices de découverte

B-dec-50

Ajoutez à la classe Document un constructeur qui crée une instance représentant un document sans auteur (la liste d’auteurs est vide), mais ayant un nom.

public Document(String nom)
{
    // À compléter
}
"Solution"

public Document(String nom)
{
    this.nom = nom;
    this.auteurs = new ArrayList<Personne>();
}   

B-dec-51

Ajoutez à la classe Document une méthode addAuteur permettant d’ajouter un auteur au document courant.

public void addAuteur(Personne p)
{
     // À compléter
}
"Solution"

public void addAuteur(Personne p)
{
     this.auteurs.add(p);
}

B-dec-52

Ajoutez à la classe Document une autre méthode addAuteur permettant d’ajouter un auteur au document courant.

public void addAuteur(String nom, String Prenom)
{
     // À compléter
}
"Solution"

public void addAuteur(String nom, String Prenom)
{
     this.auteurs.add(new Personne(nom, prenom));
}


Exercices d’assimilation

B-ass-00

Réalisez une classe Compteur qui comporte un attribut (variable d’instance) cpt de type int, un constructeur par défaut qui initialise cette variable à 0, une méthode d’instance plusUn qui incrémente cette variable, et une méthode d’instance lecture qui retourne la valeur de la variable cpt.

Réalisez une méthode main située dans une classe Main qui crée une instance de Compteur, réalise trois incrémentations de cette instance et affiche sa valeur (i.e., la valeur de sa variable cpt).

"Indice"

public class Compteur
{
    private int cpt;

    public Compteur()
    {
        // À compléter
    }

    public void pluUn()
    {
        // À compléter
    }

    public int lecture()
    {
        // À compléter
    }

    public static void main(String[] args)
    {
        Compteur c = new Compteur();
        // À compléter
    }
}

"Solution"

public class Compteur
{
    private int cpt;

    public Compteur()
    {
        cpt = 0;
    }

    public void pluUn()
    {
        cpt = cpt + 1;
    }

    public int lecture()
    {
        return cpt;
    }

    public static void main(String[] args)
    {
        Compteur c = new Compteur();

        c.pluUn(); c.pluUn();; c.pluUn();
        System.out.println(c.lecture());
    }
}

B-ass-01

Réécrivez à la classe Memo (en vous limitant aux attributs, au constructeur à un paramètre et aux méthodes add et print) en remplaçant le tableau par un ArrayList.

"Indice"

public class MemoExt
{
    private ArrayList<Date> tab;

    public MemoExt()
    {
        tab = new // À compléter
    }

    public void add(Date d)
    {
        // À compléter
    }

    public void print()
    {
        for(int i=0; i<tab.size(); i++)
        {
            // À compléter
        }
    }
}

L’attribut nDate n’est plus nécessaire car le nombre de dates stockées peut être obtenu avec la méthode size de la classe ArrayList.

"Solution"

public class MemoExt
{
    private ArrayList<Date> tab;

    public MemoExt()
    {
        tab = new ArrayList<>();
    }

    public void add(Date d)
    {
        tab.add(d);
    }

    public void print()
    {
        for(int i=0; i<tab.size(); i++)
        {
            System.out.println(tab.get(i).toString());
        }
    }
}

B-ass-02

Réalisez une classe Point qui représente les coordonnées (x,y) d’un point dans un repère cartésien à 2 dimensions. Les coordonnées x et y sont représentées par des nombres en virgule flottantes de type double. La classe Point dispose d’un constructeur permettant de créer un point à partir de ses coordonnées, ainsi que d’une méthode toString, et deux accesseurs getX et getY permettant de récupérer les coordonnées. Elle possède aussi une méthode d’instance public double dist(Point p) qui calcule et retourne la distance entre le point courant et le point désigné par le paramètre p.

La méthode de classe prédéfinie double Math.sqrt(double x), qui retourne la racine carrée de x, vous sera utile.

"Indice"

public class Point
{
    private double x;
    private double y;

    public Point(double x, double y)
    {
        // À compléter
    }

    public double getX() {return x;}

    public double getY() {return y;}

    public String toString()
    {
        // À compléter
    }

    public double dist(Point p)
    {
        double dx = // À compléter
        double dy = // À compléter
        return Math.sqrt(dx*dx + dy*dy);
    }
}

"Solution"

public class Point
{
    private double x; private double y;

    public Point(double x, double y)
    {
        this.x = x; this.y = y;
    }

    public double getX() {return x;}

    public double getY() {return y;}

    public String toString()
    {
        return "(" + x + "," + y + ")";
    }

    public double dist(Point p)
    {
        double dx = this.x - p.x;
        double dy = this.y - p.y;
        return Math.sqrt(dx*dx + dy*dy);
    }
}

B-ass-03

Réalisez une classe Polygone qui représente un polygone sous la forme d’une liste de points. Cette classe dispose d’un constructeur à trois paramètres de type Point qui produit un triangle et un autre à 4 paramètres de type Point qui produit un quadrilatère. Elle dispose également d’une méthode d’instance perimetre qui retourne la somme des longueurs des côtés du polygone courant et d’une méthode toString qui produit une chaîne indiquant juste le nombre de cotés et le périmètre du polygone courant.

"Indice

public class Polygone
{
    private ArrayList<Point>  liste;

    public Polygone()
    {
        // À compléter
    }

    public Polygone(Point a, Point b, Point c)
    {
        this(); // appelle le constructeur par défaut
        // À compléter
    }

    public Polygone(Point a, Point b, Point c, Point d)
    {
        this(a, b, c); // appelle le contsructeur de triangle
        // À compléter
    }

    public double perimetre()
    {
        // À compléter
    }

    public String toString()
    {
        // À compléter
    }
}

Bien que ce ne soit pas obligatoire, dans cette solution partielle, certains constructeurs utilisent this(...) pour appeler un autre constructeur de manière à éviter de refaire ce que cet autre constructeur sait déjà faire. Ceci évite de répliquer du code identique dans plusieurs constructeurs. Lorsqu’un constructeur en appelle un autre, il faut que cet appel soit réalisé par sa première ligne de code.

"Indice

Voici le code complet des constructeurs et quelques élément de la méthode perimetre. Il vous reste à terminer cette méthode et écrire ma méthode toString.

public Polygone()
{
    liste = new ArrayList<>();
}

public Polygone(Point a, Point b, Point c)
{
    this();
    liste.add(a); liste.add(b);  liste.add(c);
}

public Polygone(Point a, Point b, Point c, Point d)
{
    this(a, b, c); liste.add(d);
}

public double perimetre()
{
    double sum = 0.0;
    for(int i=1; i<liste.size(); i++)
    {
        sum = sum + // À compléter
    }
    sum = sum + // À compléter;
    return sum;
}

"Solution"

public class Polygone
{
    private ArrayList<Point>  liste;

    // Constructeurs (voir indice 1)

    public double perimetre()
    {
        double sum = 0.0;
        for(int i=1; i<liste.size(); i++)
        {
            sum = sum + liste.get(i-1).dist(liste.get(i));
        }
        sum = sum + liste.get(0).dist(liste.get(liste.size()-1));
        return sum;
    }

    public String toString()
    {
        String s = "Polygone de " + liste.size() + " sommets";
        return s + " de périmètre " + perimetre();
    }
}

B-ass-04

Cet exercice fait suite à l’exercice précédent. Vous devez ajouter à la classe Polygone une méthode de classe (static) triangle qui crée et retourne un polygone à trois sommets dont les coordonnées lui sont passées en argument. cette méthode aura donc 6 paramètres de type double.

Vous devez également réaliser une méthode main qui crée une instance de Polygone représentant un triangle en appelant la méthode triangle et qui affiche à l’écran la description de ce triangle obtenue par un appel de la méthode toString de la classe Polygone.

"Indice"

public static Polygone triangle(double x1, double y1, double x2, double y2, double x3, double y3)
{
    Point a = // À compléter
    Point b = // À compléter
    Point c = // À compléter
    return new // À compléter
}
public static void main(String[] args)
{
    Polygone p = // À compléter
    System.out.println(p);
}

La syntaxe la plus stricte pour la deuxième ligne de main devrait être System.out.println(p.toString());. Mais lorsque la référence d’un objet est utilisée à la place d’une chaîne, le compilateur java appelle automatiquement la méthode toString de la classe concernée.

"Solution"

public static Polygone triangle(double x1, double y1, double x2, double y2, double x3, double y3)
{
    Point a = new Point(x1, y1);
    Point b = new Point(x2, y2);
    Point c = new Point(x3, y3);
    return new Polygone(a, b, c);
}
public static void main(String[] args)
{
    Polygone p = Polygone.triangle(0.0, 0.0, 1.0, 0.0, 0.0, 1.0);
    System.out.println(p);
}

B6 : Objets modifiables et non modifiables

Nous revenons ici sur une notion que nous avons déjà abordée sans la nommer dans la partie A. On l’appelle effet de bord. Prenons un exemple avec la classe Date.

public static void effetDeBord()
 {
   Date d1 = new Date(11,11,1918);
   Date d2 = d1;
   d2.setAnnee(1919);
   System.out.println(d1);
 }

On définit deux variables d1 et d2. On place dans d2 le contenu de d1, puis on change l’année de d2 et on affiche d1. Résultat…

11/11/1919

La modification de la date désignée par d2 a changé aussi celle désignée par d1. Ce n’est pas étonnant quand on regarde la configuration des données en mémoire.

image6

Les deux variables de méthode d1 et d2 contiennent la même référence, qui désigne la même instance de Date. Pour éviter un tel effet de bord, on peut par exemple doter la classe Date d’un constructeur en copie ou d’une méthode de clonage. Concentrons-nous sur la première solution.

public Date(Date d)
 {
   this.jour = d.jour;
   this.mois = d.mois;
   this.annee = d.annee;
 }

Voici comment utiliser ce constructeur pour dupliquer une instance de Date.

public static void pasDeffetDeBord()
 {
   Date d1 = new Date(11,11,1918);
   Date d2 = new Date(d1);
   d2.setAnnee(1919);
   System.out.println(d1);
 }

Cette fois-ci, la modification de la date désignée par d2 n’a pas d’effet sur celle désignées par d1. Voici la nouvelle configuration en mémoire.

image7

Une autre manière de permettre la duplication d’instance de Date, applicable à toute classe, comme la technique du constructeur en copie, consiste à ajouter à la classe Date une méthode de clonage.

public Date clone()
 {
   return new Date(this.jour, this.mois, this.annee);
 }

Voici un exemple d’utilisation.

public static void pasDeffetDeBord2()
 {
   Date d1 = new Date(11,11,1918);
   Date d2 = d1.clone();
   d2.setAnnee(1919);
   System.out.println(d1);
}

Exercices de découverte

B-dec-60

Réalisez une méthode d’instance de la classe Date retournant la date située une année plus tard que la date courante, sans modifier la date courante.

    public Date plusUnAn()
    {
        Date cpy = new // À compléter
        // À compléter
        return cpy;
    }
"Solution"

    public Date plusUnAn()
    {
        Date cpy = new Date(this);
        cpy.setAnnee(getAnnee()+1);
        return cpy;
    }

Il y a d’autres solutions valables, telles que celle-ci.

    public Date plusUnAn()
    {
        Date cpy = new Date(this.getJour(), this.getMois(), this.getAnnee() + 1);
        return cpy;
    }

B-dec-61

Réalisez une variante de la première variante de la méthode d’instance plusUnAn qui utilise la méthode de clonage de la classe Date au lieu de son constructeur en copie.

"Solution"

    public Date plusUnAn()
    {
        Date cpy = clone(); // ou this.clone()
        cpy.setAnnee(getAnnee()+1);
        return cpy;
    }

B7 : Les constantes

Le mot final rend une variable non modifiable et la transforme donc en constante. Sauf que… Examinons un exemple. image8

On ne peut pas modifier les références représentées par jours et Armistice, mais cela n’empêche pas de modifier les objets désignés par ces références. Par exemple on peut écrire…

jours[6]="sun";
Armistice.setAnnee(1919);

Ce qui modifiera les objets concernés.

Exercices de découverte

B-dec-70

Pour afficher le nombre $\pi$, on peut écrire :

System.out.println(Math.PI);

Donnez la déclaration de la variable d’instance PI dans le classe Math.

"Solution"

public static final double PI = 3.141592653589793;

B8 : Les classes enveloppes

En java, les types primitifs ne sont pas des objets. Ils constituent une sorte d’entorse au paradigme de programmation objet. Mais il existe des classes enveloppes qui permettent d’encapsuler des données de types primitifs dans des objets : Integer, Double, Boolean… Ces types enveloppe peuvent être utiles pour utiliser des données primitives dans de situation où seuls des objets sont utilisables, comme dans des ArrayList. Voici un exemple.

image9

Exercices de découverte

B-dec-80

Complétez cette illustration de la configuration en mémoire à l’issue de l’exécution des 4 premières lignes de code de l’exemple précédent.

image-20210905232113983

"Solution"

image-20210905232223671

B9 : Objets non modifiables

Les objets n’ayant que des attributs privés et ne disposant pas de méthodes permettant de modifier leurs attributs (ni les données accessibles par ces attributs) sont dits non modifiables, tout comme les classes qui définissent de tels objets.

Par exemple, les classes enveloppes sont non modifiables, ainsi que la classe String. L’intérêt de telles classes c’est qu’elles empêchent les effets de bord, mais en contrepartie d’une perte d’efficacité. On peut le voir en considérant deux classes permettant de représenter des chaînes : String, qui est non modifiable, et StringBuffer, qui est modifiable.

La méthode de classe suivante…

public static String makeString1(String s, int n)
 {
   String r = "";
   for(int i=0; i<n; i++)
   {
     r = r + s;
   }
   return r;
 }

…permet de construire une chaîne par n concaténations successives d’une chaîne s. Par exemple…

System.out.println(makeString1("abcdefgh",10));

… affiche…

abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh

Mais cette méthode n’est pas très efficace. Pour fabriquer la chaîne retournée, cette méthode crée successivement n nouvelles chaînes intermédiaires qui sont ensuite recyclées par le ramasse-miettes de la machine virtuelle. En effet, chaque concaténation réalisée par l’opérateur + ne modifie pas une chaîne existante (puisque les chaînes sont des objets non modifiables) mais en crée une nouvelle.

image10

On peut réaliser la même chose en utilisant la classe modifiable StringBuffer qui dispose d’une méthode append permettant d’ajouter une chaîne à la fin de la chaîne courante sans en recréer une nouvelle.

public static String makeString2(String s, int n)
 {
   StringBuffer r = new StringBuffer();
   for(int i=0; i<n; i++)
   {
     r.append(s);
   }
   return r.toString();
 }

Lors de la création d’une instance de StringBuffer, une certaine ressource de mémoire est réservée pour le stockage de la chaîne courante. Cette quantité de mémoire est augmentée en fonction des besoins.

image11

Exercices de découverte

B-dec-90

À quoi peut-on reconnaître une classe non modifiable en regardant son code ?

"Solution"

Une classe est non modifiable si et seulement si aucune de ses méthodes ne permet de changer la valeur d’un des attributs d’un objet après sa création.

Exercices d’assimilation

B-ass-50

Réalisez un constructeur en copie pour la classe Memo (en version ArrayList comme demandé par l’exercice 2). Ce constructeur doit accepter en paramètre une instance de Memo déjà créée et faire en sorte que l’instance construite soit identique, mais que toute modification de la copie créée (ajout ou modification d’une date) n’affecte pas l’original et réciproquement.

"Indice

public Memo(Memo m)
{
    this(); // Pour créer la liste vide
    for(int i=0; i<tab.size(); i++)
    {
        tab.add(// À compléter);
    }
}

"Indice

public Memo(Memo m)
{
    this();
    for(int i=0; i<tab.size(); i++)
    {
        tab.add( new Date(// À compléter) );
    }
}

Il est important de créer de nouvelles instances de Date identiques à celles qui sont dans le mémo servant de modèle. Si on plaçait dans la liste du nouveau mémo les références des dates du mémo servant de modèle, une modification d’une de ces dates affecterait les deux mémos, parce que Date est une classe modifiable. Le problème ne se poserait pas si c’était une classe non modifiable.

"Solution"

public Memo(Memo m)
{
    this();
    for(int i=0; i<tab.size(); i++)
    {
        tab.add( new Date(m.tab.get(i)) );
    }
}

Voici une variante qui fonctionne aussi avec un code un peu plus propre.

public Memo(Memo m)
{
    this();
    for(Date e : tab)
    {
        tab.add( new Date(e) );
    }
}

Il serait encore plus conforme aux bonnes pratiques de mettre dans la classe Memo une méthode permettant de récupérer la date située à une position passée en argument, et d’utiliser cette méthode au lieu d’accéder à l’attribut tab. Toutefois, l’accès à cet attribut privé est parfaitement légal puisque le modificateur d’accès private privatise un attribut dans le scope d’une classe et non de chacune des instances de cette classe : une méthode d’instance peut accéder aux attributs privés d’une autre instance, si elle a accès la référence de cette autre instance.

B-ass-51

Ajoutez à la classe Polygone une méthode…

public Polygone ext(Point p)

…qui produit un nouveau polygone constitué des points du polygone courant plus le point désigné par le paramètre p. L’ancien et le nouveau polygone doivent exister indépendamment l’un de l’autre. Pour rendre le code plus lisible, vous devrez définir un constructeur en copie de la classe Polygone, qui doit appeler le constructeur par défaut de cette classe. Notez que la classe Point n’est pas modifiable.

"Indice"

Voici un constructeur en copie pour la classe Polygone.

public Polygone(Polygone m)
{
    this();
    for(Point p : m.liste)
    {
        liste.add(p);
    }
}

Il reste à écrire la méthode ext.

"Solution"

// Constructeur en copie donné dans l'indice 1

public Polygone ext(Point p)
{
    Polygone poly = new Polygone(this);
    poly.liste.add(p);
    return poly;
}

Exercices d’approfondissement

B-cons-00

Vous devez développer un programme permettant de simuler un système d’offres par soumissions cachetées pour l’achat d’objets identiques. Il y a N objets en vente. Il peut y avoir plus (ou moins, ou autant) d’acheteurs intéressés que d’objets en vente. Chaque acheteur peut faire une ou plusieurs offres qui ne sont pas connues par les autres acheteurs. Les N meilleures offres seront retenues.

Chaque offre est représentée par une instance de la classe Offre suivante.

public class Offre
{
    public int id;
    public double montant;

    public Offre(int id, double montant)
    {
        this.id = id; this.montant = montant;
    }

    public int getId() {return id;}

    public double getMontant() {return montant;}

    public String toString()
    {
        return "[ " + id + " : " + montant + "]";
    }
}

Les offres sont placées dans une instance de la classe PrioFile, que vous devez implémenter. Une telle instance est appelée file de priorité. Elle contient une liste d’offres. Pour créer une file de priorité, on écrit…

PrioFile file = new PrioFile(n);

…où nest le nombre d’objets en vente. La file de priorité créée comportera au plus n offres, chacune représentée par une instance de la classe Offre. Tant que la file contient moins de n offres, chaque nouvelle offre y est automatiquement ajoutée. Au delà, quand une nouvelle offre est proposée, elle n’est ajoutée que si son montant dépasse celui de l’offre la plus récente parmi celles ayant le plus petit montant.

Pour soumettre une offre, on écrit…

file.soumission(id, montant);

…où id est l’identifiant d’une acheteur potentiel et montant est le montant proposé.

La classe PrioFile dispose d’une méthode toString qui affiche la liste complète des offres retenues.

Java en LP – Partie A

Partie A

Programmation procédurale en Java

Objectif

Savoir réaliser une application simple utilisant uniquement des méthodes statiques, des tableaux, des types scalaires, et des chaînes de caractères.

Prérequis

Une première expérience, même limitée, en programmation procédurale ou objet.

A1 : Un programme de découverte

Présentation

public class Decouverte
{
    public static void main(String[] args)
    {
        System.out.println("Bonjour");
    }
}

Tout programme est constitué de blocs qui s’imbriquent et / ou se succèdent. Ici nous avons deux blocs :

  • La classe Decouvertedont le code source doit être placé dans un fichier nommé Decouverte.java. Pour l’instant, nous pouvons voir une classe comme un un bloc contenant des méthodes et des variables. Les variables contiennent des données et les méthodes réalisent des traitements.
  • La méthode main est le point de départ de l’exécution de tout programme java.

image-20210804230252828

Pour exécuter ce programme, vous pouvez utiliser un IDE (environnement intégré de développement) tel que NetBeans, IntelliJ IDEA, Eclipse… Mais vous devez aussi maîtriser la compilation et l’exécution d’un programme Java via une console. Cette démarche comporte en trois étapes.

  1. Édition du programme avec l’éditeur de code source de votre choix.
  2. Compilation du fichier source avec la ligne de commande javac Decouverte.java. Cela suppose qu’un environnement de développement appelé Java SDK soit installé sur votre machine et que les variables d’environnement soit configurées de manière à ce que l’exécutable javac, c’est à dire le compilateur java, soit accessible. Le résultat de la compilation est un fichier Decouverte.class.
  3. Exécution du fichier résultant de la compilation par la ligne de commande java Decouverte.

Plusieurs mots-clés interviennent dans la définition de la méthode main.

public static void main(String[] args)
{
    ...
}
  • public : Toutes les méthodes du badge A seront public. Nous introduirons plus tard d’autres options, à partir de la partie B.
  • static : Toutes les méthodes de la partie A seront static. De telles méthodes sont aussi appelées méthodes de classe. Nous introduirons d’autres sortes de méthodes dans la partie B.
  • void : Signifie que la méthode main ne retourne rien.
  • main : La méthode principale d’un programme java porte toujours ce nom, tout comme elle est toujours public, static, et a toujours un paramètre de type String[].
  • Le paramètre String[] args peut permettre à la méthode main de prendre en compte des arguments passés par la ligne de commande lors de l’appel du programme depuis la console. Dans notre exemple, cette possibilité n’est pas exploitée.

image-20210805000336805

Exercices de découverte

A-dec-10

Répondez aux questions suivantes :

  1. Quelle est la première méthode exécutée par un programme Java ?
  2. Que retourne la méthode main?
  3. Quel est le rôle de la méthode System.out.println?
  4. Qu’est-ce que le SDK de Java ?
  5. Quel est le nom du compilateur java ?
"Solutions"

  1. main
  2. rien (void).
  3. afficher quelque chose à l’écran
  4. L’ensemble des ressources permettant de développer des programmes Java.
  5. javac

A2 : Un programme qui compte

Présentation

Voici notre deuxième programme. Autant que possible, testez sur machine les différents programmes qui seront présentés et tentez quelques modifications pour vérifier que vous êtes capable de prévoir leur effets. Le code des deux méthodes suivantes doit être placé dans la classe Decouverte.

    public static void compte(int k)
    {
        for(int i=0; i<k; i++)
        {
            System.out.println(i);
        }
    }
    public static void main(String[] args)
    {
        compte(10);
    }

La méthode main appelle la méthode compte en lui passant en argument la valeur 10. La méthode compte reçoit cette valeur dans la variable k qui est le paramètre de la méthode compte. Lors du début de l’exécution de compte, la variable k est initialisée avec la valeur 10.

Il y a dans le code de la méthode compte une autre variable nommée i, à laquelle l’instruction for donne successivement les valeurs de 0 à k. L’exécution de la méthode a donc pour effet l’affichage des valeurs 0 à 9, avec un passage à la ligne entre chaque valeur.

La méthode compte peut être réécrite en utilisant l’instruction while au lieu de for.

    public static void compte(int k)
    {
        int i = 0;
        while(i<k)
        {
            System.out.println(i); 
            i++;
        }
    }

Les variables k et i sont appelée variables de méthodes ou variables automatiques. Elles n’existent que pendant l’exécution de la méthode dans laquelle elle sont déclarée et seul cette méthode peut les utiliser. La variable k a un rôle particulier puisque c’est un paramètre de la méthode. Sa valeur initiale est déterminée lors de l’appel de la méthode.

Exercices de découverte

A-dec-20

Si on remplace System.out.println(i) par System.out.print(i + " ") dans la méthode compte, on obtient l’affichage 0 1 2 3 … k (les valeurs sont séparées par des espaces. Modifiez le programme pour obtenir l’affichage 0, 1, 2, 3, …, k (sans virgule après la dernière valeur affichée).

"Solution"

public static void compteBis(int k)
{
    for(int i=0; i<k; i++)
    {
        System.out.print(i);
        if(i < k-1) System.out.print(", ");
        else System.out.println();
    }
}

D’autre variantes sont possibles et correctes. Vous pouvez tester la votre sur machine.

A-dec-21

Réalisez une méthode void compte(int a, int b) qui affiche tous les entiers compris entre a et binclus, séparés par des espaces. Par exemple, l’appel compte(3, 6) doit afficher 3 4 5 6.

"Solution"

public static void compte(int a, int b)
{
    for(int i=a; i<=b; i++)
    {
        System.out.print(i + " ");
    }
    System.out.println();
}

A-dec-22

Complétez le code ci-dessous pour qu’un appel de la méthode couples(a,b) aie pour effet l’affichage de tous les couples de valeurs comprises entre a et b inclus. Par exemple, l’exécution de couples(4,5) devra afficher :

(4, 4)
(4, 5)
(5, 4)
(5, 5)
    public static void couples(int a, int b)
    {
        for(---------------------------)
        {
            for(---------------------------)
            {
                System.out.println("( " + i + " , " + j + " )" );
            }
        }
    }
"Solution"

public static void couples(int a, int b)
{
    for(int i=a; i<=b; i++)
    {
        for(int j=a; j<=b; j++)
        {
            System.out.println("( " + i + " , " + j + " )" );
        }
    }
}

A3 : Utiliser des tableaux

Présentation

En java comme dans bien d’autres langages de programmation, un tableau est une collection de données de même type stockées de manière contigüe en mémoire dans des cellules désignées par des indices. Tous les exemples de code qui seront introduits sont supposés se trouver dans la classe Decouverte.

Voici un exemple de création d’un tableau de 10 entiers.

private static int[] tab = new int[10];

Il y a beaucoup de choses à dire et à comprendre à propos de cette ligne. Tout d’abord, elle ne se trouve pas dans la définition d’une méthode, mais directement dans une classe, en l’occurrence la classe Decouverte. Comme sa déclaration n’est pas dans une méthode, tab n’est pas une variable de méthode. Il s’agit d’une variable de classe comme nous l’indique le mot clé static. Ceci signifie que ce tableau existe pendant toute la durée de l’exécution du programme et pas seulement pendant la durée d’exécution d’une méthode particulière comme c’est le cas pour une variable de méthode. Le modificateur d’accès private indique que seule les méthodes de la classe Decouverte y ont accès.

Ensuite il faut comprendre que la ligne de code ci-dessus peut se décomposer en deux lignes ayant chacune un rôle différent.

private static int[] tab;
tab = new int[10];

La première ligne déclare une variable nommée tab de type int[]. Cette variable est située dans une zone mémoire dédiée aux variables de classe. Cette variable peut contenir la référence d’un objet de type int[]. Techniquement, une référence est une adresse mémoire à laquelle est situé un objet dans une zone mémoire appelée tas. La notion d’objet sera détaillée plus tard. La seule chose que nous devons savoir à ce sujet pour le moment est qu’en java, tout tableau est un objet.

La deuxième ligne crée un tableau d’entiers de 10 cellules, situé dans le tas. Chaque cellule contient la valeur 0. Voici la représentation graphique du résultat.

image-20210806225410039

Nous allons maintenant réaliser une méthode permettant d’afficher le contenu d’un tableau d’entiers.

    public static void printTabInt(int[] t)
    {
        for(int i=0; i< t.length; i++)
        {
            System.out.print(t[i] + " ");
        }
        System.out.println();
    }

On voit que cette méthode a un paramètre t de type int[], c’est à dire une référence d’un tableau d’entiers. La notation t.length permet de récupérer la longueur du tableau t. La notation t[i] désigne la valeur située dans la cellule d’indice i du tableau t.

Voici un exemple d’appel de la méthode printTabInt depuis la méthode main.

    public static void main(String[] args)
    {
        tab[1] = 5; tab[3] = 7;
        printTabInt(tab);
    }

L’exécution de cette méthode a pour effet l’affichage 0 5 0 7 0 0 0 0 0 0.

Exercices de découverte

A-dec-30

Réalisez une méthode Rempli0123 qui accepte en paramètre la référence d’un tableau d’entiers et qui rempli ce tableau d’entiers avec les valeurs 0, 1, 2, etc. La valeur placée dans la dernière cellule devra donc être égale à la longueur du tableau moins 1.

"Solution"

public static void rempli0123(int[] t)
{
    for(int i=0; i< t.length; i++)
    {
        t[i] = i;
    }
}

Réalisez une méthode main qui teste la méthode Rempli0123 en remplissant le tableau désigné par la variable de classe tab que nous avons créé précédemment puis en affichant le contenu de ce tableau à l’aide de la méthode printTabInt.

"Solution"

public static void main(String[] args)
{
    rempli0123(tab); printTabInt(tab);
}

A4 : La pile et le tas

Présentation

Examinons la méthode suivante.

    public static int[] produitab(int a, int b)
    {
        int[] t = new int[b-a+1];
        for(int i=0; i< t.length; i++)
        {
            t[i] = a + i;
        }
        return t;
    }

Contrairement aux méthodes précédentes qui ne retournaient rien (void), celle-ci retourne la référence d’un tableau d’entiers (int[]) qui, incidemment, n’existe pas encore au moment de l’appel de la méthode. Examinons les différents blocs.

Le premier bloc est la ligne suivante.

				int[] t = new int[b-a+1];

Elle a pour effet de déclarer une variable de méthode t et d’y placer la référence d’un tableau d’entiers de taille b-a+1 qui, comme tout tableau est situé dans le tas. Le nombre de cellules de ce tableau, b-a+1, permet de stocker tous les entiers compris entre a et b. Essayez avec un exemple, par exemple a = 3 et b = 7, pour vous en convaincre.

Le deuxième bloc est la boucle suivante.

        for(int i=0; i< t.length; i++)
        {
            t[i] = a + i;
        }

Elle a pour effet de remplir le tableau précédemment créé. Voici par exemple l’état de la mémoire à la fin de son exécution pour des arguments 3 et 7 passés en paramètres de la méthode.

image-20210807141509722

On y voit les 4 variables de méthodes, dont deux sont des paramètres, situées dans une mémoire appelée pile. Ces quatre variables n’existeront plus après l’exécution de la méthode (et seront recréées automatiquement à chaque nouvelle exécution). Par contre, le tableau, situé dans le tas, continuera d’exister tant que sa référence sera contenue dans au moins une variable.

Pour illustrer le cycle de vie du tableau, appelons la méthode main de la classe Decouverte suivante.

class Decouverte
{
    private static int[] tab = new int[10];
  
    public static void printTabInt(int[] t)
    {
        for(int i=0; i< t.length; i++)
        {
            System.out.print(t[i] + " ");
        }
        System.out.println();
    }
  
    public static int[] produitab(int a, int b)
    {
        int[] t = new int[b-a+1];
        for(int i=0; i< t.length; i++)
        {
            t[i] = a + i;
        }
        return t;
    }
  
    public static void main(String[] args)
    {
        tab = produitab(3,7);
        printTabInt(tab);
    }
}

Lors de l’exécution de main il se passe plusieurs choses très intéressantes et qu’il est important que vous preniez le temps de comprendre. L’initialisation des variables de classe se fait avant l’exécution de main. Donc dès le début de cette exécution tab désigne un tableau dont les 10 cellules contiennent 0.

L’appel produitab(3,7) crée un nouveau tableau dans le tas et en retourne la référence qui remplace celle précédemment contenue dans la variable de classe tab. Donc l’ancien tableau désigné par tab n’est plus pointé par aucune variable. Il sera automatiquement détruit par un dispositif de recyclage de la mémoire appelé ramasse miettes. L’appel à printTabInt affiche le contenu du nouveau tableau, qui sera lui même détruit à l’issue de l’exécution de main.

image-20210807152945790

Exercices de découverte

A-dec-40

Dans l’exemple précédent, la variable de classe tab désigne initialement un tableau remplis de 0 qui ne sert à rien puisqu’il est détruit automatiquement par le ramasse miette sans jamais être utilisé. Quelle modification faut-il faire pour éviter la création de ce tableau inutile ?

"Solution"

public static int[] tab;

A-dec-41

Dans la classe Decouverte présentée précédemment, la variable de classe tab permet de récupérer le résultat de l’appel de la méthode produitab qui sera ensuite transmis à la méthode printTabInt. Mais il n’est pas nécessaire d’utiliser une variable de classe pour faire cela. Modifiez le code de la classe Decouverte de manière à ce que la variable tabsoit une variable de méthode de main. Dans cette nouvelle version, il ne doit plus y avoir de variable de classe.

"Solution"

class Decouverte
{ 
    public static void printTabInt(int[] t)
    {
        // Aucun changement
    }
  
    public static int[] produitab(int a, int b)
    {
        // Aucun changement
    }
  
    public static void main(String[] args)
    {
        int [] tab = produitab(3,7);
        printTabInt(tab);
    }
}

A-dec-42

Réécrivez la méthode mainde la classe Decouverte de manière à ce que son exécution ait le même effet que la version précédente, mais sans utiliser aucune variable (ni de méthode, ni de classe).

"Solution"

public static void main(String[] args)
{
    printTabInt(produitab(3,7));
}

A5 : Types primitifs et chaînes de caractères

Présentation

Dans exemple précédent, nous avons utilisé des valeur et variable de type int, qui représente des entiers relatifs. int est un type primitif, qui représente une donnée simple. On parle parfois de type scalaire. Voici quelques types primitifs de java.

  • int : entiers signés entre $−2^{31}$ et $2^{31} − 1$.
  • long : entiers signés entre $−2^{63}$ et $2^{63} − 1$.
  • char : caractère imprimable
  • byte : entier compris entre -128 et 127
  • float : nombre en virgule flottante codé sur 4 octets
  • double : nombre en virgule flottante codé sur 8 octets
  • boolean : valeur Booléenne (true ou false)

Un autre type standard très utilisé est le type String qui permet la représentation des chaînes de caractères. Tout comme les tableaux, les chaînes de caractères sont des objets. Mais pour l’instant, nous les considérons comme de simples structures de données.

Voici quelques exemples de création de chaînes (on suppose que ces lignes de code sont situées dans une méthode) :

String s1 = "Tom";
String s2 = "Tim";
String s3 = s1;

Et voici l’état des lieux en mémoire après l’exécution de ces lignes.

image-20210808145810047

Les chaînes de caractères sont des objets situés dans le tas. Ce sont des instances de la classe String. Généralement, tout objet est créé à l’aide de l’opérateur new, mais dans cet exemple il est implicite. Il n’en reste pas moins que les variables s1, s2 et s3 contiennent des références qui désignent des objets de type String situés dans le tas. L’instruction s3 = s1 n’a pas dupliqué la chaîne désignée par s1 mais a juste recopié la référence de cette chaîne dans s3.

Ajoutons une quatrième ligne.

s1 = s1 + s2;

Le résultat en mémoire est le suivant.

image-20210808150031490

L’opérateur + n’a pas ajouté "Tim" à la fin de la chaîne désignée par s1, comme on pourrait le penser à l’issue d’une observation superficielle !

Cet opérateur a en fait créé une nouvelle chaîne résultant de la concaténation des chaînes désignées par s1 et s2, et la référence de cette nouvelle chaîne a été placée dans la variable s1.

La nuance est extrêmement importante. Si vous ne la saisissez pas, c’est qu’un pan entier de la programmation en Java vous échappe encore, et vous devez revenir sur les explications précédentes et / ou questionner un enseignant ou un expert pour bien saisir la subtilité. À défaut, vous pourrez produire, dans des applications que vous développerez, des bugs que vous ne comprendrez pas.

Exercices de découverte

A-dec-50

Après les lignes de code données en exemple précédemment, on écrit :

s2 = s3;

Dessinez la configuration en mémoire après l’exécution de cette ligne. Que devient le bloc mémoire contenant "Tim" ?

"Solution"

image-20210826223717598

A-dec-51

Dans une même classe (par exemple Decouverte), on place les méthodes suivantes.

    public static void modif(String s)
    {
        s = s + s;
    }

    public static void main(String[] args)
    {
        String t = "Tim"; 
        modif(t); 
        System.out.println(t);
    }

Voici la configuration en mémoire au tout début de l’exécution de la méthode modif, juste avant l’exécution de la ligne s = s + s.

image-20210814160139997

Dessinez la configuration en mémoire à la fin de l’exécution de la méthode modif, juste après l’exécution de la ligne s = s + s. Déduisez-en ce qui est affiché par l’exécution de main.

"Solution"

image-20210826224053037

Le programme affiche Tim. L’exécution de la méthode a été sans effet sur la chaîne désignée par la variable de méthode t de main. La chaîne "TimTim" créée par la méthode modif sera détruite par le ramasse-miettes de Java sans avoir jamais été utilisée.

A6 : Ligne de commande

Présentation

Pour exécuter un programme Java directement depuis une console, vous devez taper java nom_du_programme. Mais il est possible d’ajouter des arguments, séparés par des espaces, comme par exemple :

java calc 10 + 20

Le programme calc récupérera ces arguments sous forme de chaînes de caractères dans le tableau args de sa méthode main. Voici une premier programme expérimental

public class Calc
{
      public static void main(String[] args)
      {
          for (String s : args)
          {
              System.out.println(s);
          }
			} 
}

Après compilation on tape dans la console :

java calc 10 + 20

Et on obtient l’affichage :

10 
+ 
20

Pour comprendre ce qui c’est passé, observons la configuration en mémoire juste avant la fin de l’exécution.

image-20210815222046533

On constate que les arguments passés en ligne de commande ont été placé dans un tableau de chaînes de de caractères. Techniquement, les cellules d’un tel tableau contiennent des références d’objets de types String situés dans le tas.

Tentons une première version d’un programme qui se comporte comme une calculatrice rudimentaire limitée à l’addition.

public class Calc
{
    public static void main(String[] args)
    {
        if((args.length != 3) || !args[1].equals("+"))
        {
            System.out.println("Arguments incorrects"); 
        }
        else
        {
            System.out.println(args[0] + args[2]);
        } 
    }
}

On lance l’exécution du programme depuis la console avec les arguments suivants…

java calc 10 + 20

… et on obtient l’affichage 1020 et non 30 comme attendu de la part d’une calculatrice. La raison est très simple à comprendre. L’opérateur + a été utilisé avec deux chaînes de caractères, la chaîne "10" et la chaine "20". Dans ce contexte, l’opération réalisée est une concaténation et non une addition. Pour faire une addition, il faut traduire nos chaînes en nombres.

public static void main(String[] args)
{
    if((args.length != 3) || !args[1].equals("+"))
    {
        System.out.println("Arguments incorrects"); 
    }
    else
    {
        int a = Integer.parseInt(args[0]); 
        int b = Integer.parseInt(args[2]);
        System.out.println(a+b); 
    }
}

Cette fois-ci, le programme se comporte comme attendu. L’exécution de la ligne de commande décrite précédemment a bien pour effet l’affichage de 30.

Évidemment, le programme ne fonctionne que si le premier et le troisième arguments ont un format permettant de les interpréter comme des entiers. Si on tape…

calc 10 + vingt

… on obtient un message d’erreur qui nous parle d’une exception. Nous verrons plus tard comment gérer cela.

Exercices de découverte

A-dec-60

Améliorez le programme précédent de manière à ce qu’il puisse réaliser une addition ou une multiplication.

"Solution"

public static void main(String[] args)
{
    if( (args.length != 3) || !(args[1].equals("+") || args[1].equals("*")) )
    {
        System.out.println("Arguments incorrects"); 
    }
    else
    {
        int a = Integer.parseInt(args[0]); int b = Integer.parseInt(args[2]);
        if(args[1].equals("+"))
        {
            System.out.println(a + b);
        }
        else
        {
            System.out.println(a * b);
        }
    }
}

Exercices d’assimilation

A-ass-00

On peut classer les entiers naturels (0, 1, 2, 3, …) en deux catégories : les nombres premiers et les nombres composés. Un entier $x$ est premier si et seulement s’il n’a pas d’autre diviseur que lui même et 1. Dans le cas contraire, il est composé, ce qui implique qu’il existe au moins deux entiers $a>$1 et $b>$ 1 tels que $x=ab$.

Pour savoir si un entier naturel $x$ est premier ou composé, on peut rechercher s’il a un diviseur $a$ compris entre 2 et $\lfloor \sqrt x \rfloor$. En effet, il est impossible qu’un entier $x$ soit le produit de deux entiers supérieurs à $\sqrt x$. La démonstration est laissée à la discrétion du lecteur, si affinité. Par exemple, si je veux savoir si 103 est premier, il me suffit d’essayer de la diviser par les entiers 2 à 10. Comme chacune des divisions a un reste non nul, 103 est premier.

Pour savoir si la division d’un entier $x$ par un entier $a$ a un reste, on peut utiliser l’opérateur modulo, noté %, qui donne le reste de la division $a/b$. Par exemple, 103 % 5 a pour résultat 3. Autre information utile : pour placer dans une variable borne de type int la valeur $\lfloor \sqrt x \rfloor$, on peut écrire borne = (int)Math.sqrt(x).

Sachant tout cela, réalisez une méthode de classe qui accepte en paramètre un entier supposé positif ou nul et qui retourne un Booléen valant true si et seulement si x est premier.

    public static boolean premier(int x)
    {
        // À compléter
    }
"Indice"

    public static boolean premier(int x)
    {
        if(x<2) return false;

        int borne = (int)Math.sqrt(x);

        for(int d=2; d <= borne; d++) 
        {
            // À compléter
        }

        return true;
    }

"Solution"

    public static boolean premier(int x)
    {
        if(x<2) return false;

        int borne = (int)Math.sqrt(x);

        for(int d=2; d <= borne; d++) // au lieu de <x
        {
            if(x % d == 0) return false;
        }

        return true;
    }

A-ass-01

Réalisez une méthode de classe premiers qui accepte en paramètre un entier n et retourne une tableau contenant les nombres premiers inférieurs à n. Par exemple si n vaut 10, le tableau retourné doit contenir les entiers 2, 3, 5, 7. Cette méthode doit appeler la méthode premier de l’exercice précédent.

Comme le nombre de nombre premiers n’est pas connu à l’avance, la méthode commence par les stocker dans un tableau de taille suffisante (à déterminer), qui sera ensuite recopié dans un tableau ayant exactement la taille nécessaire.

"Indice

public static int[] premiers(int n)
{
    int[] t = new int[n/2 + 1];
    int nbPremEnr = 0; // Nombre de nombres premiers enregistrés

    for(int candidat=2; candidat<n; candidat++)
    {
        // À compléter
    }

    int[] r = new int[nbPremEnr];
    // À compléter

    return r;
}

"Indice

public static int[] premiers(int n)
{
    int[] t = new int[n/2 + 1];
    int nbPremEnr = 0;

    for(int candidat=2; candidat<n; candidat++)
    {
        if(premier(candidat))
        {
            // À compléter
        }
    }

    int[] r = new int[nbPremEnr];
    for(int i=0; i<nbPremEnr; i++)
    {
        // À compléter
    }

    return r;
}

"Solution"

public static int[] premiers(int n)
{
    int[] t = new int[n/2 + 1];
    int nbPremEnr = 0;

    for(int candidat=2; candidat<n; candidat++)
    {
        if(premier(candidat))
        {
            t[nbPremEnr] = candidat; nbPremEnr++;
        }
    }

    int[] r = new int[nbPremEnr];
    for(int i=0; i<nbPremEnr; i++)
    {
        r[i] = t[i];
    }

    return r;
}

A-ass-10

Vous devez réaliser une méthode acceptant en paramètre un entier n et retournant une chaîne de caractères constituée de n lettres majuscules tirées aléatoirement. Les informations suivantes vous seront utiles :

  • La méthode de classe prédéfinie Math.ramdom() retourne une nombre aléatoire de type double compris entre 0.0 (inclus) et 1.0 (exclus).
  • Si s est une chaîne de caractères, c’est à dire un objet de type String, et si i est un entier, alors s.charAt(i) retourne le caractère situé en position i dans s.
  • Si c est un caractère et s est une chaîne, alors s+c est la chaîne obtenue en ajoutant de caractère c à la fin de s.
"Indice

public static String ramdomStr(int n)
{
    String lettres = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    String r = "";

    for(int i=0; i<n; i++)
    {
        // À compléter.
    }

    return r;
}

"Indice

public static String ramdomStr(int n)
{
    String lettres = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    String r = "";

    for(int i=0; i<n; i++)
    {
        int pos = (int)(lettres.length() * Math.random());
        // À compléter
    }

    return r;
}

"Solution"

public static String ramdomStr(int n)
{
    String lettres = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    String r = "";

    for(int i=0; i<n; i++)
    {
        int pos = (int)(lettres.length() * Math.random());
        r = r + lettres.charAt(pos);
    }

    return r;
}

A-ass-11

Vous devez réaliser une méthode acceptant en paramètre une chaîne s et retournant la chaîne de caractères obtenue en remplaçant chaque lettre minuscule par une lettre majuscule avec une probabilité 1/2.

Par exemple, l’appel…

System.out.println(effect("Bonjour, Comment allez-vous ?"));

…peut afficher des chaines telles que :

BoNJOUR, CoMMeNT ALLez-vOus ?
BOnJour, COmMeNt aLLEz-VouS ?
BonJoUR, CoMmeNt alleZ-VoUs ?

L’information suivante vous sera utile : si c est un caractère représentant une lettre minuscule, alors Character.toUpperCase(c) retourne la lettre majuscule correspondante. Dans le cas où c ne représente pas une lettre majuscule, l’appel retourne c.

"indice"

    public static String effect(String s)
    {
        String r = "";

        for(int i=0; i<s.length(); i++)
        {
            if(Math.random() > 0.5)
            {
                // À compléter
            }
            else
            {
                // À compléter
            }
        }
        return r;
    }
}

"Solution"

    public static String effect(String s)
    {
        String r = "";
        for(int i=0; i<s.length(); i++)
        {
            if(Math.random() > 0.5)
            {
                r = r + s.charAt(i);
            }
            else
            {
                r = r + Character.toUpperCase(s.charAt(i));
            }
        }
        return r;
    }
}

Exercices d’approfondissement

Ne traitez ces exercices que si vous avez traité et maitrisé les exercices de découverte et d’assimilation de toutes les parties déjà en ligne.

A-top-00

Vous devez réaliser une méthode de classe premiers qui accepte en paramètre un entier n et retourne une tableau contenant les nombres premiers inférieurs à n. Par exemple si n vaut 10, le tableau retourné doit contenir les entiers 2, 3, 5, 7. Mais cette méthode doit être plus efficace que celle demandée dans un des exercices d’assimilation de cette partie. Pour déterminer si un nombre $x$ est composé ou premier, elle doit tester sa divisibilité par les nombres premiers compris entre 2 et $\lfloor \sqrt x \rfloor$, qui ont déjà été préalablement stockés dans le tableau, et uniquement par ces entiers. En effet, tout nombre composé est divisible par au moins un nombre premier compris entre 2 et $\lfloor \sqrt x \rfloor$. La preuve est laissée à la discrétion du lecteur, si affinité.

Comme le nombre de nombre premiers n’est pas connu à l’avance, la méthode commence par les stocker dans un tableau de taille suffisante (à déterminer), qui sera ensuite recopié dans un tableau ayant exactement la taille nécessaire.

A-top-01

Vous devez réaliser une méthode de classe premiers qui accepte en paramètre un entier n et retourne une tableau contenant les nombres premiers inférieurs à n. Par exemple si n vaut 10, le tableau retourné doit contenir les entiers 2, 3, 5, 7. Mais cette méthode doit être encore plus efficace que celle de l’exercice précédent. Elle doit utiliser le principe connu sous le nom de crible d’Ératosthène.

Le principe est simple, mais le codage nécessite d’être très attentifs et rigoureux. Imaginons qu’on cherche les nombres premiers inférieur à 100. On crée un tableau de 100 Booléens, dont les indices des cellules vont de 0 à 99. Les cellules sont initialisées à false. On place truedans les cellules d’indices 0 et 1, puis dans toutes les cellules d’indices pairs sauf 2, dans toutes les cellules d’indices multiples de 3 sauf 3 , multiples de 5 sauf 5, multiples de 7 sauf 7, etc. Pour faire cela, on utilise uniquement des boucles et des additions, donc ni division, ni modulo, ni multiplications. Par exemple pour les multiples de 3, on place true dans les cellules d’indices 3+3, 3+3+3, 3+3+3+3, etc. À l’issue de ce remplissage, les indices des cellules contenant falsesont les nombres premiers recherchés.

La figure suivante illustre les deux premières étapes du remplissage.

image-20210825112831788

Cette méthode est très connue, donc vous pourrez facilement trouver des explications supplémentaire sur le WEB. Mais attention, vous pourrez aussi trouver des algorithmes détaillés et même des programmes déjà réalisés. Recopier ces codes ferait perdre tout intérêt à cet exercice dont le but est de développer votre pensée algorithmique.

Le tableau retourné devra contenir la liste des nombres premiers recherchés, exactement comme pour l’exercice précédents. Le tableau ayant permis de réaliser le crible est juste un tableau intermédiaire utilisé par la méthode et n’est pas le tableau à retourner.