Examen final

CS-108 – corrigé

1. Liste de points

Partie 1

La méthode of doit convertir la liste de point reçue en un tableau de type float[] à passer au constructeur, ce qui se fait en la parcourant et en soustrayant à chacune des coordonnées l'origine correspondante.

public static PointChList of(List<PointCh> points) {
  float[] coordinates = new float[points.size() * 2];
  int i = 0;
  for (PointCh p : points) {
    coordinates[i++] = (float) (p.e() - ORIGIN_E);
    coordinates[i++] = (float) (p.n() - ORIGIN_N);
  }
  return new PointChList(coordinates);
}

Partie 2

Les méthodes size, e, n et get sont extrêmement simples et ne méritent pas vraiment de commentaire particulier. Il faut juste noter que les trois dernières, qui doivent lever une IndexOutOfBoundsException lorsque l'index reçu est invalide, peuvent soit faire une vérification explicite, soit tirer parti du fait que cette exception est automatiquement levée en Java lorsqu'on tente d'accéder à un élément inexistant d'un tableau. Dans un soucis de simplicité, c'est cette dernière solution qui a été retenue ci-dessous, même s'il serait plus propre de faire une vérification explicite.

public int size() {
  return cs.length / 2;
}

public double e(int i) {
  return ORIGIN_E + cs[2 * i];
}

public double n(int i) {
  return ORIGIN_N + cs[2 * i + 1];
}

public PointCh get(int i) {
  return new PointCh(e(i), n(i));
}

Partie 3

La classe PointChList étant immuable, ses instances devraient être comparées de manière structurelle, ce qui implique de redéfinir equals et donc hashCode.

Pour redéfinir correctement ces méthodes, il faut se souvenir qu'en Java, les tableaux sont comparés (et donc hachés) par référence. Il n'est donc pas correct d'utiliser directement les méthodes d'instance equals et hashCode sur le tableau cs. Au lieu de cela, il faut utiliser les méthode statiques fournies par la classe Arrays.

public int hashCode() {
  return Arrays.hashCode(cs);
}

public boolean equals(Object thatO) {
  return (thatO instanceof PointChList)
    && Arrays.equals(this.cs, ((PointChList) thatO).cs);
}

La méthode equals peut être simplifiée en tirant parti de la possibilité, ajoutée en Java 16, de déclarer une variable dans le contexte d'un instanceof. On obtient alors :

public boolean equals(Object thatO) {
  return (thatO instanceof PointChList that)
    && Arrays.equals(this.cs, that.cs);
}

Les méthodes hashCode et equals ci-dessus étant des redéfinitions, il faudrait leur attacher l'annotation @Override. Dans le contexte d'un examen, il est toutefois acceptable de ne pas le faire.

Partie 4

La méthode toString s'écrit facilement au moyen d'une instance de StringJoiner.

public String toString() {
  StringJoiner j = new StringJoiner(", ", "[", "]");
  for (int i = 0; i < size(); i += 1)
    j.add("(" + e(i) + "," + n(i) + ")");
  return j.toString();
}

Partie 5

La manière la plus simple de définir la méthode iterator consiste à utiliser une classe imbriquée anonyme. Une fois ce choix fait, il faut constater qu'au moyen d'un simple index et des méthodes size et get de PointChList, l'itérateur s'écrit très simplement, comme illustré ci-dessous.

(Cet itérateur est très similaire à celui écrit dans le cours sur la mise en œuvre des collections pour la classe SArrayList.)

public Iterator<PointCh> iterator() {
  return new Iterator<>() {
    int nextI = 0;

    public boolean hasNext() {
      return nextI < size();
    }

    public PointCh next() {
      if (!hasNext()) throw new NoSuchElementException();
      return get(nextI++);
    }
  };
}

Il n'est pas nécessaire de redéfinir explicitement remove, car l'interface Iterator définit une version par défaut de cette méthode, qui lève une exception de type UnsupportedOperationException, comme demandé.

Partie 6

Le bâtisseur est très similaire à la classe SArrayList écrite dans le cours sur la mise en œuvre des collections, en ce qu'il utilise aussi un tableau d'une certaine capacité pour stocker les coordonnées des points de la liste en cours de construction. Tout comme celui de SArrayList, ce tableau est « redimensionné » par copie lorsqu'il est plein et que de nouvelles coordonnées doivent être ajoutées.

La méthode build doit prendre garde à copier le tableau contenant les coordonnées avant de le passer au constructeur de PointChList, pour deux raisons : premièrement, ce tableau peut encore avoir de la capacité inutilisée ; et deuxièmement, le constructeur de PointChList ne copie pas lui-même le tableau qu'on lui passe, et il est donc impératif de faire cette copie dans la méthode build pour garantir l'immuabilité de la classe PointChList.

public static final class Builder {
  private float[] coordinates = new float[2];
  private int coordinatesCount = 0;

  public Builder add(double e, double n) {
    if (coordinatesCount == coordinates.length)
      coordinates = Arrays.copyOf(coordinates,
				  coordinates.length * 2);
    coordinates[coordinatesCount++] = (float) (e - ORIGIN_E);
    coordinates[coordinatesCount++] = (float) (n - ORIGIN_N);
    return this;
  }

  public PointChList build() {
    return new PointChList(Arrays.copyOf(coordinates,
					 coordinatesCount));
  }
}

2. Fournisseur de tuiles

Partie 1

Pour obtenir un fournisseur ayant les caractéristiques demandées, il suffit d'« emballer » celui obtenant les données du serveur OSM dans celui plaçant les tuiles dans le cache disque, puis d'emballer le tout dans celui plaçant les tuiles dans le cache mémoire.

