Classes anonymes et interfaces fonctionnelles

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:

1
2
3
public interface Hello {
  void greetings();
}

Nous pouvons déclarer une classe qui respecte cette interface et créer directement un objet de cette manière:

1
2
3
4
5
6
7
8
9
Hello h = new Hello() {
  public void greetings() { System.out.println("Hi guys!"); }
};
h.greetings(); // Affiche Hi guys!

// ou directement
new Hello() { 
  public void greetings() { System.out.println("Hi!"); } 
}.greetings(); // Affiche Hi!

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:

1
2
3
4
5
6
7
JButton myButton = new JButton("click me");
myButton.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent ev) {
        System.out.println("Someone has clicked on the button");
    }
});

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:

1
2
JButton myButton = new JButton("click me");
myButton.addActionListener(ev -> System.out.println("Someone has clicked on the button"));

Si nous reprenons notre exemple précédent, nous pouvons déclarer Hello comme une interface fonctionnelle.

1
2
3
4
@FunctionalInterface
public interface Hello {
  void greetings();
}

Au lieu d’écrire ceci

1
2
3
4
Hello h = new Hello() {
  public void greetings() { System.out.println("Hi guys!"); }
};
h.greetings();

nous pouvons directement l’écrire ainsi

1
2
Hello h = () -> System.out.println("Hi guys!");
h.greetings();

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 :

1
2
3
4
@FunctionalInteface
public interface Test {
    String f(int i);
}
  1. A quelle signature de fonction correspond la méthode f ?
  2. Donnez un exemple d’utilisation d’une fonction lambda à appeler pour la méthode g ci-dessous en précisant quel serait l’affichage.
1
2
3
4
5
6
7
8
9
public class TestApp {
    public static String g(Test t) {
        return t.f(0) + t.f(1);  
    }
    public static void main(String[] args) {
        String test = g( ... );
        System.out.println( test );
    }
}

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 Resources

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 Streams #

Les Streams 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 Streams.

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 Streams 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 Threads notamment (cf. cours de concurrence).

new Thread( () -> System.out.println("New thread is running") ).start();











comments powered by Disqus