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"));
((Person)s.getValue()).setAge(39);
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(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.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 positionQueue<E>
– modélise une file d’attente simpleDeque<E>
– extension de Queue, définit une file d’attente à double extrémitéSet<E>
– définit un ensemble sans doublonsSortedSet<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
SortedSet<E>
=>TreeSet
Map<K, V>
=>HashMap
(utilise les tables de hashage pour stocker les éléments)
(utilise les arbres pour stocker les éléments)
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
// 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émentfilter(Predicate)
: ne concerver que les éléments vérifiant le prédicatmap(Function mapper)
: remplacer chaque élément du flux par le résultat du lambdamapper
limit(long n)
: ne conserver que lesn
premiers éléments du fluxsorted()
: retourne un flux triéreduce(BinaryOperator)
: effectue une réduction du flux à une unique valeurtoArray()
: retourne un tableau contenant l’ensemble des valeurscollect(...)
: 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,>