TileProvider tileProvider =
  new MemoryCachingTP(new DiskCachingTP(new NetworkTP()));

Partie 2

ErrorSilencingTP est un décorateur relativement simple dont la méthode imageForTileAt appelle celle du décorateur sous-jacent (baseTileProvider ci-dessous) dans le contexte d'un try/catch afin de rattraper les éventuelles IOException levées. Lorsqu'une telle exception est effectivement levée, le tableau contenant la tuile d'erreur est retourné.

public final class ErrorSilencingTP implements TileProvider {
  private final TileProvider baseTileProvider;
  private final byte[] errorBytes;

  public ErrorSilencingTP(TileProvider baseTileProvider,
			  byte[] errorBytes) {
    this.baseTileProvider = baseTileProvider;
    this.errorBytes = errorBytes.clone();
  }

  public byte[] imageForTileAt(TileId tileId) {
    try {
      return baseTileProvider.imageForTileAt(tileId);
    } catch (IOException e) {
      return errorBytes;
    }
  }
}

Partie 3

Les classes DiskCachingTP, MemoryCachingTP et ErrorSilencingTP sont toutes trois des décorateurs (Decorator).

3. Empaquetage d'entiers

Partie 1

La plus grosse partie du constructeur est consacrée à la validation des arguments. La dernière ligne stocke simplement les tailles passées, en prenant soin de les copier afin de garantir l'immuabilité de la classe.

public IntPacking(int... sizes) {
  if (sizes.length < 2)
    throw new IllegalArgumentException();
  int totalSize = 0;
  for (int size : sizes) {
    if (!(0 < size && size < Integer.SIZE))
      throw new IllegalArgumentException();
    totalSize += size;
  }
  if (!(totalSize <= Integer.SIZE))
    throw new IllegalArgumentException();
  this.sizes = sizes.clone();
}

La validation des arguments peut aussi se faire au moyen de la programmation par flots, au prix d'un double parcours du tableau des tailles :

if (!IntStream.of(sizes)
    .allMatch(s -> 0 < s && s < Integer.SIZE))
  throw new IllegalArgumentException();
if (!(IntStream.of(sizes).sum() <= Integer.SIZE))
  throw new IllegalArgumentException();

Partie 2

Vu l'ordre dans lequel les valeurs doivent être empaquetées, la manière la plus simple d'écrire la méthode pack consiste à parcourir les valeurs de gauche à droite, et d'insérer chacune d'entre elles dans les bits de poids faible de la valeur empaquetée. Pour ce faire, on décale à gauche la valeur empaquetée actuelle du nombre de bits alloués à la valeur à insérer, puis on insère cette dernière au moyen d'un « ou » logique.

public int pack(int... values) {
  if (values.length != sizes.length)
    throw new IllegalArgumentException();

  int packedValue = 0;
  for (int i = 0; i < values.length; i += 1) {
    int value = values[i];
    int size = sizes[i];
    if (!(0 <= value && value < (1 << size)))
      throw new IllegalArgumentException();
    packedValue = (packedValue << size) | value;
  }
  return packedValue;
}

Partie 3

La méthode unpack peut s'écrire en deux phases consistant à :

  1. décaler à droite la valeur empaquetée afin de placer les bits de la valeur à extraire dans les bits de poids faible, ce qui se fait au moyen d'une boucle,
  2. utiliser un masquage pour ne garder que les bits de la valeur à extraire.
public int unpack(int packedValue, int index) {
  if (!(0 <= index && index < sizes.length))
    throw new IndexOutOfBoundsException();

  for (int i = index + 1; i < sizes.length; i += 1)
    packedValue >>= sizes[i];
  return packedValue & ((1 << sizes[index]) - 1);
}

4. Vue de matrice

Partie 1

L'idée de la méthode ofRow consiste à utiliser une enjambée de ligne valant 0 afin de (conceptuellement) dupliquer la ligne reçue en argument. Les autres valeurs à passer au constructeur sont triviales.

public static MatrixView ofRow(int[] row, int rows) {
  return new MatrixView(row, 0, 1, rows, row.length);
}

Partie 2

La méthode ofColumn s'écrit de manière tout à fait similaire à ofRow.

public static MatrixView ofColumn(int[] column, int columns) {
  return new MatrixView(column, 1, 0, column.length, columns);
}

Partie 3

La méthode get est triviale et ne fait qu'accéder à l'élément du tableau dont l'index est déterminé par la formule donnée dans l'énoncé. Les index de ligne et de colonnes sont auparavant validés, car il est possible qu'ils soient invalides mais que l'index de tableau calculé à partir d'eux soit néanmoins valide.

public int get(int r, int c) {
  if (!(0 <= r && r < rows && 0 <= c && c < columns))
    throw new IndexOutOfBoundsException();
  return array[r * rStride + c * cStride];
}

Partie 4

Comme l'exemple des matrices \(M_1\) et \(M_2\) de l'énoncé l'illustre, la transposition s'obtient simplement en échangeant le rôle des enjambées et des tailles.

public MatrixView transposed() {
  return new MatrixView(array,
			cStride, rStride,
			columns, rows);
}

Partie 5

La méthode toString s'écrit relativement facilement en utilisant deux instances de StringJoiner, une pour les lignes et l'autre pour les colonnes.

public String toString() {
  StringJoiner rowsJ = new StringJoiner(",\n ", "[", "]");
  for (int r = 0; r < rows; r += 1) {
    StringJoiner columnsJ = new StringJoiner(", ", "[", "]");
    for (int c = 0; c < columns; c += 1)
      columnsJ.add(String.valueOf(get(r, c)));
    rowsJ.add(columnsJ.toString());
  }
  return rowsJ.toString();
}