Examen intermédiaire

CS-108 – corrigé

1. Bâtisseur de chaînes

Partie 1

Les méthodes capacity et size sont triviales à écrire pour peu qu'on se souvienne de la différence entre ces deux notions.

public int capacity() {
  return chars.length;
}

public int size() {
  return size;
}

Partie 2

La seule petite difficulté de la méthode charAt est qu'il ne faut pas oublier de valider l'index reçu, car on ne peut pas faire l'hypothèse qu'un index invalide va provoquer la levée d'une IndexOutOfBoundsException lors de l'accès au tableau. En effet, les index compris entre la taille et la capacité sont valides pour le tableau mais invalides pour le bâtisseur.

public char charAt(int index) {
  // Autre option : Objects.checkIndex(index, size)
  if (!(0 <= index && index < size))
    throw new IndexOutOfBoundsException();
  return chars[index];
}

Partie 3

La méthode append consiste en deux parties.

Premièrement, on « redimensionne » le tableau chars, par copie, si sa taille ne permet pas de stocker la totalité des caractères de la chaîne en cours de construction, après l'ajout. Pour cela, on fait progressivement doubler sa taille jusqu'à ce qu'elle soit suffisante.

Deuxièmement, une fois le tableau éventuellement « redimensionné », on y copie les caractères de la chaîne au moyen de la méthode getChars, avant de mettre à jour la taille de la chaîne en cours de construction.

public SStringBuilder append(String s) {
  int size1 = size + s.length();

  int capacity1 = chars.length;
  while (capacity1 < size1)
    capacity1 <<= 1; // autre option : capacity1 *= 2
  if (capacity1 != chars.length)
    chars = Arrays.copyOf(chars, capacity1);

  s.getChars(0, s.length(), chars, size);
  size = size1;
  return this;
}

Partie 4

La principale difficulté de la méthode removeIf est de ne faire qu'un seul parcours du tableau chars pour éliminer les éléments indésirés. Cela peut se faire au moyen de deux index, nommés src et dst ci-dessous.

L'index src parcourt tout le tableau et désigne le caractère en cours d'examen. L'index dst quant à lui désigne la position à laquelle le prochain caractère non supprimé doit être placé, et il est toujours inférieur ou égal à src.

Lorsque le caractère désigné par src doit être gardé, c.-à-d. que la méthode test du prédicat retourne faux pour lui, on le copie au besoin à la position désignée par dst, puis on avance dst.

Une fois la totalité du tableau parcouru par src, dst contient forcément l'index du caractère qui suit le dernier qui a été conservé, soit la nouvelle taille de la chaîne en cours de construction.

public SStringBuilder removeIf(Predicate<Character> predicate) {
  int dst = 0;
  for (int src = 0; src < size; src += 1) {
    if (predicate.test(chars[src])) continue;
    if (dst != src) chars[dst] = chars[src];
    dst += 1;
  }
  size = dst;
  return this;
}

La technique utilisée par removeIf est identique à celle utilisée, dans le projet, pour éliminer les tuples dominés par le nouveau lors d'un ajout.

Partie 5

La méthode build est triviale et consiste en un simple appel au constructeur de String mentionné dans l'énoncé.

public String build() {
  return new String(chars, 0, size);
}

2. Tableau d'octets

Partie 1

L'écriture de la classe ByteArrayMSBF ne pose pas de problème particulier, si ce n'est que dans la méthode getShort il ne faut pas oublier d'appliquer toUnsignedInt à l'octet de poids faible, faute de quoi son bit de poids le plus fort est dupliqué dans les 8 bits de poids fort du résultat lors de l'extension de signe.

public final class ByteArrayMSBF implements ByteArray {
  private final byte[] array;

  public ByteArrayMSBF(byte[] array) {
    this.array = array;
  }

  @Override
  public int size() {
    return array.length;
  }

  @Override
  public byte getByte(int index) {
    // Optionnel:
    // if (!(0 <= index && index < size())) throw …
    return array[index];
  }

  @Override
  public short getShort(int index) {
    // Optionnel:
    // if (!(0 <= index && index < size() - 1)) throw …
    return (short) ((array[index] << 8)
                    | Byte.toUnsignedInt(array[index + 1]));
  }
}

Partie 2

La classe ByteArraySlice est plus complexe à écrire correctement que ByteArrayMSBF car ses méthodes getByte et getShort doivent obligatoirement valider l'index reçu. En effet, tout comme pour la méthode charAt de l'exercice précédent, il existe des index invalides pour une tranche mais valides pour le tableau sous-jacent.

Le constructeur doit lui aussi valider les index reçus, comme spécifié dans l'énoncé.

public final class ByteArraySlice implements ByteArray {
  private final ByteArray byteArray;
  private final int fromInclusive, size;

  public ByteArraySlice(ByteArray byteArray,
                        int fromInclusive,
                        int toExclusive) {
    if (!(0 <= fromInclusive
          && fromInclusive < byteArray.size()))
      throw new IndexOutOfBoundsException();
    if (!(fromInclusive <= toExclusive
          && toExclusive <= byteArray.size()))
      throw new IndexOutOfBoundsException();

    this.byteArray = byteArray;
    this.fromInclusive = fromInclusive;
    this.size = toExclusive - fromInclusive;
  }

