Examen intermédiaire
corrigé
Dates empaquetées
Partie 1
La méthode isLeapYear
est une simple traduction en Java de l'énoncé, dans laquelle l'opérateur de calcul du reste de la division (%
) est utilisé pour déterminer si l'année est ou n'est pas un multiple de 4, 100 ou 400.
public static boolean isLeapYear(int y) { if (! (0 < y && y <= 3000)) throw new IllegalArgumentException(); return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0); }
Partie 2
La manière la plus simple d'écrire daysInMonth
est probablement d'utiliser un énoncé switch
pour distinguer les trois cas : mois à 31 jours, mois à 30 jours, et février.
public static int daysInMonth(int y, int m) { if (! (0 < y && y <= 3000)) throw new IllegalArgumentException(); switch (m) { case 1: case 3: case 5: case 7: case 8: case 10: case 12: return 31; case 4: case 6: case 9: case 11: return 30; case 2: return isLeapYear(y) ? 29 : 28; default: throw new IllegalArgumentException(); } }
Partie 3
Étant donné l'existence des attributs YEAR_START
, MONTH_START
et DAY_START
, la méthode pack
est simple à écrire : il suffit de décaler chaque composante afin que son bit de poids faible se trouve à la bonne position, puis d'utiliser un « ou » bit à bit pour combiner les composantes ainsi décalées.
public static int pack(int y, int m, int d) { if (! (0 < y && y <= 3000)) throw new IllegalArgumentException(); if (! (0 < m && m <= 12)) throw new IllegalArgumentException(); if (! (0 < d && d <= daysInMonth(y, m))) throw new IllegalArgumentException(); return ((y << YEAR_START) | (m << MONTH_START) | (d << DAY_START)); }
A noter que comme DAY_START
vaut 0, il n'est pas strictement nécessaire d'effectuer le dernier décalage, mais le code est plus clair et plus robuste ainsi.
Partie 4
L'extraction du mois implique de faire deux choses :
- décaler la date empaquetée pour que le bit de poids faible du mois se trouve à l'index 0,
- masquer la date ainsi décalée afin de ne garder que les bits correspondant au mois, en mettant à 0 ceux de l'année.
L'ordre de ces deux opérations peut bien entendu être échangé, pour peu que le masque soit ajusté en fonction.
Le calcul du masque se fait en combinant un décalage est une soustraction, comme expliqué dans le cours.
public static int month(int pkDate) { int mMask = (1 << MONTH_SIZE) - 1; return (pkDate >> MONTH_START) & mMask; }
Partie 5
La seule petite difficulté que présente toString
est le fait que le mois et le jour doivent toujours être écrits avec deux chiffres, en plaçant un 0 en tête au besoin. Cela n'est nécessaire que si la valeur est inférieure à 10, et le test s'effectue très naturellement au moyen de l'opérateur ternaire (?:
):
public static String toString(int pkDate) { int m = month(pkDate); int d = day(pkDate); return (String.valueOf(year(pkDate)) + (m < 10 ? "-0" : "-") + m + (d < 10 ? "-0" : "-") + d); }
Il est également possible d'utiliser la méthode format
de String
pour obtenir le même résultat de manière encore plus simple :
public static String toString(int pkDate) { return String.format("%d-%02d-%02d", year(pkDate), month(pkDate), day(pkDate)); }
Partie 6
Non, car la méthode toString
de PackedDate
n'est pas une redéfinition de la méthode toString
de Object
, pour deux raisons : d'une part elle est statique, et d'autre part elle prend un argument alors que la méthode de Object
n'en prend aucun.
Dates
Partie 1
Pour mettre en œuvre la classe Date
au moyen de PackedDate
, l'idée est bien entendu de n'y stocker qu'un seul attribut, la date empaquetée. Cet attribut est final car la classe est immuable.
Le constructeur privé ne fait rien d'autre qu'initialiser cet attribut avec la valeur qu'on lui passe en argument.
La méthode of
appelle ce constructeur avec la version empaquetée de la date dont les composantes lui sont passées en argument, qu'elle obtient au moyen de la méthode pack
de PackedDate
.
public final class Date { private final int pkDate; public static Date of(int year, int month, int day) { return new Date(PackedDate.pack(year, month, day)); } private Date(int pkDate) { this.pkDate = pkDate; } // … à compléter }
Partie 2
Oui, car il s'agit d'une classe immuable, et il faut donc que ses instances soient comparées de manière structurelle : deux dates qui représentent le même jour doivent être considérées comme égales même s'il s'agit d'objets Java différents.
La méthode equals
doit donc vérifier si la date empaquetée de this
est identique à celle de son argument, après avoir vérifié que ce dernier est lui aussi une instance de Date
.
La méthode hashCode
se contente de retourner la date empaquetée, qui constitue une excellente valeur de hachage car elle garantit que deux dates différentes ont une valeur de hachage différente.
@Override public boolean equals(Object thatO) { return (thatO instanceof Date) && (pkDate == ((Date)thatO).pkDate); } @Override public int hashCode() { return pkDate; }
Partie 3
La classe Date
doit implémenter l'interface Comparable
instanciée comme d'habitude avec le type de la classe elle-même, ici Date
. Pour mémoire, cette utilisation surprenante de la généricité permet de garantir que le type de l'argument de compareTo
est le même que celui du récepteur (this
).
La méthode compareTo
peut être mise en œuvre très facilement si on se rend compte que la technique d'empaquetage utilisée permet de comparer les versions empaquetées des dates comme s'il s'agissait d'entiers. On peut dès lors simplement appliquer la méthode compare
de Integer
aux deux dates empaquetées.
public final class Date implements Comparable<Date> { // … comme avant @Override public int compareTo(Date that) { return Integer.compare(this.pkDate, that.pkDate); } }
Ensemble d'énumération
Partie 1
La méthode noneOf
est triviale à définir, car un ensemble vide est représenté par l'entier 0, qu'il suffit de passer au constructeur en même temps que le tableau values
reçu en argument.
static <E extends Enum<E>> EnumSet<E> noneOf(E[] values) { return new EnumSet<E>(values, 0); }
Partie 2
Pour mettre en œuvre range
, il faut calculer une valeur de type long
dont les seuls bits à 1 sont ceux correspondant aux éléments compris entre from
et to
, inclus.
Si l'on nomme fromI
l'index du bit correspondant à la valeur from
et toI
l'index du bit correspondant à la valeur to
, on peut observer que :
- le nombre de bits à 1 est :
toI - fromI + 1
, - le premier d'entre eux est à l'index
fromI
, - ces bits sont contigus.
En combinant ces observations, on conclut que la valeur représentant l'ensemble se calcule en :
- générant une première valeur (
mask
ci-dessous) dont le bon nombre de bits de poids faible sont à 1, - décalant cette valeur à gauche de
fromI
positions.
En traduisant cela en Java, on obtient la méthode range
ci-dessous. Attention à ne pas oublier d'utiliser la constante 1L
(de type long
) plutôt que 1
(de type int
) dans le calcul du masque, faute de quoi le résultat obtenu est incorrect !
static <E extends Enum<E>> EnumSet<E> range(E[] values, E from, E to) { int fromI = from.ordinal(), toI = to.ordinal(); if (! (fromI <= toI)) throw new IllegalArgumentException(); long mask = (1L << (toI - fromI + 1)) - 1; return new EnumSet<>(values, mask << fromI); }
Partie 3
La méthode isEmpty
est triviale puisque, comme nous l'avons dit, l'ensemble vide est représenté par l'entier 0.
La méthode size
doit simplement retourner le nombre de bits valant 1 dans l'entier représentant l'ensemble, ce qui se fait facilement au moyen de la méthode bitCount
de Long
.
public boolean isEmpty() { return set == 0; } public int size() { return Long.bitCount(set); }
Partie 4
La méthode contains
doit vérifier si le bit correspondant à l'élément vaut 1, ce qui se fait aisément au moyen d'un masque et d'un « et » bit à bit.
La méthode containsAll
est presque encore plus simple puisqu'il n'y a pas besoin de construire un masque, celui-ci n'est autre que l'entier représentant le second ensemble !
public boolean contains(E e) { return (set & 1L << e.ordinal()) != 0; } public boolean containsAll(EnumSet<E> that) { return (this.set & that.set) == that.set; }
Partie 5
La méthode add
met à 1 le bit correspondant à l'élément, ce qui se fait encore une fois au moyen d'un masque, puis d'un « ou » bit à bit. Pour déterminer si l'ensemble a changé en raison de l'ajout, il suffit de comparer l'ancien entier représentant l'ensemble avec le nouveau.
La méthode addAll
met à 1 tous les bits correspondants aux éléments du second ensemble, ce qui se fait au moyen d'un simple « ou » bit à bit.
public boolean add(E e) { long oldSet = set; set |= 1L << e.ordinal(); return set != oldSet; } public boolean addAll(EnumSet<E> that) { long oldSet = set; set |= that.set; return set != oldSet; }
Partie 6
La méthode toString
se réalise très facilement au moyen d'un « joigneur » de chaînes de type StringJoiner
dont le contenu est calculé en :
- itérant sur toutes les valeurs du type énuméré, qui sont disponibles dans l'attribut
values
, - regardant, pour chacune d'elle, si elle appartient à l'ensemble,
- si oui, ajoutant sa représentation textuelle au joigneur de chaînes.
@Override public String toString() { StringJoiner j = new StringJoiner(",", "{", "}"); for (E e: values) { if (contains(e)) j.add(e.toString()); } return j.toString(); }
Dictionnaire pour mots croisés
La manière la plus simple de représenter le dictionnaire pour mots croisés est d'utiliser une table associative dont les clefs sont les longueurs de mots, et les valeurs sont les ensembles de mots de cette longueur. En d'autres termes, on représente le dictionnaire au moyen d'une valeur de type :
Map<Integer, Set<String>>
Cette décision prise, il reste à choisir la mise en œuvre des tables associatives et ensembles à utiliser, et ici un choix s'impose rapidement : il faut utiliser TreeMap
et TreeSet
, car elles trient leurs clefs/éléments, ce qui est exactement ce que l'on désire.
Une fois ce choix effectué, le code s'écrit facilement.
public final class CrosswordDict { private final Map<Integer, Set<String>> dict = new TreeMap<>(); public void add(String word) { if (word.isEmpty()) throw new IllegalArgumentException(); Set<String> s = dict.get(word.length()); if (s == null) { s = new TreeSet<>(); dict.put(word.length(), s); } s.add(word); } public void addAll(Collection<String> words) { for (String word: words) add(word); } public void print() { for (Map.Entry<Integer, Set<String>> e: dict.entrySet()) { System.out.println("Mots de "+ e.getKey() +" lettres :"); for (String s: e.getValue()) System.out.println(" " + s); } } }
On notera qu'au moyen des méthodes des collections acceptant des lambdas, la classe CrosswordDict
peut être écrite de manière plus claire et plus concise :
public final class CrosswordDictL { private final Map<Integer, Set<String>> dict = new TreeMap<>(); public void add(String word) { if (word.isEmpty()) throw new IllegalArgumentException(); dict .computeIfAbsent(word.length(), k -> new TreeSet<>()) .add(word); } public void addAll(Collection<String> words) { words.forEach(w -> add(w)); } public void print() { dict.forEach((l, s) -> { System.out.println("Mots de "+ l +" lettres :"); s.forEach(w -> System.out.println(" " + w)); }); } }