Généricité et Collections


Généricité

Problématique

Prenons l’exemple d’une classe SingleValueString qui possède :

  • un constructeur SingleValueString(String val)
  • une méthode (modificateur) setValue(String val)
  • une méthode (accesseur) String getValue()

 

Cette classe permet de stocker une valeur de type String.


SingleValueString s = new SingleValueString("foo");
System.out.println(s.getValue());
                        

Problématique

Si on veut pouvoir stocker un entier, on peut créer une classe SingleValueInt avec :

  • un constructeur SingleValueInt(int val)
  • une méthode (modificateur) setValue(int val)
  • une méthode (accesseur) int getValue()

 


SingleValueInt s = new SingleValueInt(42);
int c = s.getValue() + 1;
System.out.println(c);
                        

Problématique

Si on veut pouvoir stocker une personne, on peut créer une classe SingleValuePerson avec :

  • un constructeur SingleValuePerson(Person val)
  • une méthode (modificateur) setValue(Person val)
  • une méthode (accesseur) Person getValue()

 


SingleValuePerson s = new SingleValuePerson(new Person("alice"));
s.getValue().setAge(39);
System.out.println(s);
                        

Problématique

Si on veut pouvoir stocker une valeur quelconque, on peut créer une classe SingleValue avec :

  • un constructeur SingleValue(Object val)
  • une méthode (modificateur) setValue(Object val)
  • une méthode (accesseur) Object getValue()

 


SingleValue s = new SingleValue(new Person("alice"));
s.getValue().setAge(39);
System.out.println(s);
                        

SingleValue s = new SingleValue(new Person("alice"));
s.getValue().setAge(39); // Impossible, Object n'a pas de méthode setAge()
System.out.println(s);
                        

SingleValue s = new SingleValue(new Person("alice"));
((Person)s.getValue()).setAge(39);
System.out.println(s);
                        

SingleValue s = new SingleValue(45);
int c = 1 + (       )s.getValue();
System.out.println(s);
                        

SingleValue s = new SingleValue(45);
int c = 1 + (Integer)s.getValue();
System.out.println(s);
                        

Interlude : les classes enveloppes

A chaque type de base correspond une classe enveloppe :

  • char => Character
  • int => Integer
  • long => Long
  • boolean => Boolean

 

C’est l’équivalent objet des types de base. Elles s’utilisent comme les types de base, mais ont l’avantage de pouvoir être stockées dans une variable de type Object.


Integer a = 5; // équivalent à Integer a = new Integer(5)
Object b = a; // Ok
System.println(b); // 5
                      

Problématique

Si on veut pouvoir stocker une valeur quelconque, on peut créer une classe SingleValue avec :

  • un constructeur SingleValue(Object val)
  • une méthode (modificateur) setValue(Object val)
  • une méthode (accesseur) Object getValue()

 


SingleValue s = new SingleValue(new Person("alice"));
((Person)s.getValue()).setAge(39);
System.out.println(s);
                        

SingleValue s = new SingleValue(45);
int c = 1 + (Integer)s.getValue();
System.out.println(s);
                        

Type générique

On aimerait avoir une classe permettant de stocker des types quelconques mais sans avoir une classe différente par type.

Java propose d’utiliser un type générique T qui sert à représenter un type d’objet quelconque.

  • un constructeur SingleValue(T val)
  • une méthode (modificateur) setValue(T val)
  • une méthode (accesseur) T getValue()

 

Solution


public class SingleValue<T> {

  //Variable d'instance
  private T valeur;

  //Constructeur par défaut
  public SingleValue(){
    this.valeur = null;
  }

  //Constructeur avec paramètre inconnu pour l'instant
  public SingleValue(T val){
    this.valeur = val;
  }

  //Définit la valeur avec le paramètre
  public void setValue(T val){
    this.valeur = val;
  }

  //Retourne la valeur déjà « castée » par la signature de la méthode !
  public T getValue(){
    return this.valeur;
  }
}

Utilisation


SingleValue<Person> s = new SingleValue<Person>(new Person("alice"));
s.getValue().setAge(39);
System.out.println(s);

SingleValue<Person> s = new SingleValue(new Person("alice"));
s.getValue().setAge(39);
System.out.println(s);

SingleValue<Integer> s = new SingleValue(45);
int c = 1 + s.getValue();
System.out.println(s);

Autre exemple

On peut par exemple créer une classe permettant de stocker deux objets de même type.


class PairValue<T> {

  //Variable d'instance
  private T valeur1, valeur2;