  @Override
  public int size() {
    return size;
  }

  @Override
  public byte getByte(int index) {
    if (!(0 <= index && index < size()))
      throw new IndexOutOfBoundsException();
    return byteArray.getByte(fromInclusive + index);
  }

  @Override
  public short getShort(int index) {
    if (!(0 <= index && index + 1 < size()))
      throw new IndexOutOfBoundsException();
    return byteArray.getShort(fromInclusive + index);
  }
}

Partie 3

Étant donné l'existence de la classe ByteArraySlice, la méthode slice est triviale à écrire, il faut juste se rendre compte que le tableau à « trancher » n'est autre que le récepteur, this.

Aucune vérification d'index n'est à faire, car elles sont faites par le constructeur de ByteArraySlice.

default ByteArray slice(int fromInclusive, int toExclusive) {
  return new ByteArraySlice(this, fromInclusive, toExclusive);
}

Partie 4

Même si la méthode slice de ByteArray fonctionne dans tous les cas, elle n'est pas très efficace lorsqu'on obtient une tranche d'une tranche, par exemple ainsi :

byte[] a = …;
ByteArray byteArray = new ByteArrayMSBF(a);
ByteArray doubleSlice = byteArray;
doubleSlice = doubleSlice.slice(1, doubleSlice.size() - 1);
doubleSlice = doubleSlice.slice(1, doubleSlice.size() - 1);

En effet, l'objet référencé par doubleSlice est une tranche de type ByteArraySlice qui référence une seconde tranche du même type, qui référence le tableau byteArray. Dès lors, chaque appel à la méthode getByte ou getShort de cet objet implique d'effectuer 2 appels à la même méthode de ByteArraySlice. Et bien entendu, le problème s'aggrave encore si on empile plus de deux tranches.

Sachant qu'une tranche de tranche est équivalente à une unique tranche, on peut donc redéfinir la méthode slice dans ByteArraySlice afin qu'elle évite l'empilement de tranches. Ce faisant, il ne faut pas oublier de valider les index, car cette tâche ne peut plus être laissée au constructeur de ByteArraySlice comme précédemment, toujours pour les mêmes raisons.

@Override
public ByteArray slice(int fromInclusive, int toExclusive) {
  if (!(0 <= fromInclusive && fromInclusive < size()))
    throw new IndexOutOfBoundsException();
  if (!(fromInclusive <= toExclusive && toExclusive <= size()))
    throw new IndexOutOfBoundsException();

  int fromInclusive1 = this.fromInclusive + fromInclusive;
  int toExclusive1 = this.fromInclusive + toExclusive;
  return new ByteArraySlice(byteArray,
                            fromInclusive1,
                            toExclusive1);
}

3. Détecteur de fins de lignes

Partie 1

La première variante de lineEnds doit lire les caractère du flot et faire le nécessaire chaque fois qu'un caractère LF ou CR est rencontré.

Lorsqu'un caractère CR est rencontré, le caractère suivant est également lu afin de voir s'il s'agit d'un LF. Si tel est le cas, l'élément CR_LF est ajoutée à l'ensemble, sinon c'est l'élément CR qui l'est. Finalement, si ce caractère est un CR, alors le test est répété.

Sachant que les caractères LF qui suivent un caractère CR sont traités par le cas précédent, les caractères LF restants provoquent simplement l'ajout de l'élément LF à l'ensemble.

Set<LineEnd> lineEnds(Reader reader) throws IOException {
  int nextChar;
  // Autre option : lineEnds = new HashSet<>()
  Set<LineEnd> lineEnds = EnumSet.noneOf(LineEnd.class);
  while ((nextChar = reader.read()) != -1) {
    switch (nextChar) {
      case '\n' -> lineEnds.add(LineEnd.LF);
      case '\r' -> {
        do {
          nextChar = reader.read();
          lineEnds.add(nextChar == '\n'
                       ? LineEnd.CR_LF
                       : LineEnd.CR);
        } while (nextChar == '\r');
      }
    }
  }
  return lineEnds;
}

Une autre manière d'écrire lineEnds consiste à utiliser une variable booléenne, nommée pendingCR ci-dessous, pour savoir si le caractère précédent était un CR.

Set<LineEnd> lineEnds(Reader reader) throws IOException {
  int nextChar;
  boolean pendingCR = false;
  // Autre option : lineEnds = new HashSet<>()
  Set<LineEnd> lineEnds = EnumSet.noneOf(LineEnd.class);
  while ((nextChar = reader.read()) != -1) {
    if (nextChar == '\n')
      lineEnds.add(pendingCR ? LineEnd.CR_LF : LineEnd.LF);
    else if (pendingCR)
      lineEnds.add(LineEnd.CR);
    pendingCR = (nextChar == '\r');
  }
  if (pendingCR) lineEnds.add(LineEnd.CR);
  return lineEnds;
}

Partie 2

La seconde variante de lineEnds consiste en un simple appel à la première qui est fait dans le contexte d'un try-with-resource qui garantit que le flot permettant de lire les caractères du fichier est fermé dans tous les cas.

Set<LineEnd> lineEnds(String fileName) throws IOException {
  try (Reader s = new FileReader(fileName)) {
    return lineEnds(s);
  }
}