Classes anonymes et interfaces fonctionnelles #
Classe anonyme #
Une classe anonyme est une manière de déclarer une classe et d’instancier un objet de celle-ci en même temps. Ceci est utile si nous souhaitons instancier qu’un seul objet d’une classe sans avoir à déclarer la classe dans un fichier.
Imaginons une interface comme celle-ci:
|
|
Nous pouvons déclarer une classe qui respecte cette interface et créer directement un objet de cette manière:
|
|
Il s’agit d’une technique très utilisée en programmation événementielle. Voici un extrait d’une interface graphique réalisée à l’aide de java.awt
et javax.swing
pour décrire l’action à réaliser lors d’un clic sur un bouton:
|
|
Interfaces fonctionnelles et lambdas #
Bien que de nombreux didacticiels en ligne préconisent encore ce type de syntaxe, depuis Java 8, une interface qui ne contient qu’une méthode abstraite est promue automatiquement en une fonction permettant la syntaxe lambda. Ce type d’interface est appelée interface fonctionnelle. Le même code s’écrirait ainsi:
|
|
Si nous reprenons notre exemple précédent, nous pouvons déclarer Hello
comme une interface fonctionnelle.
|
|
Au lieu d’écrire ceci
|
|
nous pouvons directement l’écrire ainsi
|
|
La méthode greetings
est bien une méthode qui ne prend aucun argument ()
et retourne void
, d’où la syntaxe () -> System.out.println("Hi guys")
. L’interface Hello
est donc considérée comme une fonction: () -> ()
.
Quiz #
Soit l’interface fonctionnelle suivante :
|
|
- A quelle signature de fonction correspond la méthode
f
? - Donnez un exemple d’utilisation d’une fonction lambda à appeler pour la méthode
g
ci-dessous en précisant quel serait l’affichage.
|
|
Règles #
- L’interface fonctionnelle devrait toujours être précédée de l’annotation
@FunctionalInterface
- Une interface fonctionnelle ne peut contenir qu’une méthode abstraite, mais elle peut proposer des méthodes par défaut, statiques, privées…
Quelques interfaces fonctionnelles connues #
Voici un tableau interfaces fonctionnelles que vous devrez maîtriser.
java.util.function | signature | utilisation |
---|---|---|
Function<T,R> |
T -> R |
R apply(T t) |
Consumer<T> |
T -> () |
void accept(T t) |
Supplier<T> |
() -> T |
T get() |
Predicate<T> |
T -> boolean |
boolean test(T t) |
Runnable |
() -> () |
void run() |
Exemple d’un Consumer<T>
#
Exemple du forEach
d’un itérable
List<Integer> is = ...
is.forEach( (Integer i) -> System.out.println(i) );
is.forEach( i -> System.out.println(i) );
is.forEach( System.out::println );
Exemple d’une méthode apply
utilisant un Consumer
de Resource
s
class DeviceManager {
List<Resource> resources = ...
...
void apply(Consumer<Resource> consumer) {
for(Resource r: this.resources) {
consumer.accept(r);
}
}
...
}
et son utilisation
myResourcesManager.apply( resource -> System.out.println(resource.info()) );
myResourcesManager.apply( resource -> database.save(resource) );
la même méthode permet de réaliser différents opérations. Elle se traduit par “pour une ressource, voici ce que je fais avec”.
Exemples de Function<T,R>
et Supplier<T>
et Predicate<T>
avec des Stream
s
#
Les Stream
s sont des séquences d’éléments permettant des opérations dans un style déclaratif. Ils ne stockent pas des éléments. Ils sont paresseux (lazy, c’est-à-dire qu’ils sont évalués lors de leur utilisation seulement) par nature et peuvent être infinis. Il est courant de manipuler des collections en passant par des Stream
s.
Exemple d’un Function<T,R>
#
Stream<Integer> xs = List.of(1,2,3).stream();
List<String> zs = xs.map( (Integer i) -> i.toString() )
.collect(Collectors.toList());
List<String> zs = xs.map( i -> i.toString() )
.collect(Collectors.toList());
La méthode collect
d’un Stream
permet de le transformer en une collection. En l’occurrence ici : une liste.
Exemple d’un Supplier<T>
#
Stream<A> s = Stream.generate( () -> new A() ); // Stream infinis d'objets A. Cependant, le Stream n'a pas encore été parcouru.
List<String> as = s.map( (A a) -> a.toString() ) // description d'une opération. Le Stream n'a toujours pas appelé le supplier
.limit(10) // description d'une récupération de 10 éléments. Idem
.collect(Collectors.toList()); // Cette fois-ci, les opérations se font à la chaîne
// - appel du supplier
// - application du toString
// - arrêt après la création de 10 éléments
public static int process(int a) { return a + a; }
public static int lazySafeProcess(Supplier<Integer> supplier) {
try {
return supplier.get() + supplier.get();
} catch (ArithmeticException ex) {
return 0;
}
}
...
int i = process( 3 / 0 ); // Exception ici !
int j = lazySafeDiv( () -> 3 / 0 ); // permet de différer l'exécution de l'expression
//==> j = 0
Exemple d’un Predicate<T>
#
Stream<Person> originStream = ...
List<Person> people = originStream.filter( p -> p.isWorking() )
.collect(Collectors.toList());
List<Person> people = originStream.filter( Person::isWorking )
.collect(Collectors.toList());
Exemple complet #
L’avantage des Stream
s est de pouvoir chaîner les opérations:
List<Person> people = ...
people.filter( p -> p.isWorking() ) // filtrer les personnes qui travaillent
.map( p -> p.email() ) // récupération de leur email
.forEach( email -> send("Good work!", email) ); // envoie d'un email à chaque personne qui travaillent
L’API Stream
sera couverte plus en profondeur dans un chapitre ultérieur.
Exemple d’un Runnable
#
Il s’agit d’une fonction qui ne prend aucun argument et applique en effet de bord. Elle est utilisée pour créer des Thread
s notamment (cf. cours de concurrence).
new Thread( () -> System.out.println("New thread is running") ).start();