:: Enseignements :: Master :: M1 :: 2018-2019 :: Java Avancé ::
[LOGO]

Examen de Java Avancé - Session 1


Le but de ce examen est d'implanter une base de données (enfin une liste d’éléments) en mémoire ayant des index (listes d'indices) permettant d'accélérer la recherche.

Vos sources Java produites pendant l'examen devront être placées sous le répertoire EXAM de votre compte ($HOME) (qui est vierge dans l'environnement de TP noté); sinon, elles ne seront pas récupérées.

Tout document papier est proscrit.
Vous pouvez consulter la javadoc à travers Eclipse (onglet Javadoc) ou en tapant jdoc dans un terminal. Sinon la doc est là: /usr/local/apps/java11/docs/api/index.html.
Les seuls documents électroniques autorisés sont les supports de cours à l'url http://igm.univ-mlv.fr/~forax/ens/java-avance/cours/pdf/.

Exercice 1 - Repository

Un Repository est un objet qui contient une liste d'éléments indexés (numérotés) non nuls. La classe Repository est typée par le type des éléments que les instances de la classe contiennent. La méthode add permet d'ajouter des éléments dans le Repository. Il n'est pas possible de supprimer des éléments (sauf en reconstruisant un nouveau Repository).
Un Selector est une liste d'entiers triés (des indices) construite à partir d'une fonction indiquant si un élément est présent ou non dans le Selector. La méthode addSelector permet d'ajouter au Repository un Selector créé à partir d'une fonction de test qui renvoie un booléen pour un élément passé en paramètre. Si cette fonction de test renvoie vrai pour un élément, alors la position de cet élément (dans la liste des éléments du Repository) est stockée dans la liste des entiers de ce Selector

Par exemple avec le code suivant,
  var repository = new Repository<String>();
  repository.add("foo");
  repository.add("baz");
  repository.add("booz");
  var selector = repository.addSelector(s -> s.contains("oo"));
  System.out.println(selector);
    
le Selector affiche [0, 2] car les éléments à l'indice 0 ("foo") et à l'indice 2 ("booz") contiennent "oo". L'indice 1 ne fait pas partie du Selector car "baz" (l'élément à l'indice 1) ne contient pas "oo".
Enfin, la méthode createQuery permet de créer un objet Query associé au repository et sur lequel on peut demander d'appliquer plusieurs Selector (avec la méthode select) puis de ressortir le Stream des éléments vérifiant les différents Selector grâce à la méthode toStream.

Par exemple, avec le code suivant,
  var repository = new Repository<String>();
  repository.add("foo");
  repository.add("baz");
  repository.add("booz");
  var selector1 = repository.addSelector(s -> s.contains("oo"));
  var selector2 = repository.addSelector(s -> charAt(0) == 'f');
  var query = repository.createQuery();
  var list = query.select(selector1).select(selector2).toStream().collect(toList());
    
la liste list contient uniquement l'élément "foo", car c'est le seul élément qui vérifie les deux Selector (contenir "oo" et avoir 'f' comme première lettre).

En terme d'implantation, on pré-suppose que l'on ajoute rarement des éléments au Repository mais que par contre, on effectue fréquemment des requêtes de sélection. C'est pour cela que chaque Selector maintient un index en mémoire plutôt que de ré-appliquer les fonctions de test de chaque Selector pour chaque requête.