  //Constructeur avec paramètre inconnu pour l'instant
  public PairValue(T val1, T val2){
    this.valeur1 = val1;
    this.valeur2 = val2;
  }
  public void setValue1(T val){
    this.valeur1 = val;
  }
  public void setValue2(T val){
    this.valeur2 = val;
  }
  public T getValue1(){
    return this.valeur1;
  }
  public T getValue2(){
    return this.valeur2;
  }

}
                      

Autre exemple

On peut par exemple créer une classe permettant de stocker deux objets de type quelconque.


class PairValue<T, S> {

  //Variable d'instance
  private T valeur1,
  private S valeur2;

  //Constructeur avec paramètre inconnu pour l'instant
  public PairValue(T val1, S val2){
    this.valeur1 = val1;
    this.valeur2 = val2;
  }
  public void setValue1(T val){
    this.valeur1 = val;
  }
  public void setValue2(S val){
    this.valeur2 = val;
  }
  public T getValue1(){
    return this.valeur1;
  }
  public S getValue2(){
    return this.valeur2;
  }

}
                      

?


SingleValue<Object> s = new SingleValue<Integer>(45); // Impossible
                      

SingleValue<Object> s = new SingleValue(45);
// Equivalent a
// SingleValue<Object> s = new SingleValue<Object>(45);

s.setValue('a'); // Donc possible
                      

// s est un SingleValue de type quelconque
SingleValue<?> s = new SingleValue(45);
s.setValue('a'); // impossible
                      

? extends T


// s est un SingleValue de type quelconque
SingleValue<? extends Number> s = new SingleValue(45);
s.setValue('a'); // impossible
System.out.println(s.getValue().floatValue()); // 45.0
SingleValue<? extends Number> s = new SingleValue(45.5);
System.out.println(s.getValue().floatValue()); // 45.5
                      

Comparable

l’interface Comparable<T> impose un ordre total sur tous les objets des classes qui l’implémente ; elle charactérise les objets pouvant se compararer avec le type T donné. Une classe qui implémente cette interface doit redéfinir la méthode compareTo(T o)

Exemple, la classe Float implémente Comparable<Float>.

Méthodes génériques


public class MaximumTest {

  public static <T extends Comparable<T>> T maximum(T x, T y) {
    T max = x;   // assume x is initially the largest

    if(y.compareTo(max) > 0) {
       max = y;   // y is the largest so far
    }

    return max;   // returns the largest object
  }
}
                      

System.out.printf( MaximumTest.<Integer>maximum( 3, 4 ) );
                      

System.out.printf( MaximumTest.maximum( 3, 4 ) );
                      

Collections

Collections génériques

On voit l’intérêt des types génériques pour les clases qui implémente l’interface Collection<E> (pour stocker un ensemble d’objet) ou l’interface Map<K, V> (pour stocker des clé/valeur).

De base une collection possède des fonctionnalité de base (ajouter/supprimer un élément, parcourir les éléments, etc)

Collections génériques

Les intérfaces définissant des fonctionnalités supplémentaires existent. Par exemple :

  • List<E> – modélise un ensemble avec doublons. On peut accéder à une donnée à partir d’une position ; récupérer une donnée à partir d’une position
  • Queue<E> – modélise une file d’attente simple
  • Deque<E> – extension de Queue, définit une file d’attente à double extrémité
  • Set<E> – définit un ensemble sans doublons
  • SortedSet<E> – extension de set, définit un ensemble trié
  • SortedMap<K, V> – définit un map trié suivant les clés

 

Collections génériques

Ces interfaces sont implémentées par plusieurs classes. Par exemple :

 

  • Deque<E> => ArrayDeque, LinkedList
  • List<E> => ArrayList, LinkedList
  • Set<E> => HashSet
  • (utilise les tables de hashage pour stocker les éléments)

  • SortedSet<E> => TreeSet
  • (utilise les arbres pour stocker les éléments)

  • Map<K, V> => HashMap

 

L’interface List


List<String> listeString = new ArrayList<String>();
listeString.add("Une chaîne");
listeString.add("Une autre");
listeString.add("Encore une autre");
listeString.add("Allez, une dernière");

for(String str : listeString)
    System.out.println(str);

List listeDouble = new ArrayList();
listeDouble.add(12.25);
listeDouble.add(15.25);
listeDouble.add(2.25);
listeDouble.add(128764.25);

for(double f : listeDouble)
    System.out.println(f);
    

Stream et Lambda

Lambda

Un lambda est une fonction anonyme (sans nom)

Elle permet de définir des callback, des fonctions à effectuer en réponse à des événements, des filtres, etc…


Function<Integer, Double> f =  (n) -> { return n / 2.0; };
System.out.println(f.apply(5));

