Java 8: Collections, Stream et opérations IO par l’exemple. Déboguer les streams

 

Ce billet aborde par la pratique le nouveau design pattern de gestion des collections en java 8 : Stream.

En java 8, le design iterator est abandonné au profit d'une meilleure conception basée sur le Stream.

Nous pouvons dire brièvement, qu'en java 8, la programmation impérative est remplacée par la programmation déclarative (penser au langage  SQL).

Les exemples démos choisis sont des opérations sur les répertoires et fichiers avec des assertions sur le nombre de lignes et sur les contenus de ces fichiers.

Nous donnons aussi une manière de déboguer les streams.

Voici les ingrédients utilisés dans ce blog: Java 8, Stream, l'api AsssertJ pour le test Junit.

Question: C'est quoi alors ce concept Stream?

Un stream est construit à partir d’une collection et transforme les données au gré des opérations définies.

Voici ce qui caractérise le Stream :

  • Un stream ne modifie pas les données. Si besoin de modification, un nouveau stream est créé,

  • Un stream se charge de manière lazy (voir plus loin),

  • Un stream n'est pas limité (non borné!),

  • Un stream non réutilisable (est parcouru qu'une seule fois).

  • Un stream se contente d'enchaîner les opérations (nul stockage n'est nécessaire).

A distinguer deux types d’opérations sur un stream : intermédiaires et terminales.

Seules les opérations terminales (finales) déclenchent réellement les traitements optimisées en fonction de leurs natures.

Un pré-requis pour le projet démo est d'ajouter cette dépendance maven au projet:

<!--    ASSERTJ -->	
	
<dependency>


<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
 <version>3.6.2</version>
 <scope>test</scope>
</dependency>	

L'api Apache common est utilisée aussi pour toute la suite.

Exemple simple sur le Stream

 

Juste pour bien démarrer un petit exemple Junit sur les streams :

 

import java.io.File;
import static java.util.Arrays.*;
import java.util.List;
import static org.junit.Assert.*;
import org.junit.Test;
@Test public void testStreamSimple(){
    File directory = new File("/tmp");
    List<File> files = asList(directory.listFiles());		
	files.stream().filter( 
           file -> file.getPath().contains("tst"))
                         .forEach(System.out::println);
	}

 

 

Pensez à adapter ce test Junit en passant le bon répertoire au lieu de "/tmp". Vérifier que ce répertoire contient un fichier contenant "tst".

L'opération finale ici est forEach. L'opération intermédiaire est filter. Cette dernière ne déclenche pas les opérations.

Ainsi par exemple le code (de la ligne ci-dessous) va compiler mais rien ne se passera en exec:

files.stream().filter( file -> file.getPath().startsWith("tst"))

Ensuite, le filter, via la lambda, ne garde que les fichiers du répertoire commençant ou contenant la chaine "tst".
A vous de l'adapter à votre configuration.

 

Question: Peut-on déboguer les éléments parcourus dans des Streams?

 

C'est peut-être vrai que le stream ressemble un peu à une boite noire où plein d’enchaînements sont effectués mais à un moment ou un autre on a l'envie d'y voir un peu ce qui se passe à chaque étape (surtout au début 🙂 ).

Pour cela nous rajoutons la méthode peek comme suit:

@Test public void testStreamSimple2(){
 File directory = new File("/tmp");
 List<File> files = asList(directory
                           .listFiles());
	  
   files.stream()
         .peek(System.out::println)
         .filter( file -> file.getPath().contains("tst"))
        .forEach(System.out::println);

}
peek to debug

 

La seule chose ajoutée ici est l'opération intermédiaire peek avec System.out::println pour voir s'afficher sur la console les éléments du (dernier) stream, c'est à dire ici le contenu du répertoire.

 

Exemple avancé sur le Stream (Junit & l'api AssertJ)

 

Maintenant que nous sommes armés pour affronter les streams, allons un peu plus loin avec ce second exemple traitant toujours les IO (code non optimisé mais ce n'est pas le sujet!) :

 

@Test
public void testFilter() throws Exception {
		File dir=new File(REP_TST);
		List<File> listFiles = asList(
                 dir.listFiles(
                   f-> f.getPath().contains("tst")
                ));
		listFiles.stream()
		 .filter( 
                f->f.getPath().contains(NAME_TST)  
                ).forEach(
                  f-> {
            assertEquals(NAME_TST,f.getName()) ; 			
	    assertThat(f).hasExtension("txt");
	    assertThat( linesOf(f).size() )  
                   .isEqualByComparingTo(0);
       } 
);
}

Là je reconnais il y a bien trop de code et les lambdas ne rendent pas forcément le code très lisible (eh oui tout a un prix!).

 

Expliquons un peu:

Les deux constantes utilisées dans le code sont:

static final String REP_TST = "/tmp";

static final String NAME_TST = "tstJunit.txt";
	

A vous de les adapter selon votre contexte.

Ensuite, pensez à rajouter ces imports:

import static org.assertj.core.api.Assertions.*;
import static org.junit.Assert.*;
import static java.util.Arrays.*;
import java.io.File;
import java.util.List;
import org.junit.Test;

La ligne 5, on passe à la méthode listFiles de java.io.File, une fonction lambda permettant de filtrer les fichiers du répertoire.

La fonction lambda fournit ici le FileFilter qui est une interface fonctionnelle ayant donc une seule méthode (SAM):

boolean java.io.FileFilter.accept(File pathname)

Remarquez que cette opération aurait pu être faite durant l'opération intermédiaire sur le stream nommée filter comme illustrée juste après (ligne 9).

 

A suivre...

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *