Examen intermédiaire
corrigé

Tables bidimensionnelles

Partie 1

La transformation de la classe StringTable en une version générique ne pose aucun problème, il suffit d'ajouter un paramètre de type à la classe, nommé E ci-dessous, puis de remplacer toutes les occurrences du type String par ce paramètre de type. On obtient :

public final class Table<E> {
  public Table(List<E> elements,
               int minX, int width,
               int minY, int height) { /* … */ }

  public E get(int x, int y) { /* … */ }
}

Partie 2

La totalité des arguments passés au constructeur étant nécessaires pour la mise en œuvre de la méthode get, les attributs de la classe Table sont simplement les mêmes que les arguments du constructeur. La classe étant immuable, il faut penser à leur attacher l'attribut final.

public final class Table<E> {
  private final int minX, width;
  private final int minY, height;
  private final List<E> elements;
  // …
}

L'écriture du constructeur ne contient que deux petites difficultés :

  1. la validité des arguments doit être vérifiée conformément à l'énoncé,
  2. la liste des éléments doit être copiée défensivement — et, idéalement, rendue non modifiable — pour garantir l'immuabilité de la classe.
public final class Table<E> {
    public Table(List<E> elements,
                 int minX, int width,
                 int minY, int height) {
        if (! (0 <= width && 0 <= height))
            throw new IllegalArgumentException();
        if (! (elements.size() == width * height))
            throw new IllegalArgumentException();

        this.minX = minX;
        this.width = width;
        this.minY = minY;
        this.height = height;
        this.elements =
          unmodifiableList(new ArrayList<>(elements));
    }
}

Partie 3

L'écriture de la méthode get ne pose pas de gros problème, il faut juste penser à tenir compte des indices de départ (minX et minY) pour calculer la position de l'élément à retourner dans la liste des éléments.

public E get(int x, int y) {
  if (! (minX <= x && x < minX + width))
    throw new IndexOutOfBoundsException();
  if (! (minY <= y && y < minY + height))
    throw new IndexOutOfBoundsException();

  return elements.get((y - minY) * width + x - minX);
}

Partie 4

La méthode toString peut s'écrire de différentes manières, celle choisie ci-dessous consistant à simuler un parcours bidimensionnel des éléments au moyen de deux boucles imbriquées, ce qui facilite la construction d'une chaîne respectant les spécifications données.

Étant donné que le fait que les index ne commencent pas à 0 ne joue aucun rôle ici, on peut ignorer cette caractéristique et faire varier l'index y entre 0 et la hauteur moins un, et l'index x entre 0 et la largeur moins 1. Il faut alors prendre garde à adapter les index avant d'appeler la méthode get définie précédemment, ou alors à appeler directement la méthode get de la liste d'éléments, solution retenue ici.

@Override
public String toString() {
  StringBuilder b = new StringBuilder("[");
  for (int y = 0; y < height; ++y) {
    b.append("[");
    for (int x = 0; x < width; ++x) {
      b.append(elements.get(y * width + x));
      if (x < width - 1)
        b.append(", ");
    }
    b.append("]");
    if (y < height - 1)
      b.append(", ");
  }
  return b.append("]").toString();
}

Index de livre

Partie 1

Le constructeur doit faire deux opérations successives avec les tables reçues en argument :

  1. les valider,
  2. les copier défensivement pour garantir l'immuabilité de la classe.

La validation consiste en deux étapes :

  1. vérifier que tous les numéros de page contenus dans pageRefs sont strictement positifs,
  2. vérifier que toutes les références croisées — les valeurs de la table crossRefs — désignent des termes apparaissant dans l'index — c-à-d dans les clefs de pageRefs ou de crossRefs.

L'énoncé spécifie de plus que NullPointerException doit être levée si l'un des arguments est nul, mais cela ne requiert aucun code explicite, étant donné que les deux sont utilisés par le constructeur.

La première des deux étapes de validation peut se faire par exemple en parcourant tous les éléments de tous les ensembles de pages, au moyen de deux boucles imbriquées, et en vérifiant que tous sont positifs.

La seconde de ces deux étapes peut se faire en calculant la différence entre l'ensemble des mots désignés par les références croisées et la totalité des mots de l'index, et en vérifiant que cet ensemble est vide.

Le code de l'étape de validation ressemble donc à ceci :

public Index(Map<String, Set<Integer>> pageRefs,
             Map<String, String> crossRefs) {
  // Validation des arguments
  for (Set<Integer> ps: pageRefs.values()) {
    for (int p: ps) {
      if (p <= 0)
        throw new IllegalArgumentException();
    }
  }

  Set<String> undefinedCrossRefs =
    new HashSet<>(crossRefs.values());
  undefinedCrossRefs.removeAll(pageRefs.keySet());
  undefinedCrossRefs.removeAll(crossRefs.keySet());
  if (! undefinedCrossRefs.isEmpty())
    throw new IllegalArgumentException();

  // Copie défensive des arguments
  // … (voir ci-dessous)
}

