Collections

Pour une référence complète, voir Java 8 Lambdas, par Richard Warburton (O’Reilly) ou le tutorial Oracle.

Collection = plusieurs choses regroupées, dont le regroupement peut être manipulé.

Les collections définissent 2 structures de base :
- Une Collection est un regroupement d'objets.
- Une Map est un groupement d'associations (= dictionnaire, tableau associatif). Collections Collections Lorsqu'on choisit une implémentation de Collection pour travailler, prendre en compte les performances, suivant les cas d'utilisation.
Par ex, LinkedList a de très bonnes preformances pour l'ajout / suppression d'éléments, mais pas pour du random access.
Voir la marker interface java.util.RandomAccess :
List<?> l = ...;
if(!(l instanceof RandomAccess)) l = new ArrayList<?>(l);
ArrayList est une implémentation très souvent utilsée.

Vector n'est là que pour des raisons de compatibilité ascendante, utiliser ArrayList à la place.
Stack n'est là que pour des raisons de compatibilité ascendante, utiliser Deque à la place.

Deux classes contenant des utilitaires (méthodes statiques) souvent utilisés :
- java.util.Collections
- java.util.Arrays
Voir aussi java.lang.System.arraycopy(), java.lang.Class.isArray()

L'interface collection

Définit les méthodes communes à toutes les implémentations, voir l'apidoc de java.util.Collection.
Collection<E>, paramétrée par le type d'éléments qu'elle contient.

Quelques exemples d'utilisation :
// création "normale"
Collection<String> c1 = new ArrayList<>();

// création à partir de méthodes utilitaires
Collection<String> c2 = Arrays.asList("toto", "titi");

// La plupart des implémentations ont un constructeur par recopie
Collection<String> c3 = new ArrayList<String>(c2);

// Ajout
c2.add("tata"); // Ajout d'un élément
c3.addAll(c2); // Ajout de tous les éléments d'une autre collection

// Suppression
c2.remove("toto"); // Supprime un seul élément
c2.removeAll(c3); // Supprime une collection d'élément
c2.retainAll(c3); // Supprime tous les éléments qui ne sont pas dans c3
c.clear(); // RAZ, supprime tous les éléments d'une collection

// Accès aux éléments
c2.get(1);

// Taille d'une collection
boolean b = c1.isEmpty(); // c is now empty, so true
int s = c1.size();

// La plupart des implementations de Collection ont redéfini toString()
System.out.println(c1);

// Conversion vers un tableau.
Object[] elements = c.toArray();
String[] strings = c.toArray(new String[c.size()]);
(code dans Demo1.java) Toutes ces opérations sont utilisables avec n'importe quelle implémentation des sous-interfaces de Collection (Set, List ou Queue).

Itération

Itération classique

Boucle for ou foreach :
List<String> c = new ArrayList<String>();
for(String word : c) {
    System.out.println(word);
}
for( declaration : expression )
    statement

Iterator

expression doit être un tableau, ou une classe implémentant java.lang.Iterable.
public interface Iterable<E> {
    java.util.Iterator<E> iterator();
}
Dans une boucle classique, l'itérateur sous-jacent est invisible.

on peut explicitement utiliser l'itérateur d'une collection car java.util.Collection définit une méthode :
public Iterator<E> iterator()
Une boucle classique équivaut à :
List<String> c = new ArrayList<String>();
for(Iterator<String> i = c.iterator(); i.hasNext();) {
    System.out.println(i.next());
}
Utilisable aussi avec while :
Iterator<String> iterator() = c.iterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
}
Interface java.util.Iterator :
public interface Iterator<E> {
    boolean hasNext();
    E next();
    void remove(); 
}
Attention, next() fait 2 choses : renvoie l'élément courant de l'itération ET avance dans la collection.

Iterator est paramétré par le type de la collection qu'elle traverse :
Il faut un Iterator<String> pour parcourir une Collection<String>.

Maps

Une map est une collection contenant des associations clé, valeur ; parfois appelé dictionnaire ou tableau associatif.

Exemples d'utilisation :
import java.util.*;

class Personne{
    private String nom;
    public Personne(String nom) { this.nom = nom; }
    @Override public String toString() { return nom; }
}

public class MapTest{
    
    public static void main(String[] args) {
        
        // création de la map
        
        Map<String, Personne> map = new HashMap<>();
        map.put("p1", new Personne("Personne 1"));
        map.put("p2", new Personne("Personne 2"));
        map.put("p3", new Personne("Personne 3"));
        
        // Affichage
        
        System.out.println("=== Utilisation de values() ===");
        for(Personne p : map.values()) {
            System.out.println(p); // n'affiche que les valeurs
        }
        
        System.out.println("=== Utilisation de keySet() ===");
        for(String key : map.keySet()) {
            System.out.println(key + " : " + map.get(key));
        }
        
        System.out.println("=== Utilisation de entrySet() ===");
        for(Map.Entry<String, Personne> entry : map.entrySet()) {
            System.out.println(entry.getKey() + " : " + entry.getValue());
        }
        
        System.out.println("=== Utilisation de iterator() ===");
        Iterator<Map.Entry<String, Personne>> entries = map.entrySet().iterator();
        while(entries.hasNext()) {
            Map.Entry<String, Personne> entry = entries.next();
            System.out.println(entry.getKey() + " : " + entry.getValue());
        }
        
        System.out.println("=== Utilisation de stream() ===");
        map.forEach((k,v)->System.out.println(k + "  :  " + v));
    }
}