Certains tests unitaires correspondant à l'implantation sont ici: RepositoryTest.java.

  1. Avant de commencer à implanter les méthodes publiques de Repository, nous allons implanter la méthode utilitaire and, dans Repository, qui prend deux Iterator sur des entiers, pré-suppose que ceux-ci sont triés en ordre croissant et renvoie un nouvel Iterator qui 'contient' les entiers communs aux deux itérateurs (eux aussi en ordre croissant).
    Une façon naïve d'implanter la méthode and consiste à créer deux Set contenant les valeurs des itérateurs, puis utiliser retainAll pour sélectionner les éléments communs et enfin trier l'ensemble, soit en créant le bon ensemble pour qu'il trie les éléments, soit en triant les éléments à posteriori. Et enfin, demander l’itérateur de cet ensemble.
    Évidemment, l'implantation proposée précédemment n'est pas très efficace car elle demande des stockages intermédiaires. Il est bien plus efficace d'implanter directement l'itérateur résultant en ne stockant en intermédiaire que la future valeur qui doit être envoyée par l'itérateur.
    Implanter la méthode and.
    Note: si vous n'arrivez pas à implanter l'itérateur directement, faite la version naïve.
     
  2. Implanter les méthodes add et addSelector de telle façon que si on affiche les indices d'un Selector, on obtienne les bonnes valeurs.
    Note: addSelector peut être appelée à n'importe quel moment, pas forcément après les appels à add. Par exemple:
      var repository = new Repository<String>();
      repository.add("dad");
      repository.add("franck");
      var selector = repository.addSelector(s -> s.length() == 3);
      repository.add("bob");
      System.out.println(selector.toString()); // affiche "[0, 2]"
          
  3. Modifier la méthode permettant d'afficher un Selector de telle façon que si il y a plus de 5 indices à afficher, on affiche les 4 premiers indices (séparé par des virgules), suivis par "...", suivi par la dernière valeur d'indice.
    Indication: utiliser un Stream ici permet de simplifier le code.
     
  4. Avant d'implanter le système de Query, nous allons introduire la notion d'Index qui correspond à un objet qui est capable de créer un Iterateur d'entiers triés.
    Un Index est une interface qui possède
    • une méthode of qui permet de créer un Index à partir d'une liste d'entiers triées; l’itérateur renvoie les éléments de la liste.
    • une méthode abstraite iterator qui renvoie un Iterateur sur les entiers de l'index.
    • une méthode d'instance and qui prend un Index en paramètre et renvoie un autre Index dont l'itérateur contient (toujours triés) les entiers communs aux deux index (l'index courant et celui passé en paramètre).
    Vous pouvez noter que, définie ainsi, Index est une interface fonctionnelle et donc of et and peuvent implanter l'Index renvoyé en utilisant des lambdas.
    Note: si vous n'arrivez pas à implanter Index ainsi, implanter Index avec une interface classique.
     
  5. Implanter la méthode createQuery qui renvoie un objet de type Query ainsi que la méthode toStream sur l'objet de type Query qui renvoie le Stream des éléments dans le Repository.
    Ici, l'idée est de créer un Iterator contenant les indices de 0 au nombre d'éléments dans le Repository, de créer un Stream à partir de cette itérateur grâce aux méthodes Spliterators.spliteratorUnknownSize et StreamSupport.stream, puis, pour chaque indice, lui associer l'élément dans le Repository.
    Note: vous pouvez implanter la méthode toStream beaucoup plus simplement car il n'y a pas de sélecteur pour l'instant, mais l'idée est de préparer le terrain pour la question suivante qui introduit les sélecteurs.
     
  6. Ajouter la méthode select au type Query qui permet de spécifier un Selector. La méthode toStream renvoie alors le Stream des éléments qui vérifient les différents sélecteurs spécifiés pour la Query. Les appels à select sur une même Query doivent pouvoir être chaînés comme dans l'exemple:
      var repository = new Repository<String>();
      repository.add("foo");
      repository.add("baz");
      repository.add("booz");
      var selector1 = repository.addSelector(s -> s.contains("oo"));
      var selector2 = repository.addSelector(s -> charAt(0) == 'f');
      var query = repository.createQuery();
      var list = query.select(selector1).select(selector2).toStream().collect(toList());
      System.out.println(list); // affiche "[foo]"
        
    En interne, pour représenter un sélecteur, vous pouvez ré-utiliser la classe Index précédemment introduite; l’enchaînement des différents sélecteurs correspond à faire des and entre les différents Index.
     
  7. On peut remarquer que, si on applique un seul sélecteur, alors le résultat revient à utiliser uniquement les indices de ce sélecteur.
    On peut aussi remarquer que les sélecteurs peuvent être appliqués dans n'importe quel ordre; le résultat sera identique. Il est donc intéressant de réfléchir à l'ordre dans lequel on doit appliquer les sélecteurs pour minimiser les calculs.
    En utilisant les deux remarques précédentes, modifier votre code pour améliorer son efficacité.