Consumer<T> c = n -> { return n * 2;  };
System.out.println(c.apply(5));
                      

Interface fonctionnelle – Lambda

Une interface possédant une unique méthode est aussi un lambda (c’est une interface fonctionnelle)


interface Doubler {
    int timesTwo(int x);
}
 class Main {
    public static void main(String[] args) {
        Doubler d = n -> n * 2;
        System.out.println(d.timesTwo(5));
    }
}
                      

Lambda dans un thread


Runnable task1 = new Runnable(){

    @Override
    public void run(){
        System.out.println("Task #1 is running");
    }
};

Thread thread1 = new Thread(task1);
thread1.start();

// Lambda Runnable
Runnable task = () -> { System.out.println("Task #1 is running"); };

// start the thread
new Thread(task).start();
                      

Lambda dans un thread


                      import java.lang.Runnable;

class Main {
	 
public static void main(String[] args) {
  Runnable task1 = new Runnable(){
    @Override
    public void run(){
        while(true)  System.out.println("Task #1 is running");
    }
};
  
Thread thread1 = new Thread(task1);
thread1.start();
Runnable task = () -> { while(true) System.out.println("Task #2 is running"); };
new Thread(task).start(); 
}
}
                      

Lambda et closure

Les lambdas ont accés (en lecture) aux variables locales du context où ils sont définis


class Main {
	public static Function<Integer, Integer> inc(int i) {
		return (n) -> { return n + i; };
	}
  public static void main(String[] args) {

    Function<Integer, Integer> f = inc(1);
    System.out.println(f.apply(10));
    System.out.println(f.apply(42));

    f = inc(10);
    System.out.println(f.apply(10));
    System.out.println(f.apply(42));
  }
}
                      

Lambda et le tri


Collections.sort(personList, (p1, p2) -> p1.getAge() - p2.getAge());
                      

Stream

l’interface Collection possède une méthode Stream stream() qui permet d’obtenir un flux qui parcours les données de la collection.

Un stream (flux) est un objet qui représente l’ensemble des objets contenus dans la collection.

Stream

On peut enchainer des opérations sur un flux :

  • forEach(Consumer) : effectue une action pour chaque élément
  • filter(Predicate) : ne concerver que les éléments vérifiant le prédicat
  • map(Function mapper) : remplacer chaque élément du flux par le résultat du lambda mapper
  • limit(long n) : ne conserver que les n premiers éléments du flux
  • sorted() : retourne un flux trié
  • reduce(BinaryOperator) : effectue une réduction du flux à une unique valeur
  • toArray() : retourne un tableau contenant l’ensemble des valeurs
  • collect(...) : récupérer les valeurs dans une structure donnée.

 

Stream


List myList = Arrays.asList("a1", "a2", "b1", "c2", "c1");

myList
  .stream()
  .filter(s -> s.startsWith("c"))
  .map(String::toUpperCase)
  .sorted()
  .forEach(System.out::println);
  // C1
  // C2
  

 IntStream.range(1, 4)
    .map(n -> 2 * n + 1)
    .average()
    .ifPresent(System.out::println);  // 5.0
                      

Exemple

                     
                      IntStream i = IntStream.range(1,7);
    i.forEach(System.out::println); 
    

Stream.of("d2", "a2", "b1", "a1","b3")
    .filter(s -> 
        s.startsWith("a"))
    .filter(s -> {
        System.out.println("filter2: " + s);
        return true;
    })
    .forEach(s -> System.out.println("forEach: " + s));

Exemple


Stream.of("d2", "a2", "b1", "b3")
    .filter(s -> {
        System.out.println("filter: " + s);
        return true;
    })
    .forEach(s -> System.out.println("forEach: " + s));
                      

filter:  d2
forEach: d2
filter:  a2
forEach: a2
filter:  b1
forEach: b1
filter:  b3
forEach: b3
                      

Exemple

                    
                    
                      Stream.of("d2", "a2", "b1", "a1","b3")
    .filter(s -> 
        {System.out.println("filter3: " + s);
         return s.startsWith("a");})
    .filter(s -> {
        System.out.println("filter4: " + s);
        return true;
    })
    .forEach(s -> System.out.println("forEach2: " + s));
                    
  

collect

Récupération des personne dont le nom commence par p, dans une List.


List filtered = persons
        .stream()
        .filter(p -> p.getName().startsWith("P"))
        .collect(Collectors.toList());
                      

collect

Grouper les personnes par age dans une Map ou chaque age est associé à la liste des personnes ayant cet age.


Map<integer, list<person="">> personsByAge = persons
  .stream()
  .collect(Collectors.groupingBy(p -> p.getAge()));</integer,>