La copie défensive est quant à elle triviale pour les références croisées, mais un peu moins pour les références de page. En effet, il faut bien penser à faire une copie profonde de ces dernières, en copiant non seulement la table mais aussi chacun des ensembles de pages.

public Index(Map<String, Set<Integer>> pageRefs,
             Map<String, String> crossRefs) {
  // Validation des arguments
  // … (voir ci-dessus)

  // Copie défensive des arguments
  Map<String, Set<Integer>> pageRefs1 =
    new HashMap<>();
  pageRefs.forEach((w, ps) ->
    pageRefs1.put(w, unmodifiableSet(new TreeSet<>(ps))));
  Map<String, String> crossRefs1 =
    new HashMap<>(crossRefs);

  this.pageRefs = unmodifiableMap(pageRefs1);
  this.crossRefs = unmodifiableMap(crossRefs1);
}

Partie 2

La principale difficulté de la méthode toLines est de correctement combiner les entrées provenant des références de page avec celles provenant des références croisées.

Le plus simple pour ce faire consiste à calculer l'ensemble de la totalité des mots apparaissant dans l'index, qui est simplement l'union de l'ensemble des clefs de chacune des deux tables. En stockant cette union dans un ensemble de type TreeSet, on s'assure qu'on pourra ensuite les parcourir par ordre alphabétique.

Une fois la totalité des mots de l'index calculées, il suffit de les parcourir et de transformer la référence de page ou la référence croisée correspondant à chacun d'eux en une chaîne, et à l'ajouter aux lignes.

public List<String> toLines() {
  List<String> lines = new ArrayList<>();

  Set<String> allWords = new TreeSet<>(pageRefs.keySet());
  allWords.addAll(crossRefs.keySet());

  for (String w: allWords) {
    StringBuilder b = new StringBuilder(w);
    if (pageRefs.containsKey(w))
      pageRefs.get(w).forEach(p ->
        b.append(", ").append(p));
    else
      b.append(", voir ").append(crossRefs.get(w));
    lines.add(b.toString());
  }

  return lines;
}

Partie 3

Comme d'habitude, le bâtisseur doit avoir les mêmes attributs que la classe dont il construit des instances. Dans ce cas, cela signifie qu'il contient une table des références de pages, et une des références croisées.

Les méthodes add ne font rien d'autre que valider leurs arguments selon les règles données dans l'énoncé, puis l'ajoutent à la bonne table. Une petite difficulté se présente pour les références de pages, puisqu'il faut créer l'ensemble correspondant à un mot donné lors du premier ajout. Il y a plusieurs manières de faire cela, celle retenue ci-dessous consistant à utiliser la méthode computeIfAbsent.

public final static class Builder {
  private final Map<String, Set<Integer>> pageRefs =
    new TreeMap<>();
  private final Map<String, String> crossRefs =
    new TreeMap<>();

  public Builder add(String word, int page) {
    if (! (page > 0))
      throw new IllegalArgumentException();
    if (crossRefs.containsKey(word))
      throw new IllegalArgumentException();

    pageRefs.computeIfAbsent(word, k -> new TreeSet<>())
      .add(page);
    return this;
  }

  public Builder add(String word, String reference) {
    if (pageRefs.containsKey(word))
      throw new IllegalArgumentException();

    crossRefs.put(word, reference);
    return this;
  }

  public Index build() {
    return new Index(pageRefs, crossRefs);
  }
}

Tableaux

Partie 1

La méthode forEach consiste simplement à appeler la méthode accept du consommateur avec chacun des éléments du tableau, dans l'ordre. Le parcours de ce tableau peut se faire de différentes manières, la plus simple étant certainement d'utiliser une boucle for each.

public static void forEach(double[] a,
                           DoubleConsumer c) {
  for (double v: a)
    c.accept(v);
}

Partie 2

La méthode replaceAll fonctionne de manière assez similaire à la méthode forEach, la différence principale étant que le résultat de l'opérateur doit être stocké dans le tableau. Pour cette raison, il n'est cette fois pas possible d'utiliser une boucle for each pour le parcourir, il faut utiliser une boucle for parcourant les index.

public static void replaceAll(double[] a,
                              DoubleUnaryOperator op) {
  for (int i = 0; i < a.length; ++i)
    a[i] = op.applyAsDouble(a[i]);
}

Partie 3

La méthode reduceLeft fonctionne elle aussi par parcours des éléments du tableau, durant lequel la valeur initiale z est remplacée par le résultat de l'application de l'opérateur op à sa valeur actuelle et à l'élément courant du tableau.

public static double reduceLeft(double z,
                                double[] a,
                                DoubleBinaryOperator op) {
  for (double v: a)
    z = op.applyAsDouble(z, v);
  return z;
}