Collections, streams et lambda expressions

Une des motivations de l'introduction des lambdas expressions est de permettre d'utiliser des collections comme des touts, sans avoir à itérer.
Dans cette approche, on se concentre sur quoi faire plutôt que comment le faire.
Cette nouvelle API permet d'appliquer à des collections entières des méthodes comme filter(), map(), reduce() etc.

Collection stream API

Pour arriver à ça, les designers de java ont dû résoudre un problème : de nombreuses libs implémentant les interfaces définies par l'API collections ont été développées hors de leur contrôle, notamment des libs permettant de simuler cette nouvelle manière de travailler.
Le risque de collision de nom était fort donc une nouvelle classe a été introduite :
java.util.stream.Stream<T>

D'après Stream javadoc : A sequence of elements supporting sequential and parallel aggregate operations. L'idée est d'appliquer une suite d'opérations (un pipeline), appliquées au Stream.
Chaque opération est habituellement exprimée par une lambda expression.
A la fin du pipeline, les résultats doivent être rassemblés dans un nouveau stream.
Cela est effectué soit en utilisant un Collector, soit en terminant le pipeline par une "opération terminale" (comme reduce()), qui renvoie une valeur plutôt qu'un stream.
        stream()  filter()    map()   collect()
Collection -> Stream -> Stream -> Stream -> Collection
On distingue deux types d'opérations :
Intermédiaires, comme filter() ou map(), qui renvoient un nouveau stream.
Terminales, comme sum().
Un pipeline est constitué d'une source, de 0 ou plus opérations intermédiaires, et d'une opération terminale

Caractéristiques d'un Stream

Création d'un Stream

On peut créer un stream à partir de différentes sources, entre autre :

filter()

Idiome appliquant du code (qui renvoie un booléen) à chaque élément de la collection, et fabrique une nouvelle collection à partir des éléments passant le test.
String[] input = {"tiger", "cat", "TIGER", "Tiger", "leopard"};
String search = "tiger";
String tigers = Arrays.stream(input)
    .filter(s -> s.equalsIgnoreCase(search))
    .collect(Collectors.joining(", "));
System.out.println(tigers);
(code dans Filter1.java)
Remarquer que dans cet exemple, on a créé le stream à partir d'un tableau.

filter() prend en paramètre une instance de l'interface java.util.function.Predicate, interface fonctionnelle dont la méthode fonctionnelle est :
boolean test(T t)
Predicate contient d'autres méthodes permettant de combiner des prédicats.
Exemple : si on veut aussi accepter des léopards :
String[] input = {"tiger", "cat", "TIGER", "Tiger", "leopard"};
List cats = Arrays.asList(input);
String search = "tiger";
Predicate<String> p = s -> s.equalsIgnoreCase(search);
Predicate<String> combined = p.or(s -> s.equals("leopard"));
String pride = cats
    .stream()
    .filter(combined)
    .collect(Collectors.joining(", "));
System.out.println(pride);
(code dans Filter2.java)
Remarquer que dans cet exemple, on a converti le tableau en List, puis créé le stream à partir de la List.

map()

Idiome permettant de transformer une collection en une collection d'un type potentiellement différent.
map() prend en paramètre une java.util.function.Function<T, R>, interface fonctionnelle qui représente une fonction, dont la méthode fonctionnelle est :
R apply(T t)
T représente le type en entrée, R représente le type renvoyé.

Exemple :
String[] input = {"tiger", "cat", "TIGER", "Tiger", "leopard"};
List cats = Arrays.asList(input);        
List namesLength = cats
    .stream()
    .map(String::length)
    .collect(Collectors.toList());
System.out.println(namesLength);
(code dans Map1.java)

Rappel : String::length est équivalent à s -> s.length(); ("bound method reference").

forEach()

Idiome permettant de modifier une collection.
forEach() prend en paramètre un java.util.function.Consumer<T>, interface fonctionnelle dont la méthode fonctionnelle est :
void accept(T t)
String[] input = {"tiger", "cat", "TIGER", "Tiger", "leopard"};
List cats = Arrays.asList(input);        
cats.forEach(System.out::println);
forEach() permet de modifier la collection sous-jacente (action par effet de bord, ce qui est considéré comme "mal" dans les langages purement fonctionnels).
Exercice : MapTest
Exercice : Intersection

reduce()

Permet d'effectuer des opérations d'aggrégations.
T reduce(T identity,
         BinaryOperator<T> accumulator)
- identity est la valeur initiale du stream
- accumulator est une lambda à 2 paramètres = interface fonctionnelle dont la méthode fonctionnelle est :
R apply(T t, U u)
aggregator fabrique un total en parcourant le stream :
Il part de la valeur initiale (identity), la combine avec la première valeur du stream, fabrique un résultat, combine ce résultat avec la 2e valeur du stream etc.
T result = identity;
for (T element : this stream)
    result = accumulator.apply(result, element)
return result;
Exemple :
int sommePremiers = Stream.of(2, 3, 5, 7, 11, 13, 17, 19, 23)
    .reduce(0, (x, y) -> {return x + y;});
System.out.println("Somme : " + sommePremiers);
(code dans Reduce1.java)

Generators, lazy evaluation, infinite streams

cf "Java in a nutshell", 6th edition, chap 7, p 222.

D'autres langages fournissent aussi ces possibilités : PHP, Python.