1 Introduction à Java

Dans ce chapitre, nous verrons comment écrire, compiler et exécuter un programme Java. Nous apprendrons à récupérer les arguments (String[] args) à la ligne de commande et afficher un résultats sur la sortie standard (System.out). Les notions porterons sur l'écriture de petits algorithmes en style procédural puis orientés objets. Nous utiliserons les structures de contrôle fréquentes (if, for) ainsi que les types (int, String, etc.) et outils communs (ArrayList, StringBuilder) de l'API Java standard.

Nous survolerons :

1.1 Premier programme

Créez un dossier hello-world. Dans ce dossier éditez un fichier Main.java :

class Main {
  void main() {
  }
}

Pour compiler le programme, il faut installer le JDK.

Dans un terminal, tapez :

javac -version

Si vous avez une erreur, il faut finaliser l'installation.

Pour lancer un programme, il faut que celui-ci soit référencé dans le PATH. Il s'agit d'une variable d'environnement. Elle est définie sur Windows et Linux. Cette variable liste un ensemble de chemins séparés par des points-virgules. Par exemple :

C:\WINDOWS\system32;C:\WINDOWS

Il faut faire en sorte que cela ressemble à :

C:\WINDOWS\system32;C:\WINDOWS;C:\Program Files\Java\jdk1.8.0\bin

https://www.java.com/fr/download/help/path.xml.

Examinez le PATH :

echo %PATH%

Contient-il le chemin d'installation du compilateur ? Il est typiquement installé dans le répertoire : C:\Program Files\Java\jdk1.8.0\bin (ou plus généralement dans %ProgramFiles%\Java\jdk1.8.0\bin).

Allez dans les variables d'environnements de l'utilisateur. Si PATH n'existe pas ajoutez une variable nommée PATH valant C:\Program Files\Java\jdk1.8.0\bin. Si elle existe, modifiez la valeur pour ressembler au PATH attendu (cf. plus haut).

https://docs.oracle.com/javase/8/docs/technotes/guides/install/windows_jdk_install.html

javac -version

Si l'installation est bien finalisée vous devriez voir s'afficher : javac 1.8.0_121.

Naviguez dans le dossier de votre code source :

cd hello-world

Compilez le programme :

javac Main.java

Le compilateur a créé un nouveau fichier : Main.class.

Lancez le programme :

java Main

Le message d'erreur est le suivant :

Erreur : la méthode principale est introuvable dans la classe Main,
définissez la méthode principale comme suit :
   public static void main(String[] args)
ou une classe d'applications JavaFX doit étendre javafx.application.Application

Corrigez le programme Main.java. Relancez la compilation (javac Main.java). Relancez l'exécution (java Main).

Vous venez de créer votre premier programme Java ! Mais il ne fait pas grand chose.

Ajoutez une instruction à la méthode main() :

class Main {
  public static void main(String[] args) {
    System.out.println("Hello world!");
  }
}

Compilez avec javac, lancez avec java.

Vous devriez voir le message suivant s'afficher :

Hello world!

La classe la plus petite que l'on peut compiler (commande : javac C.java) est :

class C {}

Le programme le plus petit que l'on peut exécuter (commande java P) est :

class P {
    public static void main(String[] args) {}
}

1.2 Types et méthodes basiques

1.2.1 Type int

De combien de personne a augmenté la population française entre 1905 et 2005 ?

class Population {
    public static void main(String[] args) {
        int population1905 = 41_050_000;
        int population2005 = 61_182_000;

        System.out.println(population2005 - population1905); // 20132000
    }
}

La population a donc augmenté de 2 013 2000 personnes.

1.2.2 Type double et printf

Quel est l'accroissement relatif de la population entre 1905 et 2005 ? L'accroissement relatif entre A et B est B/A - 1.

class BugAccroissementPopulation {
    public static void main(String[] args) {
        int population1905 = 41_050_000;
        int population2005 = 61_182_000;

        System.out.println(population2005 / population1905 - 1); // 0
    }
}

Il y a un bug. L'expression population2005 / population1905 - 1 est évaluée à 0. Pour comprendre l'erreur, il faut comprendre l'ordre d'évaluation des opérations ainsi que la sémantique de la division sur des int.

Dans l'expression population2005 / population1905 - 1, la division est prioritaire. Ainsi, ou bien population2005 / population1905 a un problème ou bien x - 1 a un problème (avec int x = population2005 / population1905).

Évaluons donc avec un debugger, l'expression population2005 / population1905. Le résultat est 1 !

En fait la division pour les int est une division entière. La solution est d'effectuer une division flottante :

class AccroissementPopulation {
    public static void main(String[] args) {
        double population1905 = 41_050_000;
        double population2005 = 61_182_000;

        System.out.println(population2005 / population1905 - 1); // 0.4904263093788064
    }
}

Afficher l'accroissement en pourcentage sans virgule.

class AccroissementPopulation {
    public static void main(String[] args) {
        double population1905 = 41_050_000;
        double population2005 = 61_182_000;

        double accroissement = population2005 / population1905 - 1; // 0.4904263093788064

        int pourcentage = (int) (accroissement * 100);

        System.out.println(pourcentage + "%"); // 49%
    }
}

En java, le type int est représenté sur 32 bits. Le nombre d'entiers représentables est 2^32 - 1. Calculons ce nombre :

class MaxInteger {
    public static void main(String[] args) {
        System.out.println(Math.pow(2, 32) - 1); // 4.294967295E9
    }
}

La méthode statique Math.pow(double a, double b) retourne la valeur du premier argument élevé à la puissance du deuxième argument.

Affichons le résultat formaté selon la localisation par défaut.

class PrintfMaxInteger {
    public static void main(String[] args) {
        System.out.printf("%f%n", Math.pow(2, 32) - 1); // 4294967295,000000
    }
}

On peut donc représenter 4 294 967 295 entiers avec le type int en Java.

La chaîne "%f%n" spécifie le format d'affichage. La conversion %f formate un double et %n affiche un retour à la ligne.

La syntaxe générale d'un format est :

%[argument_index$][flags][width][.precision]conversion

https://docs.oracle.com/en/java/javase/12/docs/api/java.base/java/util/Formatter.html#syntax

1.2.3 Tableau et boucle for

Faire la somme des pays de l'union européenne en 2016.

class PopulationTotaleEurope {
    public static void main(String[] args) {
        int[] populationsPays = new int[]{82_162_000, 66_661_621, 65_341_183, 60_665_551, 46_438_422, 37_967_209, 19_760_314,
                16_979_120, 11_289_853, 10_793_526, 10_553_853, 10_341_330, 9_851_017, 9_830_485, 8_700_471, 7_153_784,
                5_659_715, 5_707_251, 5_426_252, 4_658_530, 4_190_669, 2_888_558, 2_064_188, 1_968_957, 1_315_944,
                848_319, 576_249, 434_403, 510_056_011, 78_741_053, 8_325_194, 7_076_372, 5_165_802, 3_830_911,
                3_553_056, 2_886_026, 2_071_278, 1_771_604, 622_218, 332_529};

        int totalPopulation = 0;
        for (int i = 0; i < populationsPays.length; i++) {
            totalPopulation += populationsPays[i];
        }

        System.out.println(totalPopulation); // 1134660828
    }
}

Voici une autre syntaxe pour les tableaux dont le type des éléments est inférable.

int[] populationsPays = {82_162_000, 66_661_621, 65_341_183};

On peut aussi simplifier la notation de la boucle for car l'indice i ne sert qu'à accéder à un élément du tableau :

int totalPopulation = 0;
for (int population : populationsPays) {
    totalPopulation += population;
}

1.2.4 Type String

Afficher un message qui affiche "Welcome John!" quand l'argument à la ligne de commande est "John".

class Welcome {
    public static void main(String[] args) {
        String name = args[0];
        System.out.println("Welcome " + name + "!");
    }
}

Dans un terminal :

$ java Welcome John
Welcome John!

1.2.5 If

Contrairement au langage C ou à JavaScript, en Java l'exécution conditionnelle if n'accepte qu'une condition de type booléenne :

Ainsi l'instruction suivante est valide avec args de type String[] :

if (args[0] == null) {} // OK

Mais celle-ci ne l'est pas :

if (args[0]) {} // incompatible types: java.lang.String cannot be converted to boolean

1.3 Notation binaire : exercice

Créez un dossier binary-format. Créez-y un fichier Main.java.

A chaque étape, vous devez effectuer tout le cycle d'édition, de compilation et d'exécution. N'attendez pas l'étape 3 avant de tester votre programme.

  1. Ecrivez un programme qui affiche "0". Utilisez System.out.println(String).
  2. Modifiez ce programme pour qu'il affiche "1" si l'argument au programme est la chaîne de caractères "1". Utilisez l'opérateur d'égalité == puis la méthode String.equals(String). Pour lancer un programme Java avec un argument (ici 1) exécutez : java Main 1.
  3. Modifiez ce programme pour qu'il affiche 0 si le nombre passé en paramètre est pair et 1 sinon. Utilisez l'opérateur modulo %. Essayez aussi d'utiliser l'opérateur de conjonction binaire &.
  4. Modifiez ce programme pour préfixer (quand le nombre est supérieur ou égal à 2) 0 si la moitié du nombre passé en paramètre est pair, 1 sinon. Utilisez l'opérateur de concaténation de String +. On aura donc :
$ java Main 1
1
$ java Main 0
0
$ java Main 2
10
$ java Main 3
11
$ java Main 4
00
  1. Modifiez ce programme pour que l'argument passé en argument soit entré en notation binaire plutôt qu'en format décimal. Ne pas afficher l'argument tel quel. Utilisez Integer.parseInt(int, int).
$ java Main 11
11
$ java Main 110
10
  1. Modifiez ce programme pour afficher la décomposition binaire pour n'importe quel nombre en utilisant la notation Java 7. Utilisez l'opérateur d'affectation combiné à la concaténation +=.
$ java Main 1111
0B1111
$ java Main 10000000
0B10000000
  1. Remplacez l'opérateur d'affectation-concaténation += par un StringBuilder.append(String).

1.3.1 Correction

package fr.arolla.java8esgi.binarynotation;

public class Main {
    public static void main(String[] args) {
        if (args.length == 0) {
            System.err.println("erreur");
            System.exit(1);
        }

        String arg = args[0];
        int n = Integer.parseInt(arg);
        int i = 0;
        String[] bits = new String[4];
        while (n >= 0) {
            if (n % 2 == 0) {
                bits[i] = "0";
            } else {
                bits[i] = "1";
            }
            i++;
            n = n / 2;
            if (n == 0) {
                break;
            }
        }
        for (int j = i - 1; j >= 0; j--) {
            System.out.print(bits[j]);
        }
        System.out.println();
    }
}

Pour éviter d'afficher (print) plusieurs fois chaque bit, on peut accumuler dans une chaîne de caractère ce qu'il y a à afficher. On pourra ainsi n'effectuer qu'une seule entrée/sortie (I/O).

String binary = "";
for (int j = i - 1; j >= 0; j--) {
    String bit = bits[j];
    binary += bit;
}
System.out.println(binary);

L'opérateur de concaténation-affectation binary += bit; est équivalent à binary = binary + bit;.

Notez que la comparaison de chaîne de caractère avec l'opérateur == ne donne pas de bons résultats. Parfois le code suivant affiche OK.

String argument = String.valueOf(1);
if (argument == "1") {
    System.out.println("OK");
}

L'égalité des chaînes de caractères est basée sur la comparaison des valeurs et non pas des références.

String = String.valueOf(1);
if (argument.equals("1")) {
    System.out.println("OK");
}

L'opérateur qui calcule le reste de la division entière est noté : %. Un invariant important de la division Euclidienne peut être exprimé ainsi en Java :

d * (x / d) + (x % d) == x

Attention cependant, même si l'on ne fait pas explicitement une division, calculer le reste d'une division par zéro est interdit :

int r = 2 % 0;
// java.lang.ArithmeticException: / by zero

On a également utilisé l'opérateur & conjonction binaire. Cet opérateur réalise un "et" binaire sur chaque bit des deux opérandes.

int r1 = 9 % 2; // 1
int r2 = 9 & 1; // 1

1.4 Java 8 API

La bibliothèque standard de programmation de Java est riche.

Nous avons vu jusqu'ici les méthodes suivantes :

Et des classes incontournables de java.lang :

Prenez l'habitude de lire la documentation de référence.

1.5 Unité de compilation

Le code source d'une classe est stockée dans un fichier. Le fichier constitue l'unité de compilation.

Bien qu'il soit légal de nommer une classe package-private (c-à-d. non public) différemment du fichier, ce n'est pas recommandé.

Par exemple :

// fichier "A.java"
class B {} // non standard mais compilera

La convention est d'avoir une classe par fichier (relation un-pour-un).

// fichier "Foo.java"
class Foo {}

// fichier "Bar.java"
class Bar {}

Éviter :

// fichier "nom_de_classe.java"
class NomDeClasse {} // nom différent

En revanche, dès que l'on que la classe définie devient publique, le fichier doit correspondre à la classe :

// fichier "Foo.java"
public class BarBar {}
$ javac Foo.java
Foo.java:1: error: class BarBar is public, should be declared in a file named BarBar.java
public class Bar {}
       ^
1 error

Enfin il est possible de définir plusieurs classes par fichier. Une seule d'entre elles peut-être publique.

// fichier "Main.java"
public class Main {}
class Foo {}
class Bar {}

Mais l'exemple suivant ne compile pas car il y a 2 classes publiques :

// fichier "Main.java"
public class Main {}
public class Foo {}
class Bar {}

1.6 Logarithme et exponentiation : exercice

Dans l'algorithme précédent, nous avons fixé la taille du tableau en dur.

String[] bits = new String[4];

Si on cherche à calculer la représentation binaire de 16 (0B1_0000).

Le programme crash :

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 4
    at fr.arolla.java8esgi.binaryformat.Main.main(Main.java:18)

On a dépassé les bornes du tableau. On va donc chercher à calculer la taille minimale du tableau nécessaire pour stocker la représentation binaire d'un nombre entier.

Pour trouver la formule qui nous donnerait la taille en fonction du nombre que l'on cherche à représenter en binaire, on peut noter que :

m = 2s − 1

avec m le nombre maximum que l'on peut représenter avec un tableau de taille s.

En passant cette équation au logarithme et en ajustant un peu, on peut noter que :

s = ln2(n)+1

avec n le nombre que l'on chercher à représenter et s (pour "size") le nombre de bits nécessaires.

Pourtant, en Java il n'existe pas de fonction standard pour ln2.

Il faudra donc l'implémenter en notant la relation suivante :

ln2(x)=ln(x)/ln(2)

1.7 Logarithme base 2 en Java

On définit une méthode log2, dans notre classe Main :

public class Main {
    public static void main(String[] args) {
        // ...
    }
    
    private static int log2(int n) {
        double ln = Math.log(n);
        double ln2 = Math.log(2);

        return (int) (ln / ln2);
    }
}

On peut ainsi modifier notre méthode main de la façon suivante :

String[] bits = new String[log2(n) + 1];

1.8 Visibilité : un tour d'horizon

Faisons une métaphore temporaire.

Une classe contient des méthodes. Un package contient des classes. Petite généralisation : les classes et les packages sont des boîtes ; les méthodes et classes sont des éléments. On peut donc dire qu'une boîte contient des éléments.

Un élément d'une boîte peut être public. Cela signifie qu'il sera visible par une autre boîte. Si un élément d'une boîte est privé, une autre boîte n'y aura pas accès.

    A                B
|-------|        |-------|
|  +X<--|--------|       |
|-------|        |-------|

    C                D
|-------|        |-------|
|  -Y   |        |       |
|-------|        |-------|

Revenons maintenant à Java.

Il y a 4 modificateurs de visibilité (pour seulement 3 mots-clés) :

Ces modificateurs s'appliquent pour les déclarations contenues par une classe (c'est à dire les champs, méthodes et classes imbriquées).

Par exemple, on peut appliquer les 4 modificateurs au champ a, a la méthode getA() ainsi qu'à la classe Bar.

class Foo {
    private int a;
    public int getA() { return a; }
    private class Bar {}
}

Pour les déclarations de plus haut niveau (c'est à dire défini directement dans un package et non pas à l'intérieur d'une classe) il existe seulement deux niveaux de visibilité :

Les classes Foo et Bar exemples de déclarations de haut niveau mais Nested n'en n'est pas une :

package a;

public class Foo {
}

class Bar {
    class Nested {}
}

1.9 Visibilité dans le contexte de l'encapsulation

Pour simplifier, un symbole (par exemple un champ ou méthode) déclarée dans une classe peut-être ou bien public ou bien private.

Un champ ou méthode déclaré privé n'est visible qu'à l'intérieur de la classe. Les méthodes qui ne sont appelées que par d'autres classes devraient être privées.

Un champ ou méthode déclaré publique est tout le temps visible, pour peu que la classe qui la défini soit visible.

class Constant {
    private int literal;
    public void getValue() {
        return literal;
    }
}

Ainsi, une autre classe aura accès à getValue() car cette méthode est public mais pas à literal car ce champ est privé.

Donc le code suivant ne compilera pas :

class Evaluator {
    void evaluateConstant(Constant c) {
        int literal = c.literal; // erreur
        int value = c.getValue(); // OK
    }
}

Du point de vue de la classe Evaluator tout se passe comme si la classe Constant était définie comme suit :

class Constant {
    // c'est à dire sans la déclaration : int literal 
    
    public void getValue() {
        return literal;
    }
}

1.10 Visibilité dans un contexte de packages multiples

Pour accéder à une classe à partir d'un autre package, la visibilité doit être au minimum package-protected.

Syntaxe : un champ ou méthode sans modificateur de visibilité est dit package-protected. Il n'est visible que dans le même package.

package a;

public class Public {
}

class Private { // visible uniquement dans le paquetage
}

package b;

import a.Public;

class Bravo {
    { 
        Public ok; // OK
        Private nope; // erreur
    }
}

1.11 Visibilité dans le contexte de l'héritage

Pour permettre à un symbole d'être vu en dehors du package sans pour autant être public, on peut ouvrir l'accès aux sous-classe à l'aide du modificateur protected.

Par exemple,

package alpha;

public class Base {
    protected int x;
}

class Beta {
    void f(Base b) {
        int value = b.x; // OK car Beta dans le même package que Base
    }
}

package beta;

import alpha.Base;

class Derived extends Base {
    void foo() {
        int value = x; // OK car Derived est une sous-classe de Base
    }
}

class Sibling {
    void bar(Base b) {
        int value = b.x; // erreur
    }
}

1.12 Visibilité : résumé

visibilité champ ou méthode classe
private non visible par une autre classe -
package non visible par un autre package idem
protected visible dans le package et les sous-classes -
public visible partout visible partout

Pour les membres d'une classe :

visibilité sites autorisés
public tout
protected la classe, le package et les dérivées
package-protected la classe et le package
private la classe seulement

Pour les classes d'un package :

visibilité sites autorisés
public tout
package-protected le package

1.13 Conversion

Java autorise la conversion implicite de type dès lors qu'il n'y a pas de perte d'information.

int salary = 35_000;
double amount = salary;

En revanche, il interdit cette conversion avec perte :

double amount = 2_000;
int salary = amount; // incompatible types

Pour forcer la conversion, il faut indiquer au compilateur javac que l'on sait ce que l'on fait en castant une expression dans un type plus spécifique.

double amount = 2_000;
int salary = (int) amount;

1.14 Tableau

Le type String[] est un tableau de String. Une variable de type tableau référence un objet. Une fois l’objet créé, sa longueur ne change jamais. C'est pour cela que l'on devait calculer sa taille en amont avec log2(int).

Les indices de tableaux commencent à zéro.

String[] strings = new String[2];

String premier = strings[0];
String dernier = strings[strings.length - 1];

1.15 List et ArrayList

L'utilisation de tableaux est fastidieuse et facilite l'introduction de bug. Pour éviter cela on peut utiliser une structure de données plus sophistiquée : ArrayList. C'est une classe qui définit un tableau dynamique (c.à.d. peut changer de taille).

On pourra donc écrire :

List<String> bits = new ArrayList<>();
bits.add("0");
bits.add("1");
Collections.reverse(bits);
for (String bit : bits) {
    System.out.print(bit);
}

Précision : le type (connu à la compilation) de bits est List<String>. La syntaxe List<String>, List<Integer>, Set<Object>, etc. désigne un type paramétré par un autre type.

L'instruction :

List<String> bits = new ArrayList<>();

est équivalente à :

List<String> bits = new ArrayList<String>();

Nous verrons cela en détail dans un autre cours.

1.16 StringBuilder

Concaténer avec + dans une boucle peut-être très long (ici, t < 1s) et consommer beaucoup de mémoire.

String output = "";
for (int i = 0; i < 10_000; i++) {
    int n = (int) (Math.random() * Integer.MAX_VALUE);
    String s = String.valueOf(n);

    output += s;
}
System.out.println(output);

On préférera donc l'utilisation de StringBuilder qui ne créé par une nouvelle chaîne de caractère à chaque itération contrairement à l'opérateur +.

Préférer StringBuilder pour éviter de très long temps de traitements (ici, t < 50ms) :

StringBuilder output = new StringBuilder();
for (int i = 0; i < 10_000; i++) {
    int n = (int) (Math.random() * Integer.MAX_VALUE);
    String s = String.valueOf(n);

    output.append(s);
}
System.out.println(output);

On pourra astucieusement améliorer notre implémentation d'affichage du nombre binaire :

StringBuilder buffer = new StringBuilder();
for (int j = i - 1; j >= 0; j--) {
    String bit = bits[j];
    buffer.append(bit);
}
System.out.println(buffer);

1.17 Premier objet

1.17.1 Plus grand commun diviseur : exercice

Le PGCD de 2 nombres a et b est le plus grand entier tel que PGCD divise a et b. (en anglais, on note GCD pour "Greatest Common Divisor").

Propriété : GCD(a, b)=GCD(b, r) avec r le reste de la division de a par b.

De cette propriété, nous en tirons directement l'algorithme :

pgcd de a et b vaut :
- a, si b égale zéro
- pgcd de b et du reste de la division de a par b, sinon

L'exercice consiste à implémenter cet algorithme pour calculer le PGCD de 16 et 28.

$ java Gcd 16 28
4

1.17.2 Méthode récursive et statique

Implémenter l'algorithme avec le prototype suivant :

static int computeGcd(int a, int b) {
    // TODO
}

1.17.3 Objet

Modifier le programme de sorte qu'il instancie un objet de classe Gcd :

package fr.arolla.java8esgi.gcd;

public class Gcd {
    public static void main(String[] args) {
        if (args.length != 2) {
            System.err.println("2 arguments attendus : <a> <b>");
            System.exit(1);
        }
        Gcd gcd = new Gcd(Integer.parseInt(args[0]), Integer.parseInt(args[1]));
        int divisor = gcd.computeGcd();

        System.out.println(divisor);
    }
    // ...
}

Cette surcharge de la méthode computeGcd(int, int) ne définit pas de paramètre et n'est pas statique.

int computeGcd();

1.17.4 Champ de classe

Pour cela, on peut définir et initialiser 2 champs de classe :

class Gcd {
    // ...
    int a = 16;
    int b = 28;
}

On pourra aussi simplement déclarer les champs (sans les initialiser) :

class Gcd {
    // ...
    int a;
    int b;
}

1.17.5 Constructeur par défaut

On peut invoquer un constructeur sans paramètre sans le définir explicitement :

public class Gcd {
    public static void main(String[] args) {
        Gcd calculator = new Gcd();
    }
    
    // 0 constructeur => constructeur par défaut
}

1.17.6 Constructeur sans paramètre

public class Gcd {
    Gcd() {
        a = 16;
        b = 28;
    }
}

1.17.7 Constructeur avec 2 paramètres

Gcd(int a, int b) {
    this.a = a;
    this.b = b;
}

En définissant un constructeur, on perd le constructeur par défaut. Si on veut pouvoir instancier la classe sans argument, il faut définir explicitement un constructeur sans paramètre.

1.17.8 Référence interne

La variable this référence l'objet receveur ou objet courant.

1.17.9 Premier objet : correction

package fr.arolla.java8esgi.gcd;

public class Gcd {
    public static void main(String[] args) {
        if (args.length != 2) {
            System.err.println("2 arguments attendus : <a> <b>");
            System.exit(1);
        }
        Gcd gcd = new Gcd(Integer.parseInt(args[0]), Integer.parseInt(args[1]));
        int divisor = gcd.computeGcd();

        System.out.println(divisor);
    }
    
    private int a;
    private int b;
    
    public Gcd(int a, int b) {
        this.a = a;
        this.b = b;
    }
    
    public int computeGcd() {
        return computeGcd(a, b);
    }
    
    private static int computeGcd(int a, int b) {
        if (b == 0) {
            return a;
        }
        return computeGcd(b, a % b);
    }
}

Comment être raisonnablement sûr que l'algorithme computeGcd est bien implémenté ?

On peut intégrer une post-condition de la méthode à vérifier. On peut transposer un raisonnement par l'absurde en Java :

La méthode computeGcd() calcule le plus grand nombre qui divise à la fois a et b. Donc si on trouve un nombre plus grand que le résultat retourné qui divise a et b alors c'est que ce résultat est faux.

On crée une méthode findGreaterDivisor() qui cherche en brute-force un diviseur commun (pas forcément le plus grand - noter le "greater" et pas "greatest") et on l'intègre dans une instruction assert.

Le assert (et son contenu) ne sera exécuté que si l'option -ea (ou -enableassertions) est spécifiée au programme java.

public static void main(String[] args) {
    if (args.length != 2) {
        System.err.println("2 arguments attendus : <a> <b>");
        System.exit(1);
    }
    int a = Integer.parseInt(args[0]);
    int b = Integer.parseInt(args[1]);
    Gcd gcd = new Gcd(a, b);
    int divisor = gcd.computeGcd();

    assert divisor == findGreaterDivisor(divisor, a, b) : 
        String.format("Diviseur trouvé (%d) plus grand que le PGCD (%d)", 
            findGreaterDivisor(divisor, a, b), divisor);
    System.out.println(divisor);
}

private static int findGreaterDivisor(int divisor, int a, int b) {
    for (int potentialDivisor = divisor + 1;
            potentialDivisor <= Math.max(a, b) - 1;
            potentialDivisor++) {
        if (a % potentialDivisor == 0 && b % potentialDivisor == 0) {
            return potentialDivisor;
        }
    }
    return divisor;
}

1.18 Priorité des opérateurs

Dans l'expression a % potentialDivisor == 0 && b % potentialDivisor == 0, comment sont combinés les opérandes et opérateurs ?

Simplifions :

a % potentialDivisor == 0

% et == sont des opérateurs binaires. On peut donc se poser la question suivante : doit-on évaluer le % ou bien == en premier ?

La réponse est simple : l'opérateur % est plus prioritaire que ==.

Ainsi,

a % potentialDivisor == 0

est équivalent à faire le modulo en premier (signifié par les parenthèses) :

(a % potentialDivisor) == 0

Reprenons l'expression complète : a % potentialDivisor == 0 && b % potentialDivisor == 0. Maintenant, il faut savoir classer par priorité non seulement % et == mais aussi &&.

Voici le classement de ces trois opérateurs :

  1. %
  2. ==
  3. &&

Donc on groupera en premier les % (en met des parenthèses autour) :

(a % potentialDivisor) == 0 && (b % potentialDivisor) == 0

Puis on groupera autour des == :

((a % potentialDivisor) == 0) && ((b % potentialDivisor) == 0)

Et enfin autour du &&, mais inutile de l'écrire ici (pas d'expression englobante).

Le groupage avec les parenthèses est équivalent. On peut écrire les deux formes. La forme avec parenthèses dans notre cas rend explicite la priorité des opérateurs. De manière général, on évite les parenthèses de "confort". Mais attention aux expressions trop compliquées !

Il arrive que deux opérateurs aient la même priorité. Par exemple :

1 + 2 - 3

Comme l'opérateur + et - ont la même priorité, on groupe les opérations de gauche à droite.

On aura donc :

(1 + 2) - 3

Voici un tableau général (mais incomplet) des priorités :

  1. expr++ expr-- (postfix)
  2. ++expr --expr +expr -expr ~ ! (unaire)
  3. * / % (multiplicatif)
  4. + - (additif)
  5. < > <= >= instanceof (relationel)
  6. == != (égalité)
  7. && (et logique)
  8. || (ou logique)
  9. ? : (ternaire)
  10. = += -= *= (affectation)

Voici quelques moyens mémotechniques pour retenir l'idée des priorités.

Comme en mathématique usuelle, la multiplication l'emporte sur l'addition. Le pendant de la multiplication côté logique est le "et logique" qui l'emporte sur le "ou logique".

Maintenant mettons nous dans la peau de James Gosling (principal) créateur de Java. Analysons ce qu'il se serait passé si les priorités de certains opérateurs étaient inversés.

Pourquoi le == est plus prioritaire que le && ? Parce que la forme a==b && c==d est plus courante que a&&b == c&&d (qu'on écrira donc (a && b) == (c && d)).

Mais pourquoi < est plus prioritaire que == ? Parce que la forme a<b == c<d est plus courante que a==b < c==d (qui n'est pas correcte du point de vue typage).

Enfin pourquoi l'opérateur ternaire est-il plus prioritaire que l'opérateur d'affectation ? Parce que la forme x = ok?1:2 est un idiome de programmation alors que (si l'affectation > ternaire) ok=true ? 1 : 2 (qu'on serait obligé de coder : (ok = true) ? 1 : 2) est à éviter bien que légal.

Il faut noter que la règle de lecture de gauche à droite (l'associativité) n'est pas valable pour les affectations. Quand il y a plusieurs affectations, on associe de droite à gauche (l'inverse du cas usuel).

Ainsi,

1 * 2 / 3 % 4

s'associe de gauche à droite :

((1 * 2) / 3) % 4

Mais pour les affectations :

x = y += z

Se lira

x = (y += z)

1.18.1 Exercice opérateurs

  1. Placer les parenthèses en fonction de la priorité.
  2. Qu'affiche l'instruction println() ?
int x;
System.out.println(x = true && 1 < 2 || 3 == 4 ? 1 : 2);

1.19 Package

Le package est une structure dans laquelle on regroupe des classes. Il définit un espace de nom ainsi qu'une zone de contrôle d'accès.

Pour déclarer qu'une classe appartient à un package, l'unité de compilation (c'est à dire le fichier) commence par l'instruction :

package nom.de.package;

Les noms de package suivent une convention : des mots en minuscule séparés par des points. Il est recommandé de nommer un package selon le sens inverse de nom de domaine qui possède le code source.

Ainsi on aura com.sun qui correspond au nom de domaine sun.com.

Pour utiliser une classe d'un autre package, on doit l'importer.

package com.example.alpha; // file Alpha.java

public class Alpha {}
package com.example.beta; // file Beta.java

class Beta {
    Alpha a; // erreur
}

L'unité de compilation qui défini Beta doit importer explicitement Alpha en utilisant son nom qualifié (nom de package + nom de classe).

package com.example.beta;

import com.example.alpha.Alpha;

class Beta {
    Alpha a; // OK
}

On regroupera les classes d'un même package dans le même dossier. On utilisera l'arborescence du système de fichier pour encoder un nom de package. Ainsi, les classes du package com.example.alpha seront placé dans le dossier com/example/alpha.

Un package est donc représenté comme une annotation d'une unité de compilation. Il n'existe pas de fichier de package.

Contrairement ce qu'on pourrait penser, le package n'est pas une structure hierarchique. Par exemple, le package java.nio n'est pas le parent du package java.nio.charset. Ainsi java.nio.charset ne bénéficie aucunement des classes de java.nio. On peut seulement dire que java.nio et java.nio.charset sont des package différents.

1.19.1 Exercice

La classe Gcd est codée dans une seule unité de compilation.

  1. Diviser la classe Gcd en une classe main() et une classe contenant computeGcd().
// file Gcd.java
class Gcd {
    public static void main(String[] args) {}
    static computeGcd(int n) {}
}
  1. Créer deux classes dans la même unité de compilation.
// file GcdMain.java
class GcdMain {
    public static void main(String[] args) {}
}

class Gcd {
    static computeGcd(int n) {}
}
  1. Placer les deux classes deux unités de compilation.
// file GcdMain.java
class GcdMain {
    public static void main(String[] args) {}
}

// file Gcd.java
class Gcd {
    static computeGcd(int n) {}
}
  1. Placer les deux unités de compilation dans deux packages distincts.
package main; // main/GcdMain.java

import gcd.Gcd;

class GcdMain {
}
package gcd; // gcd/Gcd.java

public class Gcd {
}

1.20 Nombres rationels

Dans la suite d'exercices qui suit nous allons développer un calculateur symbolique de nombres rationnels. Un nombre rationnel est défini par un ratio (ou quotient) de 2 entiers naturels. L'avantage de cette représentation est qu'elle est précise et que l'on peut inspecter le programme plus facilement. En revanche, ce modèle risque d'être moins rapide que les calculs flottant directement implémentés sur les processeurs.

On peut combiner 2 nombres rationnels en effectuant leur somme, leur différence, leur produit ou le quotient. Voici comment effectuer de telles combinaisons :

n1/d1 + n2/d2 = (n1 × d2 + n2 × d1)/(d1 × d2)

n1/d1 − n2/d2 = (n1 × d2 − n2 × d1)/(d1 × d2)

n1/d1 × n2/d2 = (n1 × n2)/(d1 × d2)

(n1/d1)/(n2/d2)=(n1 × d2)/(d1 × n2)

(n1/d1)=(n2/d2)≡(n1 × d2)=(d1 × n2)

1.20.1 Exercice 1

Ecrire un programme qui prend en arguments 4 nombres (n1, d1, n2, d2) et affiche la somme : n1/d1 + n2/d2. Exemple :

$ java Rational 1 4 2 8
16/32

Ici 1 4 se lit 1/4 et 2 8 se lit 2/8.

  1. Ecrire une méthode et une seule, le main(String[]) qui affiche la somme des 2 nombres rationels encodés sous 4 arguments.
  2. Introduire 2 champs de classe (numérateur et dénominateur) et un constructeur qui les initialise.
  3. Extraire le code qui calculait la somme dans une nouvelle méthode add qui prend en entrée 2 paramètres de type Rational et retourne un Rational.

Dans la méthode main(String[] args), après le calcul vous pouvez afficher avec :

System.out.println(sum.numerator + "/" + sum.denominator);

Puis vous pouvez extraire le code : sum.numerator + "/" + sum.denominator dans une nouvelle méthode show(). Voilà un exemple d'usage :

Rational left = ...
Rational right = ...
Rational sum = add(left, right);
System.out.println(sum.show());

Renommer show() en toString() pour avoir :

public class Rational {
    // ...
    
    @Override
    public String toString() {
        // ...
    }
}

Enfin, à l'affichage, supprimer l'appel à la méthode toString().

Rational sum = ...
System.out.println(sum);

On vient de redéfinir la méthode toString() qui est définie sur la super-classe de toutes les classes Java : Object.

  1. Tout en conservant l'annotation @Override, essayer de supprimer le public du toString().
  2. Essayer de renommer la méthode toString().
  3. Regarder l'implémentation de la méthode PrintStream.println(Object).

1.20.2 Invariant et bug

Tester le programme avec d'autres valeurs. Créer plusieurs configurations de lancement dans votre IDE pour cela (run configurations).

Trouver des invariants à l'opération +. Par exemple, 0 + 1/2 = 1/2 + 0 = 1/2. De manière générale : 0 + n/d = n/d + 0 = n/d.

Si vous cassez un invariant vous savez que vous avez un bug.

1.20.3 Exercice 2

Modifier le format d'entrée au programme. Exemple :

$ java Rational 1/4 2/8
16/32

Utiliser String.split(String regex). Voir https://docs.oracle.com/javase/8/docs/api/java/lang/String.html#split-java.lang.String-

1.20.4 Exercice 3

Modifier le format d'entrée au programme. Exemple :

$ java Rational 1/4 + 2/8
16/32

1.20.5 Exercice 4

Afficher la forme canonique du nombre rationel. Exemple :

$ java Rational 1/4 + 2/8
1/2

Indice : utiliser le PGCD pour cela.

1.20.6 Exercice 5

Traiter la différence de nombres rationels. Exemple :

$ java Rational 1/4 - 2/8
0/1

1.20.7 Exercice 6

Simplifier la sortie quand le résultat est zéro. Exemple :

$ java Rational 1/4 - 2/8
0

1.20.8 Exercice 7

Traiter les nombres négatifs. À la construction de l'objet, normaliser les signes. Afficher un moins seulement devant le numérateur uniquement si le nombre rationel est négatif. Exemple :

$ java Rational 1/3 - 2/3
-1/3

1.20.9 Exercice 8

Calculer le produit de 2 nombres rationels. Exemple :

$ java Rational 1/4 * 1/2
1/8
  1. Vérifier que le nombre d'arguments est exactement 3.
  2. Protéger l'argument * avec des guillemets simple '*' l'expansion des noms de fichiers du shell.

1.20.10 Exercice 9

Calculer le ratio de 2 nombres rationels. Exemple :

$ java Rational 2/3 / 1/3
2/1

1.20.11 Exercice 10

Simplifier la sortie quand le dénominateur est 1. Exemple :

$ java Rational 2/3 / 1/3
2/1

1.20.12 Exercice 11

Tester l'égalité de 2 nombres rationels. Exemple :

$ java Rational 1/4 == 2/8
true

1.20.13 Exercice 12

Gérer la réduction (c.à.d. la forme canonique) de la fraction au moment ou le numérateur et le dénominateur sont calculés. Observer ce qui change dans le programme. Puis gérer la réduction dans une méthode dédiée (statique puis non statique).

1.20.14 Exercice 13

Modifier les méthodes d'addition, soustraction, multiplication et division de sorte qu'elles ne prennent plus qu'un paramètre plutôt que deux. Par exemple :

public class Rational {
    Rational add(Rational b) {
        // ...
    }
}

Il faut remplacer l'ancien paramètre a par l'objet receveur de la méthode.

1.20.15 Exercice 14

Créer une classe IntegerNumber qui étend RationalNumber. Tester son comportement avec un nouveau main(String[]).

1.20.16 Exercice 15

Brancher cette classe lorsque le dénominateur est 1. Observer les changements avant de revenir en arrière.

Par exemple :

public class Rational {
    Rational sum(Rational b) {
        int sumDenuminator = ...
        if (sumDenuminator == 1) {
            return new IntegerRational(sumNumerator);
        }
        // ...
    }
}

1.20.17 Exercice 16

Introduire une méthode statique fabriquant selon que le dénominateur est 1 ou pas un RationalNumber ou un IntegerNumber.

1.20.18 Exercice 17

Redéfinir la méthode toString() de IntegerNumber pour n'afficher que le numérateur.

1.20.19 Exercice 18

En suivant le même chemin, introduire une sous-classe ZeroNumber. Quelles méthodes peuvent-être redéfinies ?

1.20.20 Correction

package fr.arolla.java8esgi.rational;

public class Rational {
    public static void main(String[] args) {
        if (args.length != 3) {
            System.err.println(String.join(" ", args));
            System.err.println("Usage: <n1>/<d1> <operator> <n2>/<d2>");
            System.err.println("Available operators: + - x / ==");
            System.exit(1);
        }
        Rational left = parseRational(args[0]);
        Rational right = parseRational(args[2]);
        String operator = args[1];

        Object result = null;
        switch (operator) {
            case "+":
                result = left.add(right);
                break;
            case "-":
                result = left.substract(right);
                break;
            case "x":
                result = left.multiply(right);
                break;
            case "/":
                result = left.divide(right);
                break;
            case "==":
                result = left.isEqualTo(right);
                break;
            default:
                System.err.println("Unknown operator: " + operator);
                System.exit(2);
        }

        System.out.println(result);
    }

    private static Rational parseRational(String arg) {
        String[] tokens = arg.split("/");
        int numerator = Integer.parseInt(tokens[0]);
        int denominator = Integer.parseInt(tokens[1]);
        return createRational(numerator, denominator);
    }

    private static Rational createRational(int numerator, int denominator) {
        int gcd = Gcd.computeGcd(numerator, denominator);
        int canonicalNumerator = numerator / gcd;
        int canonicalDenominator = denominator / gcd;

        int normalizedNumerator;
        int normalizedDenominator;
        if (canonicalNumerator * canonicalDenominator < 0) {
            normalizedNumerator = -Math.abs(canonicalNumerator);
            normalizedDenominator = Math.abs(canonicalDenominator);
        } else {
            normalizedNumerator = canonicalNumerator;
            normalizedDenominator = canonicalDenominator;
        }
        if (normalizedNumerator == 0) {
            return new ZeroNumber();
        }
        if (normalizedDenominator == 1) {
            return new IntegerNumber(normalizedNumerator);
        }
        return new Rational(normalizedNumerator, normalizedDenominator);
    }

    private final int numerator;
    private final int denominator;

    public Rational(int numerator, int denominator) {
        this.numerator = numerator;
        this.denominator = denominator;
    }

    public boolean isEqualTo(Rational right) {
        return areEqual(this, right);
    }

    private static boolean areEqual(Rational left, Rational right) {
        return (left.numerator * right.denominator) == (left.denominator * right.numerator);
    }

    public Rational divide(Rational right) {
        return divide(this, right);
    }

    private static Rational divide(Rational left, Rational right) {
        int resultNumerator = left.numerator * right.denominator;
        int resultDenominator = left.denominator * right.numerator;
        return createRational(resultNumerator, resultDenominator);
    }

    public Rational multiply(Rational right) {
        return multiply(this, right);
    }

    private static Rational multiply(Rational left, Rational right) {
        int resultNumerator = left.numerator * right.numerator;
        int resultDenominator = left.denominator * right.denominator;
        return createRational(resultNumerator, resultDenominator);
    }

    public Rational substract(Rational right) {
        return substract(this, right);
    }

    private static Rational substract(Rational left, Rational right) {
        int resultNumerator = left.numerator * right.denominator - right.numerator * left.denominator;
        int resultDenominator = left.denominator * right.denominator;
        return createRational(resultNumerator, resultDenominator);
    }

    public Rational add(Rational right) {
        return add(this, right);
    }

    private static Rational add(Rational left, Rational right) {
        int resultNumerator = left.numerator * right.denominator + right.numerator * left.denominator;
        int resultDenominator = left.denominator * right.denominator;
        return createRational(resultNumerator, resultDenominator);
    }

    public int getNumerator() {
        return numerator;
    }

    @Override
    public String toString() {
        return numerator + "/" + denominator;
    }
}

class IntegerNumber extends Rational {

    public IntegerNumber(int numerator) {
        super(numerator, 1);
    }

    @Override
    public String toString() {
        return "" + getNumerator();
    }
}

class ZeroNumber extends IntegerNumber {
    public ZeroNumber() {
        super(0);
    }

    @Override
    public String toString() {
        return "0";
    }
}

1.21 Héritage

En Java, extends signale une relation d'héritage entre 2 classes. On dit que la classe fille hérite de la classe mère. La classe mère est dite "générique". La classe fille est dite "spécifique".

class Person {
    public String getDescription() {
        return "Je suis une personne.";
    }
}
class Employee extends Person {
    @Override
    public String getDescription() {
        return "Je suis un employé.";
    }
}

1.21.1 Affectations

Le type de la lvalue (pour "left-value") est, dans ces 2 exemples, identique au type effectivement instancié.

Person p = new Person();
System.out.println(p.getDescription());
// Je suis une personne.
Employee e = new Employee();
System.out.println(e.getDescription());
// Je suis un employé.

L'affectation d'un objet d'un type à une référence d'un super-type est toujours légale.

Person p = new Employee();
System.out.println(p.getDescription());

L'affectation d'un objet d'un type à une référence d'un sous-type nécessite une conversion.

Person p = new Employee();
Employee e = (Employee) p; // cast
System.out.println(e.getDescription());

En revanche, l'opération de conversion échoue parfois. Le cas échéant, une exception est levée à l'exécution : ClassCastException.

Person p = new Person();
Employee e = (Employee) p;
// Person cannot be cast to Employee

Un objet déclaré String ne peut jamais (même en y mettant du nôtre), être à l'exécution de type Person. La classe Person n'hérite pas de String.

Par conséquent, le code ci-dessous ne compile pas car la conversion n'est pas permise :

String s = "hello";
Person p = (Person) s;
// incompatible types
// String cannot be converted to Person

1.21.2 Constructeur super

Etant donné la super classe :

public class RationalNumber {
    private int numerator;
    private int denominator;

    public RationalNumber(int numerator, int denominator) {
        this.numerator = numerator;
        this.denominator = denominator;
    }
    ...
}

Ecrire cette sous classe :

public class IntegerNumber extends RationalNumber {
}

Donne l'erreur de compilation suivante :

constructor RationalNumber in class RationalNumber cannot be applied to given types;
  required: int,int
  found: no arguments
  reason: actual and formal argument lists differ in length

Une classe qui ne définit pas de constructeur explicitement comme IntegerNumber, définit implicitement un constructeur par défaut. Tout constructeur doit appeler le constructeur de sa classe mère. Ici le constructeur par défaut de IntegerNumber appelle implicitement le constructeur par défaut de RationalNumber. Or il n'existe pas de constructeur par défaut pour RationalNumber puisque l'on en a définit un explicitement.

La solution consiste à créer un constructeur qui appelle le constructeur de la super-classe avec le mot clé super.

public class IntegerNumber extends RationalNumber {
    public IntegerNumber(int numerator) {
        super(numerator, 1);
    }
}

1.21.3 Redéfinition

Pour redéfinir une méthode dans une classe fille il est de bon ton d'utiliser l'annotation @Override. @Override n'est pas obligatoire pour des raisons de rétro-compatibilité avec Java 1.4 qui n'avait pas le concept d'annotation. Cette annotation s'applique aux méthodes uniquement (d'autres annotations peuvent s'appliquer aux classes, champs, paramètre, etc.).

Toute erreur de saisie dans le nom de la méthode ou bien dans les types des paramètres sera détectée par le compilateur si le but était de redéfinir la méthode.

Par exemple la classe mère définit la méthode show() :

public class RationalNumber {
    public String show() {
        return this.numerator + "/" + this.denominator;
    }
    ...
}

On a l'intention de redéfinir la méthode show() dans la classe fille. Or ici on s'est trompé et on l'a nommé display (et la classe mère ne définit pas de méthode display()).

public class IntegerNumber extends RationalNumber {
    ...

    @Override
    public String display() {
        return "" + numerator;
    }
}

Le message du compilateur ressemble à ceci :

method does not override or implement a method from a supertype

De la même manière si on nomme correctement la méthode mais qu'on change les paramètres formels on obtiendra une erreur de compilation :

@Override
public String show(boolean newLine) {
    return "" + numerator + (newLine ? "\n" : "");
}

1.21.4 Périmètre et air

Voici un exemple de code procédural :

public class Geometry {

    public static void main(String[] args) {
        System.out.println(area(new Circle(10)));
        System.out.println(area(new Rectangle(3, 4)));
        System.out.println(area(new Square(5)));
        System.out.println(area("hello")); // IllegalArgumentException
    }

    public static double area(Object shape) {
        if (shape instanceof Circle) {
            Circle circle = (Circle) shape;
            return circle.radius * circle.radius * Math.PI;
        }
        if (shape instanceof Rectangle) {
            Rectangle circle = (Rectangle) shape;
            return circle.width * circle.height;
        }
        if (shape instanceof Square) {
            Square circle = (Square) shape;
            return circle.side * circle.side;
        }
        throw new IllegalArgumentException("Unknown shape: " + shape.getClass());
    }

    public static double perimeter(Object shape) {
        if (shape instanceof Circle) {
            Circle circle = (Circle) shape;
            return 2 * circle.radius * Math.PI;
        }
        if (shape instanceof Rectangle) {
            Rectangle circle = (Rectangle) shape;
            return 2 * circle.width + 2 * circle.height;
        }
        if (shape instanceof Square) {
            Square circle = (Square) shape;
            return 4 * circle.side;
        }
        throw new IllegalArgumentException("Unknown shape: " + shape.getClass());
    }
}

1.21.5 Exercice

  1. Ecrire une méthode main(String[]) qui teste le calcul du périmètre d'un rectangle.
  2. Coder la classe Rectangle.
  3. Ecrire et tester les classes pour le carré et le cercle.
  4. Tester le calcul de l'air pour les 3 types de formes.

1.21.6 Style objet

Pour traiter de manière uniforme une variété d'objets de types potentiellement différents, Java fournit le concept d'interface. C'est une sorte de classe abstraite qui fournit seulement un contrat. Le contrat doit être satisfait par le fournisseur et par le consommateur. Le fournisseur du contrat correspond à la classe qui implémente l'interface. Le consommateur correspond à la classe qui invoque une méthode de l'interface en question.

public interface Shape {
    double area();
    double perimeter();
}
public class Circle implements Shape {
    private final double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double area() {
        return radius * radius * Math.PI;
    }

    @Override
    public double perimeter() {
        return 2 * Math.PI * radius;
    }
}

1.21.7 Exercice

Dans cet exercice, on va réécrire le code procédural de Geometry dans un style objet. Il faut pour cela créer un nouveau projet (pour ne pas faire de conflit avec les classes Circle, Rectangle et Square de l'exercice précédant). La classe Geometry définissait les 2 méthodes perimeter et area. Ces 2 méthodes supportaient les 3 types de formes. Dans cet exercice, chaque classe de forme (c-à-d. Circle, Rectangle et Square) définira ces 2 méthodes. On aura donc au total 3 définitions pour le périmètre et 3 définitions pour l'aire.

  1. Tester et expérimenter dans un main(String[]) le calcul du périmètre d'un cercle.
  2. Tester le calcul de l'aire pour le cercle.
  3. Ecrire et tester la classe pour le carré (périmètre et air).
  4. Ecrire et tester la classe pour le rectangle (idem).

L'avantage de cette solution est que l'on peut facilement ajouter de nouvelles formes sans modifier le code existant.

1.21.8 Correction

On code chaque classe dans son propre fichier.

Dans Geometry.java :

public class Geometry {
    public static void main(String[] args) {
        System.out.println(new Circle(10).area());
        System.out.println(new Rectangle(3, 4).area());
        System.out.println(new Square(5).area());
    }
}

Dans Shape.java, on définit l'interface commune à toute les formes :

public interface Shape {
    double area();

    double perimeter();
}

Puis on code dans Circle.java uniquement le comportement relatif aux cercles :

public class Circle implements Shape {
    private final double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double area() {
        return radius * radius * Math.PI;
    }

    @Override
    public double perimeter() {
        return 2 * Math.PI * radius;
    }

}

Idem pour Rectangle.java :


public class Rectangle implements Shape {
    private final double width;
    private final double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double area() {
        return width * height;
    }

    @Override
    public double perimeter() {
        return 2 * width + 2 * height;
    }

}

Idem pour Square.java :

public class Square implements Shape {
    private final double side;

    public Square(double side) {
        this.side = side;
    }

    @Override
    public double area() {
        return side * side;
    }

    @Override
    public double perimeter() {
        return 4 * side;
    }
}

On dit que area, perimeter sont des méthodes polymorphes.

1.21.9 Exercice

Modifier la méthode main(String[]) de sorte de prendre :

Voici deux exemples d'usage :

$ java Geometry perimeter Circle 10
62.83
$ java Geometry area Rectangle 2 3
6

1.21.10 Correction

En pseudo code Java :

void main(String[] args) {
    Shape s;
    switch (args[1]) {
        case "Circle":
            s = new Circle(parseDouble(args[2]));
            break;
        case "Rectangle":
            s = new Rectangle(parseDouble(args[2]), parseDouble(args[3]));
            break;
        default:
            s = null;
    }

    double r;
    switch (args[0])
        case "perimeter":
            r = s.perimeter();
            break;
        case "Rectangle":
            r = s.area();
            break;
        default:
            r = 0;
    }

    println(r);
}

En mettant au propre

void main(String[] args) {
    Shape s = parse(args);

    double r;
    switch (args[0])
        case "perimeter":
            r = s.perimeter();
            break;
        case "Rectangle":
            r = s.area();
            break;
        default:
            r = 0;
    }

    println(r);
}

Shape parse(String[] args) {
    switch (args[1]) {
        case "Circle":
            return new Circle(parseDouble(args[2]));
        case "Rectangle":
            return new Rectangle(parseDouble(args[2]), parseDouble(args[3]));
        default:
            return null;
    }
}

1.22 Résumé

1.22.1 Quelle est la signature de la fonction main ?

La signature d'une fonction comprend le nom de la méthode et les types de ses paramètres formels.

Definition: Two of the components of a method declaration comprise the method signature—the method's name and the parameter types.

http://docs.oracle.com/javase/tutorial/java/javaOO/methods.html

Ainsi la signature est :

main(String[])

La méthode main(String[] args) doit être public, static et de type void pouvoir être exécutée par la machine virtuelle Java.

1.22.2 Quelle est la différence entre == et equals ?

Premièrement == est un opérateur alors que Object.equals(Object) est une méthode.

L'opérateur == évalue si 2 références sont égales. Ainsi

new Object() == new Object() // false

Ceci puisque les adresses mémoire (a.k.a. référence) des 2 objets sont différentes.

Mais

Object a = new Object();
Object b = a;
a == a; // true
a == b // true

La méthode Object.equals(Object obj) doit être implémentée par les sous-classes pour définir comment elle sont égales en valeur c'est à dire en contenu. Par défaut (sur la classe Object), equals est implémentée avec la plus discriminante des relations entre objets : l'égalité des références (c.à.d. avec l'opérateur ==).

La classe String redéfinit la méthode equals de sorte que 2 objets différents mais contenant la même chaîne de caractères soient considérés comme égaux :

String a = new String("hello");
String b = new String("hello");
a.equals(b); // true
a == b; // false

https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#equals-java.lang.Object-

Les types primitifs n'étant par définition pas des types références ne peuvent pas être comparés avec equals. Ce n'est pas problématique puisque dans ce cas l'opérateur == considère la valeur et non pas l'adresse des variables à comparer.

1.22.3 Quelle est la syntaxe pour convertir un double en int ?

La conversion de type autrement appelée cast a la syntaxe suivante :

int i = (int) 1.0d;

On pourra également utiliser des méthodes :

int i = Double.valueOf(1.0d).intValue();

Il existe des méthodes similaires pour restreindre n'importe quel nombre à un type byte, short, int, long, float, double.

https://docs.oracle.com/javase/8/docs/api/java/lang/Number.html

1.22.4 Comment déclarer et initialiser un tableau de int de taille 3 ?

Pour instancier un tableau, on utilise l'opérateur new suivi du type des élements puis de sa taille.

int[] array = new int[3];

Ceci alloue la mémoire nécessaire sur le tas ("heap"). Le tableau contient la valeur par défaut du type stocké (ici la valeur par défaut de int est 0).

System.out.println(array.length); // 3
System.out.println(array[0]); // 0
System.out.println(array[1]); // 0
System.out.println(array[2]); // 0

En revanche, un tableau de 1 booléen pourra être défini comme ceci :

boolean [] array = new boolean[1];

Il contiendra par défaut uniquement des false (valeur par défaut de boolean).

System.out.println(array[0]); // false

On peut également utiliser un initialisateur de tableau :

double[] array = new double[]{1.0, 2.0};
System.out.println(array.length); // 2
System.out.println(array[0]); // 1.0
System.out.println(array[1]); // 2.0

Exemple d'initialisation d'un tableau d'objets :

Object[] array = new Object[]{null, new Object(), "hello"};
System.out.println(array.length); // 3
System.out.println(array[0]); // null
System.out.println(array[1]); // java.lang.Object@32a1bec0
System.out.println(array[2]); // hello

On peut également omettre le type du tableau si on peut déduire le type des éléments :

double[] d = {1.0, Double.NaN};
String[] s = {"", null};
Object[] o = {new Object(), null};

1.22.5 Quels sont les 4 modificateurs de visibilités et leur signification ?

Une méthode (resp. un champ) private n'est visible que dans sa classe.

Une méthode (un champ) public est visible par toutes les classes qui on accès à la classe qui la définit.

Une méthode (un champ) protected est visible par sa classe ainsi que toutes ses sous-classes et par toutes les classes du même package.

Une méthode (un champ) package-protected est visible uniquement par les classes du même package. Noter qu'il n'y a pas de mot-clé du langage pour cette dernière visibilité. L'absence d'indication de portée indique que la méthode est package-protected.

Une classe peut-être ou bien public ou bien package-protected.

Une classe public est visible au delà de son package. Une classe package-protected ne peut être utilisée que dans son package.

1.22.6 Quelle classe définit une liste dynamique ?

La classe ArrayList implémente une liste dynamique (sa taille peut varier pendant l'exécution). Cette classe implémente l'interface List. Il existe d'autres implémentations de liste dynamique : ArrayList et LinkedList.

Contrairement à ArrayList qui utilise un tableau, la classe LinkedList utilise une liste chaînée.

On peut stocker n'importe quel type référence dans une ArrayList. C'est à dire, une ArrayList (ou plus généralement une Collection) ne peut pas stocker de valeur de type primitif). Concrètement une ArrayList ne pourra pas stocker des int.

En revanche, elle supporte le stockage de null. ArrayList ne supporte pas le multithreading.

On ne peut pas modifier la liste quand on la parcourt :

ArrayList<Object> list = new ArrayList<>();
list.add("hello");
for (Object element : list) {
    list.remove("hello"); // java.util.ConcurrentModificationException
}

https://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html

https://docs.oracle.com/javase/8/docs/api/java/util/List.html

1.22.7 Quelle classe doit-on utiliser quand on concatène beaucoup de chaînes de caractères dans une boucle ?

Il est recommendé d'utiliser StringBuilder. Voici un exemple d'utilisation :

ArrayList<Object> list = new ArrayList<>();
list.add("hello"); // imaginons plutôt une très longue liste

StringBuilder buffer = new StringBuilder();
for (Object element : list) {
    buffer.append(element.toString());
}
System.out.println(buffer.toString());

Un StringBuilder représente une séquence mutable de caractère. Cette classe implémente CharSequence qui représente une chaîne de caractère (String en est une implémentation). Elle implémente également Appendable qui représente une séquence sur laquelle on peut ajouter un nouveau caractère ou une autre séquence (concaténation).

Il existe une autre implémentation de Appendable qui supporte le multithreading : StringBuffer.

https://docs.oracle.com/javase/8/docs/api/java/lang/StringBuilder.html

https://docs.oracle.com/javase/8/docs/api/java/lang/StringBuffer.html

1.22.8 Quelle méthode Java permet de générer un nombre (double) aléatoire ?

Le moyen le plus simple est d'utiliser java.util.Math.random().

System.out.println(Math.random()); // un nombre entre 0 et 1
System.out.println(Math.random() * 100); // un nombre entre 0 et 100
System.out.println(1000 + Math.random() * 100); // un nombre entre 1000 et 1100

https://docs.oracle.com/javase/8/docs/api/java/lang/Math.html#random--

1.22.9 Quelle est la différence entre une méthode statique (static) et une méthode non statique ?

Un champ ou une méthode static existe indépendamment d'une quelconque instance de classe. Tous les champs static existent avant qu'on n'instancie leur classe. Il n'existe qu'une seule copie d'un champ statique quelque soit le nombre d'instances créées. En d'autres termes : les champs statiques peuvent être partagés par toutes les instances. Les méthodes, variable, classes imbriquées dans une classes et les bloc d'initalisation peuvent être déclarés static.

class Dog {
  private static int count;
  private String name;
  public Dog(String name) {
    this.name = name;
    count++;
  }
  public String getName() {
    return name;
  }
  public static int getNumberOfDogs() {
    return count;
  }
}

Un champ ou une méthode non statique n'existe que sur des instances de sa classe.

Dog fizz = new Dog("fizz");
Dog buzz = new Dog("buzz");
System.out.println(fizz.getName()); // "fizz"
System.out.println(buff.getName()); // "buzz"
System.out.println(Dog.getNumberOfDogs()); // 2

1.22.10 Qu'est-ce que la surcharge (overloading) ?

Une surcharge de méthode est une autre méthode qui a le même nom mais une signature différente. Ainsi la méthode doStuff(int) surcharge la méthode doStuff().

public void doStuff() {}
public void doStuff(int x) {}

Surcharger c'est donner le même nom à des choses différentes.

D'ailleurs Henri Poincaré, un grand mathématicien du début du XXe siècle aurait dit :

la mathématique est l’art de donner le même nom à des choses différentes.

1.22.11 Qu'est-ce que la redéfinition ou (overriding) ?

Chaque classe qui hérite d'une autre classe peut redéfinir les méthodes non private ou non final de sa super-classe.

Le bénéfice consiste à redéfinir le comportement qui est spécifique à la sous-classe.

public class Animal {
  public void printHello() {
    System.out.println("hello");
  }
}
public class Cat extends Animal {
  @Override // fortement conseillé mais pas obligatoire
  public String printHello() {
    System.out.println("Meouw");
  }
}

On appelle pompeusement polymorphisme, la capacité d'une classe à avoir comportement différent de sa classe mère (ou classe de base, ou super-classe).

1.22.12 Expliquer ce qu'est le constructeur par défaut ?

Le constructeur par défaut est créé implictement par le compilateur lorsque (et uniquement) la classe n'en définit pas un. Ce constructeur ne prend aucun argument et est public.

Ainsi la classe class Dog {} définit implicitement un constructeur vide et sans paramètre. On pourra ainsi instancier la classe comme ceci : new Dog().

1.22.13 À quoi correspond le mot-clé "this" ?

La variable this est une référence à l'objet en cours.

class Cat {
  private String name;
  public Cat(String name) {
    this.name = name;
  }
  @Override
  public boolean equals(Object o) {
    Cat other = (Cat) o;
    System.err.println(this);
    return this.name.equals(o.name);
  }
  @Override
  public String toString() {
    return name;
  }
}

Cat rope = new Cat("rope");
Cat barrel = new Cat("barrel");
rope.equals(barrel); // println("rope")
barrel.equals(rope); // println("barrel")

1.22.14 Comment récupérer la longueur d'un tableau ?

La longueur d'un tableau est stockée dans un champ length.

double[] array = new double[]{1.0, 2.0};
int lastIndex = array.length - 1; // 1
System.out.println(array[lastIndex]); // 2.0

1.22.15 Comment récupérer la longueur d'une liste dynamique ?

La longueur d'une List se calcule dynamiquement à l'aide de la méthode size().

System.out.println(Arrays.asList("a", "b", "c").size()); // 3

1.22.16 Quelle est la syntaxe d'un switch ?

Voici un exemple de switch sur un int :

switch (100 % 2) {
    case 0:
        // n'importe quelle suite d'instruction
        System.out.println("0");
        break; // optionel
    case 1:
        System.out.println("1");
        break;
    default: // optionel
        System.out.println("Don't know...");
}

1.22.17 Quelle est la valeur par défaut pour un int ?

La valeur pour tous les type primitifs numériques est 0 (0.0 pour les double et float).

La valeur null est la valeur par défaut des type références (les objets).

1.22.18 Quelle est la valeur par défaut pour un boolean ?

false

1.22.19 Quelle est la valeur par défaut pour une String ?

Comme String est un type référence (objet) sa valeur par défaut est : null.

1.22.20 Quelle est la valeur par défaut pour un Object ?

Pas de surprise, la valeur par défaut est null. Attention ce n'est pas NULL ni 0 comme en C.

1.22.21 Quelle méthode permet de scinder une chaîne de caractère ?

On peut utiliser String.split(String).

Le paramètre de split est une expression rationnelle (regular expression a.k.a. "regex" ou "regexp").

String[] split = "a b\tc\nd".split("\\s");
System.out.println(Arrays.toString(split)); // [a, b, c, d]

Voici un exemple avec une expression rationnelle plus compliquée :

String[] split = "1a2!3 4".split("[^0-9]");
System.out.println(Arrays.toString(split)); // [1, 2, 3, 4]

https://docs.oracle.com/javase/8/docs/api/java/lang/String.html#split-java.lang.String-

https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html#sum

1.22.22 Quelle est la syntaxe pour appeler une méthode d'instance ?

Foo myObject = new Foo();
myObject.myMethod();

À la 2e ligne, myObject est l'objet receveur de la méthode myMethod.

1.22.23 Quelle est la syntaxe pour référencer un champ d'instance ?

Bar myObject = new Bar();
int value = myObject.myField;

1.22.24 Etant donné "class Animal {}" et "class Dog extends Animal {}", lesquelles de ces affectations compilent :

class Animal {}
class Dog extends Animal {}

Voici les résultats :

Animal a = new Animal(); // OK
Dog d = new Dog(); // OK
Dog d = new Animal(); // fail!
Animal a = new Dog(); // OK

1.22.25 Etant donné une super classe Animal qui définit uniquement le constructeur Animal(String name), quelle définition du constructeur Dog compilent t'elles ?

Dog() {} // fail!
Dog() { super(); } // fail!
Dog() { super("I'm a dog!"); } // OK
Dog(String dogName) { super(dogName); } // OK

1.22.26 Quelles règles s'appliquent pour la redéfinition de méthode ?

La signature doit être strictement identique.

Le nom doit être identique.

Le type de retour peut-être plus spécialisé (sous classe du type de retour de la méthode qui est redéfinie). On appelle cela pompeusement covariance du type de retour.

La méthode qui redéfinie ne doit pas restreindre la visibilité. Une méthode public ne peut pas être redéfinie comme private, protected ou package-protected.

1.23 Correction Evaluation 1

1.23.1 Exercice 1

1.23.1.1 Enoncé

Les permissions de fichier Unix sont encodées en octal (par ex. 0755). En revanche, elles sont affichées à l'utilisateur sous la forme de 3 "rwx". Par exemple, lister les fichiers donnera :

$ ll /bin/echo
-rwxr-xr-x 1 thieux 197121 28352 août  28  2015 /bin/echo*

Pour extraire :

Chaque chiffre ainsi extrait peut varier entre 0 et 7. Voici comment on les interprète :

Noter que le reste de la division entière :

Consignes :

Créer une classe UnixPermissions et une méthode public String permissionsFor(int mode) qui suit le contrat suivant :

On pourra utiliser :

1.23.1.2 Correction

package fr.arolla.java8esgi.controle1;

import java.util.ArrayList;
import java.util.Collections;

public class UnixPermissions {

    public String permissionsFor(int mode) {
        if (mode < 0 || mode > 0777) {
            throw new IllegalArgumentException(
                    String.format("Invalid mode %s%s", Math.signum(mode) > 0 ? "" : "-", Integer.toOctalString(Math.abs(mode))));
        }
        ArrayList<String> permissionList = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            int singlePermission = mode % 8;
            int[] permission = decodeAsBits(singlePermission);
            String formattedPermission =
                    ((permission[0] == 1) ? "r" : "-") +
                    ((permission[1] == 1) ? "w" : "-") +
                    ((permission[2] == 1) ? "x" : "-");
            permissionList.add(formattedPermission);
            mode /= 8;
        }
        Collections.reverse(permissionList);
        return String.join(" ", permissionList);
    }

    private int[] decodeAsBits(int singlePermission) {
        int[] bits = new int[3];
        for (int j = 0; j < 3; j++) {
            int bit = singlePermission % 2;
            bits[3 - j - 1] = bit;
            singlePermission /= 2;
        }
        return bits;
    }

}

1.23.2 Exercice 2

1.23.2.1 Enoncé

Etant donné la suite d'instructions suivante :

ArrayList<Shape> shapes = new ArrayList<>();
shapes.add(new Circle(0, 10));
shapes.add(new Triangle(0, 0, 0, 1, 1, 0));
shapes.add(new Rectangle(0, 0, 10, 15));

for (Shape shape : shapes) {
    System.out.println(shape.dumpContent());
}

Et les constructeurs suivants :

Circle(double center, double radius)
Triangle(double x1, double y1, double x2, double y2, double x3, double y3)
Rectangle(double topLeftX, double topLeftY, double width, double height)

Consignes :

  1. Définir l'interface Shape.
  2. Implémenter les classes Circle, Triangle et Rectangle.

1.23.2.2 Correction

Etant donné le programme suivant :

public class ShapesDemo {
    public static void main(String[] args) {
        ArrayList<Shape> shapes = new ArrayList<>();
        shapes.add(new Circle(0, 10));
        shapes.add(new Triangle(0, 0, 0, 1, 1, 0));
        shapes.add(new Rectangle(0, 0, 10, 15));

        for (Shape shape : shapes) {
            System.out.println(shape.dumpContent());
        }
    }
}

On peut coder les classes Shape, Circle, Triangle et Rectangle comme suit :

public interface Shape {
    String dumpContent();
}
public class Circle implements Shape {
        private final double center;
        private final double radius;

        public Circle(double center, double radius) {
            this.center = center;
            this.radius = radius;
        }

        @Override
        public String dumpContent() {
            return String.format("center=%s; radius=%s", center, radius);
        }
    }
public class Triangle implements Shape {
        private final double x1;
        private final double y1;
        private final double x2;
        private final double y2;
        private final double x3;
        private final double y3;

        public Triangle(double x1, double y1, double x2, double y2, double x3, double y3) {
            this.x1 = x1;
            this.y1 = y1;
            this.x2 = x2;
            this.y2 = y2;
            this.x3 = x3;
            this.y3 = y3;
        }

        @Override
        public String dumpContent() {
            return String.format("x1=%s; y1=%s; x2=%s; y2=%s; x3=%s; y3=%s", x1, y1, x2, y2, x3, y3);
        }
    }
public class Rectangle implements Shape {
        private final double topLeftX;
        private final double topLeftY;
        private final double width;
        private final double height;

        public Rectangle(double topLeftX, double topLeftY, double width, double height) {
            this.topLeftX = topLeftX;
            this.topLeftY = topLeftY;
            this.width = width;
            this.height = height;
        }

        @Override
        public String dumpContent() {
            return String.format("topLeftX=%s; topLeftY=%s; width=%s; height=%s", topLeftX, topLeftY, width, height);
        }
    }

1.24 HashMap

Une Map est une structure de données qui associe des clés à des valeurs (une clé pour une valeur et vice-versa). Elle n'accepte pas de clé en doublon. Enfin, une clé peut s'associer à au plus une seule valeur.

Cette définition ressemble à celle d'une fonction mathématique en ce qu'un élément de l'ensemble des antécédants associe un et un seul élément de l'ensemble image.

Pour créer une association clé-valeur on peut utiliser Map.put(Object, Object).

HashMap<Object, Object> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");

Pour récupérer une valeur à partir d'une clé on utilise : Map.get(Object).

HashMap<Object, Object> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.get(1); // "one"
map.get(2); // "two"

Le dernier qui applique put a gagné :

HashMap<String, Integer> map = new HashMap<>();
map.put("I", 1);
map.put("I", 10);
System.out.println(map.get("X")); // 10

On récupère null lorsque la clé n'existe pas :

HashMap<String, Integer> map = new HashMap<>();
System.out.println(map.get("MISSING_KEY")); // null

Mais on peut explicitement stocker des valeurs null dans la map. Ainsi on pourra récupérer null même si la clé existe...

HashMap<String, Integer> map = new HashMap<>();
map.put("I", null);
System.out.println(map.get("I")); // null

Ainsi récupérer une valeur null n'indique pas qu'une association est absente. Il faut utiliser pour cela la méthode Map.containsKey(Object).

HashMap<String, Integer> map = new HashMap<>();
System.out.println(map.containsKey("MISSING_KEY")); // false

Mais en créant une association :

HashMap<String, Integer> map = new HashMap<>();
map.put("I", null);
System.out.println(map.containsKey("I")); // true

Autre bizzarie de l'implémentation HashMap, on peut utiliser une clé null (à éviter cependant).

HashMap<String, Integer> map = new HashMap<>();
map.put(null, 10);
System.out.println(map.get(null)); // 10
System.out.println(map.containsKey(null)); // true

Toute classe utilisée comme clé doit redéfinir hashCode() et equals(). Si on ne le fait pas, on ne retrouvera pas les données dans la Map.

1.24.1 Exercice : compter les mots

Ecrire une méthode Map<String, Integer> countWords(String text) qui calcule le nombre d'occurence pour chaque mot.

1.24.2 Exercice : hashCode et equals

Etant donné l'extrait de programme suivant :

import static java.lang.System.out;

public static void main(String[] args) {
    HashMap<Key, String> map = new HashMap<>();
    map.put(new Key("foo"), "FOO");
    out.println(map.get(new Key("foo"))); // FOO
    out.println(map.get(new Key("bar"))); // null
}
  1. Ecrire une classe Key et son constructeur.
  2. Lancer le programme.
  3. Implémenter hashCode et equals
  4. Relancer le programme.

1.24.3 Exercice : lettre anonyme

Pour écrire une lettre anonyme, on découpe dans le journal des mots. Ces bouts de journaux découpés forment notre dictionnaire. Pour écrire une lettre, tous les mots nécessaires doivent être présent dans le dictionnaire. De plus, si le mot "bonjour" est présent 2 fois dans la lettre, le dictionnaire devra comporter au moins 2 fois le mot "bonjour".

  1. Implémenter une méthode canWriteLetter(List<String> wordsInLetter, HashMap<String, Integer> dictionary) qui décide s'il est possible d'écrire une lettre anonyme à l'aide d'un dictionnaire donné.

1.24.4 Correction : compter les mots

Voici une première implémentation naïve (elle ne découpe pas très bien les mots et oublie certains mots). Cependant nous concentrons pour le moment sur la logique de comptage des mots.

package fr.arolla.java8esgi.hashmap;

import java.util.*;

public class WordCounter {

    public static void main(String[] args) {
        WordCounter wordCounter = new WordCounter();
        Map<String, Integer> countPerWord = wordCounter.countWords(TEXT);
        List<String> wordsSortedAlphabetically = new ArrayList<>(countPerWord.keySet());
        Collections.sort(wordsSortedAlphabetically);
        for (String word : wordsSortedAlphabetically) {
            System.out.printf("%16s: %d%n", word, countPerWord.get(word));
        }
    }

    public Map<String, Integer> countWords(String text) {
        List<String> words = Arrays.asList(text.split("\\s")); // découpage naïf
        ArrayList<String> filteredWords = new ArrayList<>();
        for (String word : words) {
            if (word.matches("[a-zA-Z]+")) { // filtre naïf
                filteredWords.add(word);
            }
        }
        HashMap<String, Integer> map = new HashMap<>();
        for (String word : filteredWords) {
            Integer count = map.containsKey(word) ? map.get(word) : 0;
            map.put(word, count + 1);
        }
        return map;
    }

    public static final String TEXT = "La conscience\n" +
            "\n" +
            "Lorsque avec ses enfants vêtus de peaux de bêtes,\n" +
            "Echevelé, livide au milieu des tempêtes,\n" +
            "Caïn se fut enfui de devant Jéhovah,\n" +
            "Comme le soir tombait, l'homme sombre arriva\n" +
            "Au bas d'une montagne en une grande plaine ;\n" +
            "Sa femme fatiguée et ses fils hors d'haleine\n" +
            "Lui dirent : « Couchons-nous sur la terre, et dormons. »\n" +
            "Caïn, ne dormant pas, songeait au pied des monts.\n" +
            "Ayant levé la tête, au fond des cieux funèbres,\n" +
            "Il vit un oeil, tout grand ouvert dans les ténèbres,\n" +
            "Et qui le regardait dans l'ombre fixement.\n" +
            "« Je suis trop près », dit-il avec un tremblement.\n" +
            "Il réveilla ses fils dormant, sa femme lasse,\n" +
            "Et se remit à fuir sinistre dans l'espace.\n" +
            "Il marcha trente jours, il marcha trente nuits.\n" +
            "Il allait, muet, pâle et frémissant aux bruits,\n" +
            "Furtif, sans regarder derrière lui, sans trêve,\n" +
            "Sans repos, sans sommeil; il atteignit la grève\n" +
            "Des mers dans le pays qui fut depuis Assur.\n" +
            "« Arrêtons-nous, dit-il, car cet asile est sûr.\n" +
            "Restons-y. Nous avons du monde atteint les bornes. »\n" +
            "Et, comme il s'asseyait, il vit dans les cieux mornes\n" +
            "L'oeil à la même place au fond de l'horizon.\n" +
            "Alors il tressaillit en proie au noir frisson.\n" +
            "« Cachez-moi ! » cria-t-il; et, le doigt sur la bouche,\n" +
            "Tous ses fils regardaient trembler l'aïeul farouche.\n" +
            "Caïn dit à Jabel, père de ceux qui vont\n" +
            "Sous des tentes de poil dans le désert profond :\n" +
            "« Etends de ce côté la toile de la tente. »\n" +
            "Et l'on développa la muraille flottante ;\n" +
            "Et, quand on l'eut fixée avec des poids de plomb :\n" +
            "« Vous ne voyez plus rien ? » dit Tsilla, l'enfant blond,\n" +
            "La fille de ses Fils, douce comme l'aurore ;\n" +
            "Et Caïn répondit : « je vois cet oeil encore ! »\n" +
            "Jubal, père de ceux qui passent dans les bourgs\n" +
            "Soufflant dans des clairons et frappant des tambours,\n" +
            "Cria : « je saurai bien construire une barrière. »\n" +
            "Il fit un mur de bronze et mit Caïn derrière.\n" +
            "Et Caïn dit « Cet oeil me regarde toujours! »\n" +
            "Hénoch dit : « Il faut faire une enceinte de tours\n" +
            "Si terrible, que rien ne puisse approcher d'elle.\n" +
            "Bâtissons une ville avec sa citadelle,\n" +
            "Bâtissons une ville, et nous la fermerons. »\n" +
            "Alors Tubalcaïn, père des forgerons,\n" +
            "Construisit une ville énorme et surhumaine.\n" +
            "Pendant qu'il travaillait, ses frères, dans la plaine,\n" +
            "Chassaient les fils d'Enos et les enfants de Seth ;\n" +
            "Et l'on crevait les yeux à quiconque passait ;\n" +
            "Et, le soir, on lançait des flèches aux étoiles.\n" +
            "Le granit remplaça la tente aux murs de toiles,\n" +
            "On lia chaque bloc avec des noeuds de fer,\n" +
            "Et la ville semblait une ville d'enfer ;\n" +
            "L'ombre des tours faisait la nuit dans les campagnes ;\n" +
            "Ils donnèrent aux murs l'épaisseur des montagnes ;\n" +
            "Sur la porte on grava : « Défense à Dieu d'entrer. »\n" +
            "Quand ils eurent fini de clore et de murer,\n" +
            "On mit l'aïeul au centre en une tour de pierre ;\n" +
            "Et lui restait lugubre et hagard. « Ô mon père !\n" +
            "L'oeil a-t-il disparu ? » dit en tremblant Tsilla.\n" +
            "Et Caïn répondit : \" Non, il est toujours là. »\n" +
            "Alors il dit: « je veux habiter sous la terre\n" +
            "Comme dans son sépulcre un homme solitaire ;\n" +
            "Rien ne me verra plus, je ne verrai plus rien. »\n" +
            "On fit donc une fosse, et Caïn dit « C'est bien ! »\n" +
            "Puis il descendit seul sous cette voûte sombre.\n" +
            "Quand il se fut assis sur sa chaise dans l'ombre\n" +
            "Et qu'on eut sur son front fermé le souterrain,\n" +
            "L'oeil était dans la tombe et regardait Caïn.";

}

Le programme affiche :

           Alors: 3
              Au: 1
           Ayant: 1
             Cet: 1
      Chassaient: 1
           Comme: 2
     Construisit: 1
            Cria: 1
             Des: 1
            Dieu: 1
...

Remarquons cependant que le l'expression rationnelle "[a-zA-Z]+" filtre trop de mot. Par exemple, le mot "Arrêtons" n'est pas dans la sortie du programme. Mais laissons cette amélioration à un prochain exercice.

1.24.5 Correction : hashCode et equals

Quand on ne redéfinit pas hashCode ou equals on ne retrouve pas les clés précédement stockée dans la map :

public class BadKey {

    public static void main(String[] args) {
        HashMap<BadKey, String> map = new HashMap<>();
        map.put(new BadKey("foo"), "FOO");
        out.println(map.get(new BadKey("foo"))); // null, :'(
        out.println(map.get(new BadKey("bar"))); // null,
    }
    
    private int id;
    private String name;

    BadKey(String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "BadKey(id=" + id + ", name=" + name + ")";

    }
}

En revanche, en définissant hashCode et equals on retrouvera nos clés :

class GoodKey {

    public static void main(String[] args) {
        HashMap<GoodKey, String> map = new HashMap<>();
        map.put(new GoodKey("foo"), "FOO");
        out.println(map.get(new GoodKey("foo"))); // "FOO" :)
        out.println(map.get(new GoodKey("bar"))); // null :)
    }

    private int id;
    private String name;

    public GoodKey(String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public int hashCode() {
        return name.length(); // implémentation peu efficace
    }

    @Override
    public boolean equals(Object obj) {
        return obj instanceof GoodKey && name.equals(((GoodKey) obj).name);
    }

    @Override
    public String toString() {
        return "GoodKey(id=" + id + ", name=" + name + ")";
    }
}

Il faut noter que l'implémentation de hashCode() bien que correcte, donnera des performances désastreuse. En effet, GoodKey donne le même hashCode pour toutes les String de même longueur. Si notre ensemble de clés (ici String) a en moyenne beaucoup de mots de même longueur, l'utilisation de la map sera pas optimale.

Note that using many keys with the same hashCode() is a sure way to slow down performance of any hash table.

https://docs.oracle.com/javase/8/docs/api/java/util/HashMap.html

1.24.6 Correction : lettre anonyme

package fr.arolla.java8esgi.hashmap;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

public class AnonymousLetter {

    public static void main(String[] args) {
        HashMap<String, Integer> dictionary = new HashMap<>();
        dictionary.put("hello", 1);
        List<String> letter = Arrays.asList("hello", "there");
        boolean ok = new AnonymousLetter().canWriteLetter(letter, dictionary);
        System.out.println(ok);
    }

    public boolean canWriteLetter(List<String> wordsInLetter, HashMap<String, Integer> dictionary) {
        for (String word : wordsInLetter) {
            int count = dictionary.getOrDefault(word, 0);
            if (count < 1) {
                return false;
            }
            dictionary.put(word, count - 1);
        }
        return true;
    }

}

Dans cette implémentation, on a utilisé la méthode Map.getOrDefault(Object key, V defaultValue) plutôt que de gérer le cas d'une valeur null nous même.

1.25 Autoboxing

En Java les variable de types primitifs (les principaux étant int, double, boolean, char, etc.) sont stockés sur la pile. Ils sont comparés en valeur contrairement aux type références (les objets).

Chaque type primitif est associé à un type référence (par ex. Integer, Double, Boolean, Character). On les appelle type "wrapper". Ceci parce que chaque type référence encapsule une valeur immuable d'un type primitif.

Une fois créé, un objet wrapper ne peut plus être modifié :

Integer n = new Integer(16);
int i = n.intValue();
int diffSign = n.compareTo(n);
double d = n.doubleValue();
String s = n.toString();
// etc.

Il n'existe pas de méthode pour modifier l'objet Integer précédement créé.

Pour modifier l'objet il faut créer un nouvel objet et modifier la référence n (c'est à dire réaffecter un nouvel objet à la même variable).

Integer n = new Integer(16);
n = new Integer(17);
assert n != n;

L'instruction assert vérifie que la condition n != n est vrai. Si ce n'est pas le cas, une java.lang.AssertionError est lancée.

Heureusement à partir de Java 5, le langage supporte l'auto-boxing et l'auto-unboxing.

Integer n = new Integer(42);
n++; // extrait le int, l'incrémente et l'emballe dans un nouvel Integer
assert n == 43;

Voici une démonstration de l'état des références pour ce qui vient de se passer :

Integer n = new Integer(42);
Integer before = n;
n++;
Integer after = n;
assert before != after;

Avant Java 5 on devait procéder ainsi :

Integer n = new Integer(42);
int i = n.intValue();
i++;
n = new Integer(i);
assert n == 43;

Il faut cependant savoir que les objets créés peuvent être les mêmes pour des raisons de performance. C'est le cas des Integer plus petit que 127.

Integer i1 = 127;
Integer i2 = 127;

assert i1 == i2;

En revanche dès 128, les objets créés sont différents :

Integer i1 = 128;
Integer i2 = 128;

assert i1 != i2;

Il faut cependant faire très attention au référence null qui sont déballées (unboxing).

Integer n = null;
int i = n; // java.lang.NullPointerException

2 Les collections

Nous avons vu jusqu'ici quelques collections concrètes : ArrayList, HashSet et HashMap.

La bibliothèque standard fournit un ensemble riche de collections.

Une collection est un conteneur qui permet de manipuler un ensemble d'objets.

L'API standard définit des interfaces ainsi que des implementations.

Les interfaces principales sont List Set, Map, Queue et Deque.

Set n'a ni de notion d'orde ni de doublon. C'est un type qui se rapproche du concept d'ensemble en mathématique.

List est un conteneur ordonné. On accède aux éléments par leurs index (qui commence à 0 comme les tableaux).

Map est un conteneur associatif. Toute valeur est indexée par une clé.

Queue modélise une file (premier arrivé, premier servi). Et Deque modélise une pile (première assiète empilée, dernière assiette nettoyée).

Les interfaces permettent à des programmes écrits par des équipes différentes d'être interopérables en utilisant un format universel d'échange.

interface List extends Collection {}
interface Set extends Collection {}

Les implémentations principales de Set sont HashSet et TreeSet. Pour Map : HashMap et TreeMap.

Un hash ou condensat en français est une fonction cryptographique. Les hash sont utilisées en pratique pour vérifier efficacement si deux objets ont de forte chances d'etre égaux (et aussi comme signature cryptographique). Par exemple, MD5 et SHA2 sont deux fonctions de hashage. Un hash est une fonction qui possède la propriété d'être difficilement réversible. Deux plus deux éléments différents de l'ensemble des antécédants peuvent donner le même élément dans l'ensemble image. Lorsque c'est le cas on parle de collision.

Dit plus formellement,

f(x) = f(y) => x = y

Remplaçons f() par Object.hashCode(). Nous avons le lien entre HashMap, HashSet et Object : tout élément d'une collection de type HashSet devrait redéfinir la méthode hashCode() (ainsi que la méthode equals(Object)).

https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#hashCode()

https://docs.oracle.com/javase/tutorial/collections/intro/index.html

2.1 Exercice

Écrire une méthode qui calcule la somme des éléments d'une liste.

int sum(ArrayList<Integer> collection);

2.2 Exercice

Écrire une méthode qui supprime le dernier élément d'une liste non vide.

void deleteLast(ArrayList<Integer> collection);

2.3 Exercice

Écrire une méthode qui place le produit des deux premiers éléments en troisième position qui place le produit du troisième élément et du quatrième en cinquième place, etc.

Ainsi la liste composé des éléments : {2 3 20 30} => {2 3 6 20 30 600}.

Pour les cas spécifique par exemple {1} => {1}.

Pour les cas spécifique par exemple {2 3 10} => {2 3 6 10}.

Utiliser le prototype suivant :

void productInline(ArrayList<Integer> collection);

2.4 Exercice

Écrire une méthode qui supprime l'élément de valeur 0 d'une liste d'entiers.

void deleteZero(ArrayList<Integer> collection);

2.5 Exercice

class Person {
    private String name;
    Person(String name) {
        this.name = name;
    }
}

Écrire une méthode qui supprime une personne étant donné son nom.

void deleteByName(ArrayList<Person> persons, String name);

Noter qu'ici l'implémentation de deleteByName() doit appeler la méthode getName(). Cela introduit un couplage entre deleteByName et Person.

2.6 Exercice

Écrire une méthode qui supprime l'objet personne qui correspond à la référence (c'est à dire l'adresse de l'objet et non pas sa valeur) passée en argument. Ne plus utiliser la fonction getName() dans l'implémentation de delete().

void delete(ArrayList<Person> persons, Person personToDelete);

Note sur les références :

Object a = new Object();
Object b = new Object();
println(a == b); // false
println(a.equals(b)); // false

String sa = new String("A");
String sb = new String("A");
println(sa == sb); // false
println(sa.equals(sb)); // true

L'implémentation par défaut de equals (c'est à dire celle définie dans la classe Object) est équivalente à l'opérateur ==. De plus, String extends Object et redéfini la méthode equals pour calculer l'égalité en valeur (c'est à dire la suite de caractères de la chaîne).

Exemple d'usage :

ArrayList<Person> persons = new ArrayList<>();
Person empty = new Person("");
persons.add(new Person(""));
persons.add(new Person("Mathieu"));
persons.add(empty);

delete(persons, empty);
System.out.println(persons); // [Person(""), Person("Mathieu")]

2.7 Exercice

Modifier la méthode delete() pour faire une selection en valeur.

Exemple d'usage :

ArrayList<Person> persons = new ArrayList<>();
persons.add(new Person(""));
persons.add(new Person("Mathieu"));
persons.add(new Person(""));

delete(persons, new Person(""));
System.out.println(persons); // [Person("Mathieu")]

2.8 Exercice

Écrire une méthode modifie une liste de nombre de tel sorte à remplacer chaque paire de nombre par leur produit puis qui effectue une somme du résultat.

Par exemple, {1 2 10 20} => {2 200} => {202}.

void sumOfProduct(ArrayList<Integer> collection);

2.9 Exercice

Écrire une méthode qui transfère le contenu d'une ArrayList dans une autre. Le contenu de la source doit être ajouté à la fin de la destination. La source doit être vide à la fin de l'opération. Respecter le prototype suivant :

void move(ArrayList<Integer> source, ArrayList<Integer> destination);

Utiliser uniquement les méthodes de la classe ArrayList (voir https://docs.oracle.com/en/java/javase/12/docs/api/java.base/java/util/ArrayList.html).

2.10 Exercice

Faire le même traitement sur une LinkedList. Utiliser le prototype suivant :

void move(LinkedList<Integer> source, LinkedList<Integer> destination);

2.11 Exercice

Réécrire une version de move qui généralise le type de ses paramètres. Respecter le prototype suivant :

void move(List<Integer> source, List<Integer> destination);

2.12 Exercice

Réécrire une version de move qui généralise encore plus le type de ses paramètres. Respecter le prototype suivant :

void move(Collection<Integer> source, Collection<Integer> destination);

Essayer de remplacer Collection par Object dans la signature de la méthode. Que se passe-t'il ?

2.13 Classe mère

Object est la classe mère de toute classe. Ce n'est pas le cas des interfaces.

2.14 Exercice

Écrire une méthode de recherche qui renvoie l'indice d'un élément dans une liste, -1 sinon.

int find(List<Integer> collection, int element);

2.15 Exercice

Écrire une méthode de recherche qui renvoie une liste des indices des éléments trouvés dans une liste, la liste vide sinon.

List<Integer> find(List<Integer> collection, int element);

Exemple d'usage,

List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(2);
println(find(numbers, 2)); // [1, 3]

2.16 Exercice

Écrire une méthode qui extrait une sous liste à partir d'une liste d'élements et d'une liste d'index.

List<Integer> extract(List<Integer> collection, List<Integer> indices);

Exemple d'usage,

List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(2);
println(extract(numbers, Arrays.asList(1, 2))); // [2, 2]

Tester que extract(find(list, e)) est vide ou bien contient un ou plusieurs élément e.

2.17 Exercice

Écrire une méthode qui retourne une copie de la liste où seuls les éléments impairs on été conservés.

List<Integer> filterOdds(List<Integer> collection);

2.18 Exercice

Écrire une méthode qui retourne une copie de la liste où seuls les éléments multiple de 3 on été conservés.

List<Integer> filterMultipleOf3(List<Integer> collection);

Puis réécrire la méthode avec la signature plus générale :

List<Integer> filterMultipleOf(List<Integer> collection, int modulo);

2.19 Exercice

Jusqu'ici on a commencé par hardcoder le filtre dans la méthode :

if (e % 2 == 0) {}

puis on a paramétré la valeur du modulo :

if (e % modulo == 0) {}

Comment paramétrer non seulement la valeur (ici 2 paramétré en modulo) mais aussi les opérateurs (ici % et ==) ?

  1. Réimplémenter la méthode filterMultipleOf3 en extrayant une nouvelle méthode boolean accept(int n).
class IntFilters {
    static List<Integer> filterMultipleOf3(List<Integer> collection) {
        ...
        for ...
            if (accept(n)) ...
        return ...
    }
    private boolean accept(int n) { // nouvelle méthode
        return n % 3 == 0;
    }
}
  1. Extraire une classe à partir de la méthode précédament extraite
class IntFilters {
    List<Integer> filterMultipleOf3(List<Integer> collection) {
        ...
        for ...
            if (new MultipleFilter(3).accept(n)) ...
        return ...
    }
}
class MultipleFilter {
    private int modulo;
    MultipleFilter(int modulo) {
        this.modulo = modulo;
    }
    public boolean accept(int n) {
        return n % modulo == 0;
    }
}
  1. Extraire comme paramètre selon le prototype suivants :
class IntFilters {
    List<Integer> filterMultipleOf(List<Integer> collection, MultipleFilter predicate);
}
  1. Extraire une interface à partir de la classe MultipleFilter :
interface IntFilter {
    boolean accept(int n);
}
class MultipleFilter implements IntFilter {
    ...
}
class IntFilters {
    List<Integer> filter(List<Integer> collection, IntFilter predicate) {
        ...
    }
}

2.19.1 Exercice

Trier une liste d'entiers en ordre croissant en utilisant une méthode de la classe Collections. Chercher dans la javadoc.

https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Collections.html

2.19.2 Exercice

Trier une liste de Person par age. Utiliser la surcharge Collections.sort(List<T> list, Comparator<? super T> c). Utiliser la définition de Person suivante :

class Person {
    private int age;
    Person(int age) {
        this.age = age;
    }
    int getAge() {
        return age;
    }
}

Le comparateur est un mécanisme par lequel un développeur peut modifier l'ordre naturel d'une classe. L'ordre naturel d'une classe est défini par implémentation à Comparable. Par exemple, String et Integer implémentent Comparable. Pour les classes qui ne l'implémentent pas ou bien dont l'implémentation n'est pas satisfaisante, on utilisera un comparateur.

https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Comparator.html

2.19.3 Exercice

Trier une liste de Person par age, pour les personnes du même age trier par nom. Coder la logique d'ordre dans un comparateur.

class Person {
    private String name;
    private int age;
    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    String getName() {
        return name;
    }
    int getAge() {
        return age;
    }
}

2.19.4 Exercice

Trier par une liste d'employés par nom de service puis par le nom d'employé. Utiliser un comparateur.

class Employee {
    private String name;
    private BusinessUnit businessUnit;

    Employee(String name, BusinessUnit businessUnit) {
        this.name = name;
        this.businessUnit = businessUnit;
    }

    String getName() {
        return name;
    }

    BusinessUnit getBusinessUnit() {
        return businessUnit;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", businessUnit=" + businessUnit +
                '}';
    }
}

class BusinessUnit {
    private String name;

    BusinessUnit(String name) {
        this.name = name;
    }

    String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "BusinessUnit{" +
                "name='" + name + '\'' +
                '}';
    }
}

2.19.5 Exercice

Trier par une liste d'employés par nom de service puis par le nom d'employé. Ne pas utiliser de comparateur. À la place faire Employee et BusinessUnit implémenter Comparable.

2.20 List : les pièges

2.20.1 Surcharges de remove

Ne pas confondre les surcharges List.remove(int) et List.remove(Object).

List<Integer> elements = new ArrayList<>();
...
Integer i = 0;
elements.remove(i);

Dans l'extrait précédant, le premier élément qui a la valeur 0 sera supprimé et non pas le premier élément de la liste.

2.20.2 Modifications concurrentes

List<Integer> list = new ArrayList<>();
for (Integer element : list) {
    list.remove(Integer.valueOf(0)); // ConcurrentModificationException
}

Une exception est lancée quand on parcours une collection avec une boucle for-each pendant qu'on modifie la collection.

https://docs.oracle.com/javase/8/docs/api/java/util/ConcurrentModificationException.html

En aparté, la syntaxe for-each est un sucre syntaxique. Le compilateur génère une boucle for traditionnelle et fait appel à un Iterator.

void printAll(Collection<Person> c) {
    for (Iterator<Person> i = c.iterator(); i.hasNext(); ) {
        println(i.next());
    }
}

https://docs.oracle.com/javase/1.5.0/docs/guide/language/foreach.html

2.21 Les séquence d'éléments (Stream)

Les Stream supportent des opérations séquentielle ou parallèle dans un pipeline de transformation.

Cette API est un style de pipe et filtre.

3 Les Flux

3.1 Flux de caractères

3.1.1 Lire un caractère à partir d'un fichier

Pour lire un fichier texte, on utilise par commodité la classe FileReader.

Le constructeur ouvre une connexion au fichier (représenté par un descripteur de fichier).

// nom de fichier : data.txt
FileReader inputStream = new FileReader("data.txt");

Attention, une java.io.FileNotFoundException est jetée si :

int c = inputStream.read();

Cette méthode bloque jusqu'à ce que :

La méthode read() :

3.1.2 Écrire un caractère dans un fichier

FileWriter outputStream = new FileWriter("out.txt");

Crée un descripteur de fichier représentant la connexion au fichier. De plus, un fichier "out.txt" est créé sur le disque.

Attention, une java.io.IOException peut-être jetée :

Pour écrire un caractère :

int a = 97;
outputStream.write(a);

Le caractère à écrire doit être contenu dans les 16 bits de poids faible de l'entier passé en paramètre. Les 16 bits de poids forts sont ignorés.

3.1.3 Exercice 1

Écrire un programme de copie de fichier texte.

Modifiez ce programme de sorte de sorte de faire une mutation dans les bits de poids forts du caractère à écrire :

int a = (1 << 16) | c;
System.out.println(a);
outputStream.write(c);

Le fichier "output.txt" devrait toujours être la copie exacte du fichier "input.txt".

3.1.4 Exercice 2

Écrire un programme qui copie un fichier à la fin d'un autre fichier.

3.1.5 Exercice 3

3.1.6 Corrections

Exercice 1 : voici une première version incomplète :

FileReader fileReader = new FileReader("input.txt");
FileWriter fileWriter = new FileWriter("output.txt");
fileWriter.write(fileReader.read());

Avec le fichier input.txt contenant la ligne :

hello world

Le programme crée un fichier output.txt qui a le mauvais goût d'être vide.

Deuxième version en fermant le fichier de sortie :

FileReader fileReader = new FileReader("input.txt");
FileWriter fileWriter = new FileWriter("output.txt");
fileWriter.write(fileReader.read());
fileWriter.close();

Le fichier output.txt contiendra une seule lettre : h.

Dernière version sans gestion d'exception (cf. « Les exceptions ») :

package fr.arolla.java8esgi.basicio.ex1;

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class Ex1 {
    public static void main(String[] args) throws IOException {
        FileReader fileReader = new FileReader("input.txt");
        FileWriter fileWriter = new FileWriter("output.txt");

        int character;
        while ((character = fileReader.read()) != -1) {
            fileWriter.write(character);
        }
        fileWriter.close(); // déclenche l'écriture sur disque
        fileReader.close(); // rend les ressource à l'OS
    }
}

On pourra réécrire ce programme de la manière suivante pour localiser la logique de copie :

public static void main(String[] args) throws IOException {
    FileReader reader = new FileReader("input.txt");
    FileWriter writer = new FileWriter("output.txt");

    copy(reader, writer);

    writer.close();
    reader.close();
}

private static void copy(FileReader fileReader, FileWriter fileWriter) throws IOException {
    int character;
    while ((character = fileReader.read()) != -1) {
        fileWriter.write(character);
    }
}

Exercice 2 : pour écrire à la fin du fichier on activera le mode append :

public static void main(String[] args) throws IOException {
    FileReader reader = new FileReader("input.txt");
    FileWriter writer = new FileWriter("output.txt", true);

    copy(reader, writer);

    writer.close();
    reader.close();
}

Exercice 3 : en modifiant l'encodage du fichier input.txt à ISO-8859-1, le fichier output pourra ressembler à :

b?po

On a perdu de l'information. On a un bug !

3.2 Flux d'octets

3.2.1 Lecture

Pour ouvrir en lecture un flux de bytes bruts (sans décodage) :

FileInputStream in = new FileInputStream("input.txt");

3.2.2 Écriture

Pour ouvrir un flux en écriture de bytes bruts (sans encodage) :

FileOutputStream out = new FileOutputStream("output.txt");

3.2.3 Lecture et écriture

Le protocole c = in.read() et out.write(c) sont identique à FileReader et FileWriter.

3.2.4 Exercice 4

3.2.5 Exercice 5

3.2.6 Corrections

Pour corriger le bug d'encodage on utilisera des flux orientés octets qui n'interprètent pas la séquence d'octets lue.

package fr.arolla.java8esgi.basicio.ex4;

import java.io.*;

public class Ex4 {
    public static void main(String[] args) throws IOException {
        FileInputStream reader = new FileInputStream("input.txt");
        FileOutputStream writer = new FileOutputStream("output.txt", true);

        copy(reader, writer);

        writer.close();
        reader.close();
    }

    private static void copy(FileInputStream fileReader, FileOutputStream fileWriter) 
        throws IOException {
        int character;
        while ((character = fileReader.read()) != -1) {
            fileWriter.write(character);
        }
    }
}

3.2.7 Résumé

Ouvrir Fermer Lire Écrire
new close read write

Tout programme se voit attribuer un répertoire courant (CWD). Ce chemin devient la racine de tout les fichiers dénotés par des chemins relatifs.

Tenter d'ouvrir une connexion à un fichier qui n'existe pas lève une exception :

Exception in thread "main" java.io.FileNotFoundException: input.txt (Le fichier spécifié est introuvable)

Écrire dans un fichier avec write(int) ne garanti pas que les données seront effectivement écrites physiquement (par exemple sur disque). Pour garantir que les octets sauvés dans un tampon (par des appels à write) sont effectivement écrit par le système d'exploitation il faut utiliser flush().

Quand on a terminé d'utiliser la connexion au fichier, il est recommandé de le fermer en utilisant close(). Par ailleurs, avant que la connexion soit fermée, les octets restant dans le tampon sont déchargés avec flush(). L'idiome de programmation est donc d'utiliser close() mais pas flush().

Attention, une fois la connexion au fichier fermée on ne peut plus écrire dedans. Cela donne le message suivant :

Exception in thread "main" java.io.IOException: Stream closed

Quand un programme effectue des entrées/sorties, il utilise des ressources chères et limitées. La méthode close() les libère.

Les Reader et Writer sont orientés caractères tandis que InputStream et OutputStream sont orientés octets. L'interprétation des octets en caractères par Reader et Writer est faite par ses classes. En revanche, les classes InputStream et OutputStream laissent le développeur décider de la sémantique à donner au flux d'octets.

3.3 File et Path

C:\Users\mathieupauly\AppData\Local\Temp ou /tmp sont des chemins absolus de fichier. Ils représentent tout 2 un répertoire.

Temp est un chemin relatif.

En Java, un chemin est représenté par la classe java.nio.file.Path.

Dans le vocabulaire de Path, Temp est le fileName, C:\Users\mathieupauly\AppData\Local\Temp est le parent, et C:\ le root.

3.3.1 Chemin constant

Représenter un chemin :

Path p = Paths.get("/tmp/foo");

Attention, cela ne fait pas d'opération sur le système de fichier.

Pour récupérer le répertoire parent d'un chemin :

Path example = Paths.get("example.txt");

Path parent = example.getParent(); // null
Path root = path.getRoot(); // null
Path fileName = path.getFileName(); // example.txt

Ici le parent était null car il ne contient que la composante fileName.

En revanche, quand on transforme le premier chemin en chemin absolu :

Path example = Paths.get("example.txt").toAbsolutePath(); 
// C:\Users\mathieupauly\Documents\java8-esgi\java8-esgi-examples\example.txt

Path parent = example.getParent();
// C:\Users\mathieupauly\Documents\java8-esgi\java8-esgi-examples

Pour représenter un chemin avec 2 composantes (un répertoire contenant un fichier) :

Path p = Paths.get("examples", "hello.txt");

Dans ce cas, le root de p sera null, et son parent vaudra examples.

3.3.2 Exercice 6

Écrire un programme qui affiche le chemin absolu du répertoire courant.

3.3.3 Composer et décomposer un chemin

Pour concatèner (ou résoudre) 2 composants d'un chemin.

Path p = Paths
    .get("auie")
    .resolve("tsrn"); // auie\tsrn

Attention, si le composant de droite est absolu, le chemin de gauche est ignoré.

Path p = Paths
    .get("auie")
    .resolve("C:\\tsrn"); // C:\tsrn

L'opération inverse de décomposition (ou relativisation) consiste à trouver un chemin entre 2 points (eux mêmes représentés par des chemins).

Path p1 = Paths.get("eee");
Path p2 = Paths.get("ttttt");
Path navigation = p1.relativize(p2);  // ..\ttttt

Exemple avec des chemins absolus.

Path p1 = Paths.get("C:\\Users\\mathieupauly");
Path p2 = Paths.get("C:\\Program Files (x86)");
Path navigation = p1.relativize(p2); // ..\..\Program Files (x86)

3.3.4 Exercice 7

Écrire un programme qui crée autant de fichier que spécifié en argument.

3.3.5 Lister et créer un répertoire

Pour lire le contenu d'un dossier :

Path currentDirectory = Paths.get(".");
DirectoryStream<Path> content = Files.newDirectoryStream(currentDirectory);
for (Path p : content) {
    System.out.println(p);
}
// .\.idea
// .\input.txt
// .\java8-esgi-examples.iml
// .\output.txt
// .\pom.xml
// .\src
// .\target

Pour créer un dossier :

Files.createDirectory(Paths.get("example"));

Si on essaie de créer un dossier qui existe déjà, une exception est levée : java.nio.file.FileAlreadyExistsException: example.

Pour savoir si un chemin dénote un répertoire :

boolean directory = Files.isDirectory(Paths.get(".")); // true

3.3.6 Corrections

L'exercice 6 : en première approximation on peut calculer le chemin absolu :

System.out.println(Paths.get(".").toAbsolutePath());
// C:\Users\mathieupauly\Documents\java8-esgi\java8-esgi-examples\.
System.out.println(Paths.get(".").toAbsolutePath().normalize());
// C:\Users\mathieupauly\Documents\java8-esgi\java8-esgi-examples

On pourra plus simplement utiliser :

System.out.println(System.getProperty("user.dir"));

Ou également :

System.out.println(new File(".").getCanonicalFile());

Exercice 7 :

package fr.arolla.java8esgi.basicio;

import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class Ex7 {
    public static void main(String[] args) throws IOException {
        String temporaryDirectory = System.getProperty("java.io.tmpdir");
        for (int i = 1; i <= 10; i++) {
            Path path = Paths.get(temporaryDirectory, i + ".txt");
            new FileWriter(path.toFile());
        }
    }
}

3.4 Flux orienté ligne

3.4.1 BufferedReader

Pour lire du texte de manière plus efficace qu'avec FileReader on utilisera BufferedReader. Il faut remarquer que dans "buffered" "reader" il n'y a pas le mot "file". En effet, un BufferedReader est conçu pour lire dans un flux quelconque (un Reader).

BufferedReader bufferedReader = new BufferedReader(new FileReader("input.txt"));
String line = bufferedReader.readLine();
bufferedReader.close(); // ferme le flux sous-jacent (ici FileReader)

Noter que readLine() lit jusqu'à trouver un \n ou \r\n mais n'inclut pas cette fin de ligne dans l'objet retourné.

3.4.2 Exercice 8

Écrire un programme qui lit dans un fichier "doublons.txt" affiche les lignes uniques.

Par exemple "doublons.txt" contiendra :

file
file
input
stream
reader
stream

Le programme doit afficher :

file
input
stream
reader

3.4.3 BufferedWriter

De la même manière, BufferedWriter est conçu pour rendre FileWriter performant et plus simple à utiliser. Un writer bufferisé écrit des lots plus gros de données en une seule fois. Le fait de minimiser le nombre de lectures (opération lente) économise des ressources.

BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter("test.txt"));
bufferedWriter.write("hello");
bufferedWriter.newLine();
bufferedWriter.close(); // ferme le flux sous-jacent (ici FileWriter)
// hello\r\n

3.4.4 Exercice 9

Modifier le programme de l'exercice 8 pour écrire dans un fichier plutôt que d'afficher.

3.4.5 Exercice 10

Écrire un programme qui copie le contenu direct d'un répertoire. Le répertoire à copier sera donné en premier argument du programme. Le répertoire destination sera donné en second argument.

Par exemple si on invoque le programme avec les arguments : "C:\Program Files\Java\jdk1.8.0_60" "C:\Users\mathieupauly\AppData\Local\Temp", alors le répertoire Temp contiendra entre autres les répertoires bin, jre, lib et entre autres les fichiers COPYRIGHT, LICENSE, etc.

3.4.6 Reader et Writer

Comme vu précédement, BufferedReader n'est pas obligé de lire ses données à partir d'un fichier. Même principe pour BufferedWriter qui peut écrire dans un flux quelconque.

Un flux en lecture seule est représenté par Reader. Un flux en écriture est lui représenté par Writer. Ces 2 classes sont des abstractions car elles ne manipulent pas un flux en particulier.

En revanches elles permettent de composer des flux complexes en permettant de faire varier le média physique en jeu (par ex. en fichier ou en mémoire) ainsi que le paramétrage de lecture/écriture (par ex. utilisation d'un tampon ou non).

BufferedReader bufferedReader = new BufferedReader(new StringReader("a\nb\nc"));
String line = bufferedReader.readLine(); // "a"
String character = bufferedReader.read(); // 98 pour 'b'

Ici on a utilisé un tampon (BufferedReader) pour lire une information en mémoire ("a\nb\nc").

L'intérêt est que la ligne de code qui utilise read() peut ignorer qu'elle consomme un BufferedReader. Pour elle tout se passe comme si elle consomme l'interface Reader.

Pour résumer : un BufferedReader est construit à partir d'un autre Reader mais implémente lui même Reader.

public class BufferedReader extends Reader {
    public BufferedReader(Reader in) {
        // ...
    }
    // ...
}

On peut tenir exactement le même raisonnement avec BufferedWriter et Writer.

StringWriter out = new StringWriter();
BufferedWriter bufferedWriter = new BufferedWriter(out);
bufferedWriter.write("one");
bufferedWriter.newLine();
bufferedWriter.write("two");
bufferedWriter.close();
String content = out.toString(); // one\ntwo

La ligne de code qui utilise write(String) ne sait pas qu'elle écrit dans un StringBuffer (utilisé par StringWriter). De plus, elle ne sait pas qu'elle écrit dans un tampon.

Pour résumer, BufferedWriter est construit sur un autre Writer et implémente lui même Writer.

public class BufferedWriter extends Writer {
    public BufferedWriter(Writer out) {
        // ...
    }
}

Ce motif de conception (design pattern) dans lequel une classe expose la même interface qu'elle consomme est nommé «Décorateur» d'après le «Gang of Four» (GOF).

3.4.7 InputStream et OutputStream

Une abstraction similaire existe du côté des flux orientés octets. Un FileInputStream (resp. FileOutputStream) peut-être bufferisé à l'aide d'un BufferedInputStream (resp. BufferedOutputStream). Remarquons une fois de plus l'absence du mot file dans "buffered" "input" "stream".

Pour lire de manière plus efficace dans un flux binaire :

BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("input.txt"));
int c = bufferedInputStream.read();
bufferedInputStream.close(); // ferme le flux sous-jacent

Et pour écrire :

BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream("out.txt"));
bufferedOutputStream.write(97);
bufferedOutputStream.close(); // ferme le flux sous-jacent

Un BufferedInputStream (resp. BufferedOutputStream) est construit sur un InputStream (resp. OutputStream) et implémente également l'interface InputStream (resp. OutputStream). C'est à dire que ces flux orientés octets bufferisés ne sont pas forcément construit sur un flux fichier. Les classes InputStream et OutputStream sont des flux d'octets quelconques.

La classe de lecture d'un flux bufferisé est définie ainsi dans le JDK :

public class BufferedOutputStream extends FilterOutputStream {
    protected OutputStream out;
    public FilterOutputStream(OutputStream out) {
        this.out = out;
    }
    // ...
}

Avec la définition suivante :

public class FilterOutputStream extends OutputStream {
    // ...
}

Et la classe d'écriture de flux bufferisé est définie ainsi dans le JDK :

public class BufferedInputStream extends FilterInputStream {
    public BufferedInputStream(InputStream in) {
        // ...
    }
}

Avec la définition suivante :

public class FilterInputStream extends InputStream { 
    // ...
}

C'est encore le motif de conception «Décorateur» (GOF) qui est utilisé ici dans lequel une classe expose la même interface qu'elle consomme.

3.4.8 InputStream, OutputStream, Reader et Writer

De plus, on peut convertir un flux orienté octet en un flux orienté caractère ou ligne grâce à ces abstractions (InputStream, OutputStream, Reader et Writer). En effet, il existe aussi un 2 adaptateurs qui font le pont entre un flux binaire (byte-oriented) et un flux texte : InputStreamReader et OutputStreamWriter.

BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream("in.txt")));
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("out.txt")));

3.4.9 Exercice 11

Cet exercice est une réécriture du programme de l'exercice précédant. C'est à dire que le but est toujours de faire une copie du contenu direct d'un répertoire. En revanche nous allons séparer le programme en 2 programmes : un lecteur et un écrivain. Le lecteur lira le contenu d'un répertoire. L'écrivain écrira la copie.

bin
lib
bin;directory
lib;directory
LICENSE;file

On lancera donc 2 programmes pour réaliser la copie :

java ContentReader "C:\Program Files\Java\jdk1.8.0_60"
java ContentWriter "C:\Users\mathieupauly\AppData\Local\Temp"

3.4.10 Exercice 12

Modifier les 2 programmes de sorte que la description précédement écrite dans le fichier list.csv soit écrite par ContentReader sur sa sortie standard et lu par ContentWriter sur son entrée standard. On n'utilisera donc plus le fichier list.csv.

Le programmes devront donc être coordonnés à l'aide d'un «pipe» (|). La sortie du premier programme est redirigé dans l'entrée du second.

java ContentReader "C:\Program Files\Java\jdk1.8.0_60" | java ContentWriter "C:\Users\mathieupauly\AppData\Local\Temp"

3.4.11 Corrections

Exercice 8 : on utilisera un Set pour ne pas conserver les doublons.

package fr.arolla.java8esgi.basicio.ex8;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

public class Ex8 {
    public static void main(String[] args) throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new FileReader("doublons.txt"));
        Set<String> uniqueLines = new HashSet<>();
        String line;
        while ((line = bufferedReader.readLine()) != null) {
            if (!uniqueLines.contains(line)) {
                System.out.println(line);
            }
            uniqueLines.add(line);
        }
        bufferedReader.close();
    }
}

Exercice 9 : on conserve le type de l'objet System.out (PrintStream) mais on change l'instance concrète qui sera connectée sur un fichier.

package fr.arolla.java8esgi.basicio.ex9;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;
import java.util.HashSet;
import java.util.Set;

public class Ex9 {
    public static void main(String[] args) throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new FileReader("doublons.txt"));
        PrintStream out = new PrintStream("sansDoublon.txt");
        Set<String> uniqueLines = new HashSet<>();
        String line;
        while ((line = bufferedReader.readLine()) != null) {
            if (!uniqueLines.contains(line)) {
                out.println(line);
            }
            uniqueLines.add(line);
        }
        bufferedReader.close();
        out.close();
    }
}

Exercice 10 : on parcourt le contenu du répertoire source en se demandant pour chaque chemin à copier s'il référence un répertoire ou un fichier normal (pas un périphérique par ex.).

package fr.arolla.java8esgi.basicio.ex10;

import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class Ex10 {
    public static void main(String[] args) throws IOException {
        String source = args[0];
        String destination = args[1];
        System.out.println("Copy from: " + source + " to: " + destination);
        Path destinationDirectory = Paths.get(destination);
        DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get(source));
        for (Path toCopy : stream) {
            Path toCreate = destinationDirectory.resolve(toCopy.getFileName());
            System.out.println("Create: " + toCreate + "...");
            if (Files.isDirectory(toCopy)) {
                Files.createDirectory(toCreate);
            } else if (Files.isRegularFile(toCopy)) {
                Files.createFile(toCreate);
            } else {
                System.err.println("Cannot process " + toCopy);
            }
        }
        stream.close();
        out.close();
    }
}
Copy from: C:\Program Files\Java\jdk1.8.0_60 to: C:\Users\mathieupauly\AppData\Local\Temp
Create: C:\Users\mathieupauly\AppData\Local\Temp\bin...
Create: C:\Users\mathieupauly\AppData\Local\Temp\COPYRIGHT...
Create: C:\Users\mathieupauly\AppData\Local\Temp\db...
Create: C:\Users\mathieupauly\AppData\Local\Temp\include...
Create: C:\Users\mathieupauly\AppData\Local\Temp\javafx-src.zip...
Create: C:\Users\mathieupauly\AppData\Local\Temp\jre...
Create: C:\Users\mathieupauly\AppData\Local\Temp\lib...
Create: C:\Users\mathieupauly\AppData\Local\Temp\LICENSE...
Create: C:\Users\mathieupauly\AppData\Local\Temp\README.html...
Create: C:\Users\mathieupauly\AppData\Local\Temp\release...
Create: C:\Users\mathieupauly\AppData\Local\Temp\src.zip...
Create: C:\Users\mathieupauly\AppData\Local\Temp\THIRDPARTYLICENSEREADME-JAVAFX.txt...
Create: C:\Users\mathieupauly\AppData\Local\Temp\THIRDPARTYLICENSEREADME.txt...

Exercice 11 :

Le programme de lecture du répertoire source pourra être lancé en premier.

package fr.arolla.java8esgi.basicio.ex11;

import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class ContentReader {
    public static void main(String[] args) throws IOException {
        String source = args[0];
        System.out.println("Copy from: " + source);
        DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get(source));
        PrintStream out = new PrintStream("list.csv");
        for (Path toCopy : stream) {
            Path fileName = toCopy.getFileName();
            if (Files.isDirectory(toCopy)) {
                out.println(fileName + ";" + "directory");
            } else if (Files.isRegularFile(toCopy)) {
                out.println(fileName + ";" + "file");
            } else {
                out.println(fileName + ";" + "unknown");
            }
        }
        stream.close();
        out.close();
    }
}
bin;directory
COPYRIGHT;file
db;directory
include;directory
javafx-src.zip;file
jre;directory
lib;directory
LICENSE;file
README.html;file
release;file
src.zip;file
THIRDPARTYLICENSEREADME-JAVAFX.txt;file
THIRDPARTYLICENSEREADME.txt;file

Puis on lancera l'écrivain :

package fr.arolla.java8esgi.basicio.ex11;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class ContentWriter {
    public static void main(String[] args) throws IOException {
        String destination = args[0];
        System.out.println("Copy to: " + destination);
        Path destinationDirectory = Paths.get(destination);
        BufferedReader listing = new BufferedReader(new FileReader("list.csv"));
        String line;
        while ((line = listing.readLine()) != null) {
            String[] columns = line.split(";");
            String filename = columns[0];
            String type = columns[1];
            Path toCreate = destinationDirectory.resolve(filename);
            System.out.println("Create: " + toCreate + "...");
            switch (type) {
                case "directory":
                    Files.createDirectory(toCreate);
                    break;
                case "file":
                    Files.createFile(toCreate);
                    break;
                default:
                    System.err.println("Cannot process " + toCreate + " (unknown type)");
                    break;
            }
        }
        listing.close();
    }
}
Copy to: C:\Users\mathieupauly\AppData\Local\Temp
Create: C:\Users\mathieupauly\AppData\Local\Temp\bin...
Create: C:\Users\mathieupauly\AppData\Local\Temp\COPYRIGHT...
Create: C:\Users\mathieupauly\AppData\Local\Temp\db...
Create: C:\Users\mathieupauly\AppData\Local\Temp\include...
Create: C:\Users\mathieupauly\AppData\Local\Temp\javafx-src.zip...
Create: C:\Users\mathieupauly\AppData\Local\Temp\jre...
Create: C:\Users\mathieupauly\AppData\Local\Temp\lib...
Create: C:\Users\mathieupauly\AppData\Local\Temp\LICENSE...
Create: C:\Users\mathieupauly\AppData\Local\Temp\README.html...
Create: C:\Users\mathieupauly\AppData\Local\Temp\release...
Create: C:\Users\mathieupauly\AppData\Local\Temp\src.zip...
Create: C:\Users\mathieupauly\AppData\Local\Temp\THIRDPARTYLICENSEREADME-JAVAFX.txt...
Create: C:\Users\mathieupauly\AppData\Local\Temp\THIRDPARTYLICENSEREADME.txt...

Exercice 12 : pour le lecteur, on change new PrintStream("list.csv") à System.out ; pour l'écrivain, on substitue new FileReader("list.csv") par new InputStreamReader(System.in).

package fr.arolla.java8esgi.basicio.ex12;

import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class ContentReader {
    public static void main(String[] args) throws IOException {
        String source = args[0];
        System.out.println("Copy from: " + source);
        DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get(source));
        for (Path toCopy : stream) {
            Path fileName = toCopy.getFileName();
            if (Files.isDirectory(toCopy)) {
                System.out.println(fileName + ";" + "directory");
            } else if (Files.isRegularFile(toCopy)) {
                System.out.println(fileName + ";" + "file");
            } else {
                System.out.println(fileName + ";" + "unknown");
            }
        }
        stream.close();
    }
}
package fr.arolla.java8esgi.basicio.ex12;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class ContentWriter {
    public static void main(String[] args) throws IOException {
        String destination = args[0];
        System.out.println("Copy to: " + destination);
        Path destinationDirectory = Paths.get(destination);
        BufferedReader listing = new BufferedReader(new InputStreamReader(System.in));
        String line;
        while ((line = listing.readLine()) != null) {
            String[] columns = line.split(";");
            String filename = columns[0];
            String type = columns[1];
            Path toCreate = destinationDirectory.resolve(filename);
            System.out.println("Create: " + toCreate + "...");
            switch (type) {
                case "directory":
                    Files.createDirectory(toCreate);
                    break;
                case "file":
                    Files.createFile(toCreate);
                    break;
                default:
                    System.err.println("Cannot process " + toCreate + " (unknown type)");
                    break;
            }
        }
        listing.close();
    }
}

Une erreur courante de programmation consiste à oublier de libérer les ressources (BufferedReader, BufferedOutputStream, DirectoryStream, etc). Dans le pire, des cas cela aboutit à des fuites mémoires.

3.4.12 Sources

https://docs.oracle.com/javase/tutorial/essential/io/

4 Les exceptions

Dans le chapitre d'introduction aux entrées/sorties nous avons utilisé des méthodes pouvant générer des exceptions.

void touchLock() {
    FileOutputStream stream = new FileOutputStream("lock.txt"); // unreported exception java.io.FileNotFoundException;
                                                                // must be caught or declared to be thrown
    stream.close();
}

Ce code ne compile pas.

Le contructeur de FileOutputStream déclare dans la section Throws de sa Javadoc qu'une FileNotFoundException est levée si le fichier existe mais correspond à un répertoire ou bien quand le fichier n'existe pas et ne peut pas être créé, etc.

Le contructeur est déclaré comme comme cela dans le JDK :

public FileOutputStream(String name) throws FileNotFoundException {
   ...
}

Quand on appelle une méthode qui peut lever des exceptions, la méthode appelante doit ou bien :

4.1 Throw et throws

Ce code ne compile pas :

public static void main(String[] args) {
    throw new Exception(); // unreported exception
}

On peut corriger en déclarant l'exception.

public static void main(String[] args) throws Exception {
    throw new Exception();
}

Une exception est un objet comme un autre. Ce qui le distingue d'un autre objet classique est qu'il peut être lancé (throw) au contexte appelant sans passer par le flot d'exécution habituel (if, return, etc.). L'expression new Exception() est un Object comme un autre. On pourrait donc écrire :

public static void main(String[] args) throws Exception {
    Exception e = new Exception();
    throw e;
}

Le code suivant ne compile pas car le 2e println n'est jamais atteignable :

public static void main(String[] args) throws Exception {
    System.out.println("before");
    throw new Exception();
    System.out.println("before"); // unreachable statement
}

En effet, l'exception levée entre les 2 lignes de println interrompt la méthode main(String[] args).

L'erreur de compilation est similaire à l'erreur produite avec un return :

public static void main(String[] args) throws Exception {
    System.out.println("before");
    return;
    System.out.println("after"); // unreachable statement
}

En revanche, pour l'exposer on peut en quelque sorte tromper le compilateur Java pour corriger l'erreur de compilation :

public static void main(String[] args) throws Exception {
    System.out.println("before");
    if (true) {
        throw new Exception();
    }
    System.out.println("after"); // OK
}

Même si ce code est sémantiquement équivalent (if (true) foo(); est équivalent à foo();) le compilateur considère uniquement la forme du if et pas la condition.

Le programme pourra afficher :

before
Exception in thread "main" java.lang.Exception
    at fr.arolla.java8esgi.exceptions.Demo.main(Demo.java:7)

Mais également :

Exception in thread "main" java.lang.Exception
    at fr.arolla.java8esgi.exceptions.Demo.main(Demo.java:7)
before

Note secondaire : on peut se demander pourquoi les messages affichés sur la sortie standard sont comme dilués dans la trace. La raison est que la méthode printStackTrace() affiche sur la sortie erreur qui n'est pas bufferisée (sinon l'erreur risquerait de n'être jamais affichée). Une donnée écrite dans un buffer ne sera effectivement écrite que lorsque nécessaire. L'ordre d'affichage n'est pas garanti entre le flux de sortie standard (out) et le flux d'erreur standard (err).

On réécrit (on dit aussi "refactor") le programme on faisant une extraction d'une méthode foo :

public static void main(String[] args) throws Exception {
    System.out.println("before");
    foo();
    System.out.println("after");
}

private static void foo() throws Exception {
    if (true) {
        throw new Exception();
    }
}

La méthode appelante (main) et appelée (foo) ont maintenant la même clause throws. Dans ce cas, la clause throws est virale. Elle a infecté celui qui l'appelait. En choisissant la même tactique (déclaration avec throws), le dernier appelant (main(String[] args)) est aussi en quelque sorte infecté. Si une exception est levé la JVM s'arretera.

Quand une exception est levée, le flot d'exécution normal est interrompu. La JVM dépile la fonction en cours, puis sa fonction appelante et ainsi de suite jusqu'à la méthode main. En bref, le programme est interrompu. Le programme stoppe et la JVM affiche la pile d'exécution ayant causé l'exception.

Une réécriture doit s'appliquer à laisser inchangé le comportement pour l'utilisateur. Mais certains aspect techniques peuvent changer comme la "stack trace" :

before
Exception in thread "main" java.lang.Exception
    at fr.arolla.java8esgi.exceptions.Demo.foo(Demo.java:12)
    at fr.arolla.java8esgi.exceptions.Demo.main(Demo.java:6)

Le programme affiche toujours "before" (comportement inchangé) mais la "stack trace" est maintenant composée de 2 lignes.

Continuons l'expérience :

before
Exception in thread "main" java.lang.Exception
    at fr.arolla.java8esgi.exceptions.Demo.bar(Demo.java:16)
    at fr.arolla.java8esgi.exceptions.Demo.foo(Demo.java:11)
    at fr.arolla.java8esgi.exceptions.Demo.main(Demo.java:6)

Remarquons qu'en première ligne de la "stack trace" est toujours la fonction la plus profonde dans la pile d'exécution ("stack"). Cette ligne correspond toujours à l'endroit qui a levé l'exception.

Analysons la première ligne : fr.arolla.java8esgi.exceptions.Demo.bar(Demo.java:16).

Exemple Description
fr.arolla.java8esgi.exceptions package
Demo class
bar method
Demo.java source file
16 line number

4.2 Try et Catch

Quand dans une fonction on appelle du code qui peut lever une exception (directement avec throw ou bien indirectement en appelant une méthode qui déclare throws) on peut déclarer ou gérer.

On a vu comment déclarer avec throws. On gère avec try et catch.

public static void main(String[] args) {
    try {
        if (true) throw new Exception("1");
        if (true) throw new Exception("2");
        if (true) throw new Exception("3");
        System.out.println("OK");
    } catch (Exception e) {
        e.printStackTrace();
    }
    System.out.println("Life continues");
}

Ceci affichera probablement :

java.lang.Exception: 1
    at fr.arolla.java8esgi.exceptions.Demo.main(Demo.java:6)
Life continues

Il faut noter que "OK" n'est pas affiché car le bloc catch a pris la main. Le bloc try a été interrompu définitivement. Mais le message "Life continues" est affiché comme si rien ne s'était passé.

On peut éviter de remonter toute la pile d'appel et d'interrompre le programme. Il est parfois préférable de traiter l'exception localement de sorte que le programme continue quand même. On dira d'un tel programme qu'il fait de son mieux (best-effort).

Pour éviter qu'une exception ne soit propagée, on doit l'intercepter. Le code qui produit l'exception doit être embarqué dans un bloc try {}. Celui-ci doit-être suivi d'un bloc catch {} (ou finally {} mais nous verrons cela après).

Si une exception est levée dans le try, le gestionnaire catch sera exécuté.

4.3 Bloc et portée

Attention avec la portée des variables. Le code suivant ne compile pas :

public static void main(String[] args) {
    try {
        boolean ok = false;
        if (true) throw new Exception("1");
        if (true) throw new Exception("2");
        if (true) throw new Exception("3");
    } catch (Exception e) {
        e.printStackTrace();
    }
    System.out.println(ok); // cannot find symbol "ok"
}

C'est le même phénomène que

public static void main(String[] args) {
    {
        String a = "";
    }
    System.out.println(a); // cannot find symbol "a"
}

Les variables définies dans un bloc sont locales à ce bloc et ne peuvent être référencée à l'extérieur. On parle de portée lexicale.

Quand un calcul dépend d'une méthode pouvant lever une exception et qu'il est géré (try), il est recommandé aussi traiter ce calcul dans le bloc try.

public static void main(String[] args) {
    try {
        boolean ok = getInfo();
        if (ok) {
            System.out.println("hello");
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

private static boolean getInfo() throws Exception {
    if (true) throw new Exception("1");
    return false;
}

et non pas :

public static void main(String[] args) {
    boolean ok = false;
    try {
        ok = getInfo();
    } catch (Exception e) {
        e.printStackTrace();
    }
    if (ok) {
        System.out.println("hello");
    }
}

private static boolean getInfo() throws Exception {
    if (true) throw new Exception("1");
    return false;
}

4.4 Exceptions maisons

Il n'est pas recommandé de lever Exception directement car cela est trop générique. On perd la possibilité de filtrer statiquement par type d'exception. On devrait utiliser des exceptions spécifiques.

Mais ce code ne compile pas.

public class UserDefinedExceptions {
    public static void main(String[] args) {
        throw new CustomerNotFoundException(); // incompatible types:
        // fr.arolla.java8esgi.exceptions.CustomerNotFoundException 
        // cannot be converted to java.lang.Throwable
    }
}

class CustomerNotFoundException {

}

C'est une erreur similaire à :

public static void main(String[] args) {
    throw new Object(); // incompatible types
}

La solution consiste à faire hériter notre exception maison de Exception :

public class UserDefinedExceptions {
    public static void main(String[] args) throws CustomerNotFoundException {
        throw new CustomerNotFoundException();
    }
}

class CustomerNotFoundException extends Exception {

}

Comme CustomerNotFoundException est un sous-type de Exception on peut déclarer que notre main peut lancer une exception plus générique.

public static void main(String[] args) throws CustomerNotFoundException {
    throw new CustomerNotFoundException();
}

On pourra également décider de traiter l'erreur avec un try/catch.

public static void main(String[] args) {
    try {
        throw new CustomerNotFoundException();
    } catch (CustomerNotFoundException e) {
        e.printStackTrace();
    }
}

On pourra attraper plus d'exception :

public static void main(String[] args) {
    try {
        throw new CustomerNotFoundException();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

Si notre code peut lancer plusieurs types d'exceptions (c'est quand même le but quand on est capable de filtrer).

public class UserDefinedExceptions {
    public static void main(String[] args) {
        try {
            if (true) throw new CustomerNotFoundException();
            if (true) throw new DuplicateCustomerException();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class CustomerNotFoundException extends Exception {

}

class DuplicateCustomerException extends Exception {

}

Dans ce cas c'est la CustomerNotFoundException qui est lancée et gérée par le bloc catch.

Mais en inversant :

try {
    if (true) throw new DuplicateCustomerException();
    if (true) throw new CustomerNotFoundException();
} catch (Exception e) {
    e.printStackTrace();
}

Ici c'est DuplicateCustomerException qui est lancée et rattrapée.

Si on ne souhaite pas traiter toute les exceptions on pourra ajouter plusieurs gestionnaire spécifiques plutôt qu'un seul générique.

public static void main(String[] args) {
    try {
        if (true) throw new DuplicateCustomerException();
        if (true) throw new CustomerNotFoundException();
    } catch (DuplicateCustomerException e) {
        e.printStackTrace();
    } catch (CustomerNotFoundException e) {
        e.printStackTrace();
    }
}

Mais attention à l'ordre des gestionnaires. L'exemple suivant ne compile pas car le premier bloc va traiter toute les exceptions laissant le 2e gestionnaire inutile.

public static void main(String[] args) {
    try {
        if (true) throw new DuplicateCustomerException();
        if (true) throw new CustomerNotFoundException();
    } catch (Exception e) { 
        System.err.println("1");
    } catch (CustomerNotFoundException e) { // exception CustomerNotFoundException 
                                            // has already been caught
        System.err.println("2");
    }
}

En revanche avoir un catch qui attrape les exceptions des blocs précédents n'est pas un problème :

public static void main(String[] args) {
    try {
        if (true) throw new DuplicateCustomerException();
        if (true) throw new CustomerNotFoundException();
    } catch (DuplicateCustomerException e) {
        System.err.println("1");
    } catch (Exception e) {
        System.err.println("2");
    }
}

4.5 Multicatch

Notons que pour plus de lisibilité, Java 7 a ajouté une syntaxe pour les catch multiples :

public static void main(String[] args) {
    try {
        if (true) throw new DuplicateCustomerException();
        if (true) throw new CustomerNotFoundException();
    } catch (DuplicateCustomerException | CustomerNotFoundException e) {
        System.err.println("1");
    }
}

On peut tout à fait relancer une exeption qu'on a attrapée (le code qui suit ne compile pas pour une autre raison).

public static void main(String[] args) {
    try {
        if (true) throw new DuplicateCustomerException();
        if (true) throw new CustomerNotFoundException();
    } catch (DuplicateCustomerException | CustomerNotFoundException ex) {
        throw e; // unreported exception CustomerNotFoundException; 
                 // must be caught or declared to be thrown
    }
}

À ce stade, une question nous brule tous les lèvres : quel est le type de ex ?

Son type est le dénominateur commun entre DuplicateCustomerException et CustomerNotFoundException : le super type commun le plus proche. Dans notre cas c'est Exception.

public static void main(String[] args) throws Exception {
    try {
        if (true) throw new DuplicateCustomerException();
        if (true) throw new CustomerNotFoundException();
    } catch (DuplicateCustomerException | CustomerNotFoundException ex) {
        throw e;
    }
}

Pourtant le compilateur arrive aussi à calculer les types spécifique qui pourront être levés :

public static void main(String[] args) throws DuplicateCustomerException, CustomerNotFoundException {
    try {
        if (true) throw new DuplicateCustomerException();
        if (true) throw new CustomerNotFoundException();
    } catch (DuplicateCustomerException | CustomerNotFoundException e) {
        throw e;
    }
}

4.6  Chaînage

Il est souvent souhaitable d'encapsuler des exceptions dans d'autre exceptions plus abstraites. L'exception encapsulée est nommée « cause ». Dans la plupart des cas, les exceptions sont chaînées entre elles.

public static void main(String[] args) throws Exception {
    try {
        if (true) throw new CustomerNotFoundException(); // line 6
    } catch (CustomerNotFoundException ex) {
        throw new Exception(ex); // line 8
    }
}

L'expression new Exception(e) construit une nouvelle exception sur sa cause ex.

Le programme affichera une trace contenant l'exception cause :

Exception in thread "main" java.lang.Exception: fr.arolla.java8esgi.exceptions.demo.CustomerNotFoundException
    at fr.arolla.java8esgi.exceptions.demo.UserDefinedExceptions.main(UserDefinedExceptions.java:8)
Caused by: fr.arolla.java8esgi.exceptions.demo.CustomerNotFoundException
    at fr.arolla.java8esgi.exceptions.demo.UserDefinedExceptions.main(UserDefinedExceptions.java:6)

En revanche, en ne construisant pas une nouvelle exception on perd le contexte de capture :

public static void main(String[] args) throws Exception {
    try {
        if (true) throw new CustomerNotFoundException(); // line 6
    } catch (CustomerNotFoundException ex) {
        throw ex;
    }
}
Exception in thread "main" fr.arolla.java8esgi.exceptions.demo.CustomerNotFoundException
    at fr.arolla.java8esgi.exceptions.demo.UserDefinedExceptions.main(UserDefinedExceptions.java:6)

C'est une bonne pratique d'encapsuler une exception attrapée plutôt que de relancer la relancer ("rethrow").

Pour combiner l'encapsulation et la généricité des exceptions on écrira donc :

public class UserDefinedExceptions {
    public static void main(String[] args) throws Exception {
        try {
            if (true) throw new CustomerNotFoundException();
        } catch (CustomerNotFoundException ex) {
            throw new MainException(ex); // constructor MainException cannot be applied
        }
    }
}

class MainException extends Exception {

}

Attention ce code ne compile pas car MainException ne définit aucun contructeur à un argument.

Corrigeons :

class MainException extends Exception {
    public MainException(Exception ex) {

    }
}

Cependant il manque quelque chose. En effet, la trace n'affiche plus la cause :

Exception in thread "main" fr.arolla.java8esgi.exceptions.demo.MainException
    at fr.arolla.java8esgi.exceptions.demo.UserDefinedExceptions.main(UserDefinedExceptions.java:8)

Il faut appeler explicitement le super constructeur de Exception pour bénéficier du chaînage :

class MainException extends Exception {
    public MainException(Exception ex) {
        super(ex);
    }
}

La trace indique maintenant l'exception de plus au niveau ainsi que sa cause :

Exception in thread "main" fr.arolla.java8esgi.exceptions.demo.MainException: fr.arolla.java8esgi.exceptions.demo.CustomerNotFoundException
    at fr.arolla.java8esgi.exceptions.demo.UserDefinedExceptions.main(UserDefinedExceptions.java:8)
Caused by: fr.arolla.java8esgi.exceptions.demo.CustomerNotFoundException
    at fr.arolla.java8esgi.exceptions.demo.UserDefinedExceptions.main(UserDefinedExceptions.java:6)

Bien évidement la chaîne pourra comporter autant d'exception que l'on veut (ici 2) :

public static void main(String[] args) throws Exception {
    try {
        activateCustomer();
    } catch (CustomerNotFoundException ex) {
        throw new MainException(ex);
    }
}

private static void activateCustomer() throws CustomerNotFoundException {
    try {
        throw new NoSuchRecordException();
    } catch (NoSuchRecordException e) {
        throw new CustomerNotFoundException(e);
    }
}
Exception in thread "main" fr.arolla.java8esgi.exceptions.demo.MainException: fr.arolla.java8esgi.exceptions.demo.CustomerNotFoundException: fr.arolla.java8esgi.exceptions.demo.NoSuchRecordException
    at fr.arolla.java8esgi.exceptions.demo.UserDefinedExceptions.main(UserDefinedExceptions.java:8)
Caused by: fr.arolla.java8esgi.exceptions.demo.CustomerNotFoundException: fr.arolla.java8esgi.exceptions.demo.NoSuchRecordException
    at fr.arolla.java8esgi.exceptions.demo.UserDefinedExceptions.activateCustomer(UserDefinedExceptions.java:16)
    at fr.arolla.java8esgi.exceptions.demo.UserDefinedExceptions.main(UserDefinedExceptions.java:6)
Caused by: fr.arolla.java8esgi.exceptions.demo.NoSuchRecordException
    at fr.arolla.java8esgi.exceptions.demo.UserDefinedExceptions.activateCustomer(UserDefinedExceptions.java:14)
    ... 1 more

Comme on peut s'y attendre, l'instruction throw entraine les mêmes contraintes (vis-à-vis de l'appelant) que l'appel d'une méthode qui déclare lancer (throws) une exception.

4.7 Quelques exceptions communes

4.7.1 NullPointerException

C'est l'erreur qui représente probablement au moins 50% des bugs des applications Java.

package fr.arolla.java8esgi.exceptions;

public class NullPointerDemo {
    public static void main(String[] args) {
        Object o = null;
        System.out.println(o.hashCode());
    }
}

Le programme termine en erreur avec la sortie suivante :

Exception in thread "main" java.lang.NullPointerException
    at fr.arolla.java8esgi.exceptions.NullPointerDemo.main(NullPointerDemo.java:6)

4.7.2 ArrayIndexOutOfBoundsException

package fr.arolla.java8esgi.exceptions;

public class ArrayExceptions {
    public static void main(String[] args) {
        int[] values = new int[0];
        System.out.println(values[0]);
    }
}

Le programme termine en erreur avec cette trace de l'API du JDK :

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
    at fr.arolla.java8esgi.exceptions.ArrayExceptions.main(ArrayExceptions.java:6)

4.7.3 IllegalArgumentException

L'API du JDK lance cette exception lorsque le programmeur invoque une méthode avec un paramètre incorrect. Le développeur d'application est aussi encouragé à lancer cette exception lorsque un paramètre est invalide.

Par exemple, l'instruction Integer.parseInt("te"); lance une NumberFormatException qui est une spécialisation de IllegalArgumentException.

4.7.4 StackOverflowError

package fr.arolla.java8esgi.exceptions;

public class StackOverflowDemo {
    public static void main(String[] args) {
        foo();
    }

    private static void foo() {
        foo();
    }
}

Ce programme terminera aussi en erreur :

Exception in thread "main" java.lang.StackOverflowError
    at fr.arolla.java8esgi.exceptions.StackOverflowDemo.foo(StackOverflowDemo.java:9)
    at fr.arolla.java8esgi.exceptions.StackOverflowDemo.foo(StackOverflowDemo.java:9)
    at fr.arolla.java8esgi.exceptions.StackOverflowDemo.foo(StackOverflowDemo.java:9)
        ...

4.8 Intérêt des exceptions

Les exceptions résolvent un problème dans l'écriture de programmes : le mélange de la logique principale et de la gestion d'erreurs. Les exceptions sont un moyen de séparer ces 2 considérations.

Imaginons en effet le programme très simple (pas de règle logique => pas de if), qui exécute en 3 actions en séquence :

action 1
action 2
action 3

Tout programme devrait traiter les cas d'erreurs. Traditionnellement (par ex. en C) on utilise des code erreurs retournés par les fonctions appelées.

essayer l'action 1
if (l'action 1 est succès) {
   essayer l'action 2
   if (l'action 2 est succès) {
      essayer l'action 3
      if (l'action 3 est succès) {
         ...
      } else {
         traiter l'erreur 3
      }
   } else {
     traiter l'erreur 2
   }
} else {
  traiter l'erreur 1
}

Si notre programme était plus complexe qu'une séquence, les if codant le métier et les if traitant des erreurs seraient mélangés et donnerait un code encore plus compliqué que celui-ci.

De plus, le mécanisme de traçage permet de montrer la source d'une erreur. Dans de nombreux langages, trouver la ligne responsable d'une erreur à l'exécution est une tâche ardue.

4.9 Catégories d'exceptions

Le compilateur et l'environnement d'exécution distinguent 3 catégories : les Exception, les RuntimeException et les Error. Les Exception (et les classes filles, sauf RuntimeException) que nous avons vu jusqu'ici obligent le programmeur à (1) propager ou à (2) traiter. En revanche, il existe une catégorie spéciale d'exception qui n'obligent pas le programmeur à quoi que ce soit.

Par défaut donc les Exception sont vérifiées (checked). Parmis elles, les RuntimeException sont exemptées de vérification (unchecked).

Ainsi, on pourra compiler le code suivant :

void fail() {
    throw new RuntimeException();
}

De la même manière, on peut compiler :

void checkNotNull(Object a) {
    if (a == null) {
       throw new NullPointerException();
    }
}

Car NullPointerException est une classe dérivée de RuntimeException.

Notons que bien qu'exempté de l'obligation, il est légal, mais déconseillé de déclarer et surtout de traiter une exception non vérifiée.

void test(String text) {
    try {
        checkNotNull(text);
    } catch (NullPointerException e) { // à éviter !
        // ...
    }
}

void checkNotNull(Object a) throws NullPointerException {
    if (a == null) {
       throw new NullPointerException();
    }
}

Dans le cas général, il est déconseillé de traiter des exceptions non vérifiées car se sont des erreurs grave de programmation. Dans la plupart des cas, il est préférable que le programme ou le composant s'arrète plutôt qu'il soit dans un état incontrolable. Le programmeur devrait toujours s'assurer (par ex. à l'aide d'un if) qu'aucune "runtime" ne soit levée.

class A {
    void foo(String text) {
        if (text != null) { // on contrôle la situation
            test(text);
        }
    }
}

class B {
    public void test(String text) {
        checkNotNull(text);
    }

    private void checkNotNull(Object a) throws NullPointerException {
        if (a == null) {
           throw new NullPointerException();
        }
    }
}

En revanche, les erreurs vérifiées correspondent à des erreurs métiers (par ex. le possesseur d'une carte bleue est en découvert au moment d'un retrait) ou du à l'environnement (fichier inexistant, serveur injoignable, etc).

Les Error pour finir sont lancées par la JVM elle même. Elle ne devraient pas être traitées par le programmeur d'application.

4.10 Exercice 1

Écrire une méthode qui prend en paramètres 2 objets non null et retourne la somme de leur hashCode(). Quelles exceptions devrait-on lancer si un des 2 paramètres est null ?

4.11 Exercice 2

  1. Écrire un programme qui affiche le titre entier du livre dont un mot-clé de recherche a été passé en argument.
  2. Quand aucun livre n'est trouvé, lancer une BookNotFoundException.
  3. Quand plus d'un livre est trouvé, lancer une AmbiguousBookException.

Le fichier books.csv contiendra par exemple :

1984
Le meilleur des mondes
La guerre des mondes

Exemple de session :

$ java BookPrice "0000"
Exception in thread "main" BookNotFoundException: no book matches "0000"

$ java BookPrice "84"
1984

$ java BookPrice "Le"
Le meilleur des mondes

$ java BookPrice "mondes"
Exception in thread "main" AmbiguousBookException: 2 books matches "mondes"

4.12 Exercice 3

Modifier le programme précédent de sorte d'afficher le prix du livre quand la recherche est fructueuse.

Le programme se comportera de la façon suivante :

$ cat books.csv
1984;59.99 EUR
Le Meilleur des Mondes;49.99 EUR
La Guerre des Mondes;69.99 EUR

$ java BookPrice "84"
59.99 EUR

$ java BookPrice "monde"
Recherche ambigüe : 2 résultats trouvés ! 

4.13 Exercice 4

Modifier le programme précédent pour rendre le programme interactif. Le programme ne prend plus d'argument mais lit sur son entrée standard.

De plus, le programme ne doit pas s'arréter quand une recherche donne 0 ou plus de 2 résultats.

Pour terminer le programme on utilisera le pseudo caractère de fin de fichier (EOF) qu'on écrit avec CTRL-D.

Exemple de session :

$ java BookPrice
> monde
Recherche ambigüe : 2 résultats trouvés !
> 84 
59.99 EUR
> ^D

4.14 Exercice 5

Ecrire un ensemble de programmes qui simule une gestion naïve de comptes bancaires. Le système doit gérer au moins les 4 fonctions suivantes :

Exemple de session :

$ java MakeDeposit 200.00 Pauly
Exception in thread "main" AccountNotFoundException: ...

$ java CreateAccount Pauly
OK

$ java PerformContactlessPayment 19.00 Pauly
OK

$ java PerformPayment 100.00 Pauly
OK

$ java PerformPayment 300.00 Pauly
Exception in thread "main" TooLowBalanceException...

Remarques :

Le code Java devra être composé de plusieurs classes (par ex. Bank, Amount, Payment, Deposit, Balance, etc.).

On utilisera la classe LocalDateTime pour vérifier de solde glissant sur un jour/semaine.

4.15 Finally

Jusqu'ici on a traité les exceptions de manière plutôt naïve. Que se passe t'il quand on ouvre un fichier (sans erreur) mais qu'une exception survient lors de l'écriture ?

FileOutputStream fileOutputStream = null;
try {
    fileOutputStream = new FileOutputStream("example.txt");
    if (true) throw new IOException(""); // simule un write en erreur
    System.err.println("Close");
    fileOutputStream.close();
} catch (IOException e) {
    e.printStackTrace();
}
System.err.println("End");
java.io.IOException: 
    at fr.arolla.java8esgi.exceptions.TryFinally.main(TryFinally.java:11)
End

Dans ce cas close() n'est jamais appelée !

Pour être sûr de libérer les ressources à tous les coups, il faut utiliser un bloc finally :

FileOutputStream fileOutputStream = null;
try {
    fileOutputStream = new FileOutputStream("example.txt");
    if (true) throw new IOException("");
} catch (IOException e) {
    e.printStackTrace();
} finally {
    System.err.println("Close");
    if (fileOutputStream != null) {
        fileOutputStream.close();
    }
}
System.err.println("End");
java.io.IOException: 
    at fr.arolla.java8esgi.exceptions.TryFinally.main(TryFinally.java:11)
Close
End

Le bloc catch est optionnel :

try {
    
} finally {
    
}

Une fois entré dans le try, le bloc finally est toujours appelé, même en cas de return. Le return sera exécuté mais après que le finally termine. Ainsi,

public static void main(String[] args) {
    int foo = foo();
    System.err.println("return " + foo);
}

private static int foo() {
    try {
        System.err.println("try");
        return 1;
    } finally {
        System.err.println("finally");
    }
}

Affichera :

try
finally
return 1

En revanche, en cas de return dans le finally, celui-ci prendra la main.

public static void main(String[] args) {
    int foo = foo();
    System.err.println("return " + foo);
}

private static int foo() {
    try {
        System.err.println("try");
        return 1;
    } finally {
        System.err.println("finally");
        return 2;
    }
}
try
finally
return 2

Notons que la fermeture du fichier pouvant lever une exception il faut ou bien la déclarer ou bien la gérer.

On peut donc très facilement aboutir à cet idiome de programmation simplement pour ouvrir et fermer un fichier...

FileOutputStream fileOutputStream = null;
try {
    fileOutputStream = new FileOutputStream("example.txt");
    if (true) throw new IOException("");
} catch (IOException e) {
    e.printStackTrace();
} finally {
    System.err.println("Close");
    if (fileOutputStream != null) {
        try {
            fileOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
System.err.println("End");

4.16 Try with

Pour gérer les ressources de manière plus simple et sécurisé on utilisera une syntaxe apportée par Java 7 :

try (FileOutputStream fileOutputStream = new FileOutputStream("example.txt")) {
    fileOutputStream.write(0);
} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

La syntaxe try (...) {} est un bloc try dans lequel on déclare 1 ou plusieurs ressources. Une ressource qui est un objet qui doit être clos dès qu'on a fini de travailler avec. Contrairement à un bloc try classique celui-ci ferme chaque ressource déclarée dans l'entête (entre parenthèses) à la fin du bloc d'instructions. L'entête attend des ressources type java.lang.AutoCloseable.

Notons que l'entête ("try-with-resource") appelle implicitement close() qui peut déclarer des exceptions.

Dans le cas ou une exception surviendrait dans le bloc try, une exception qui serait levée au close() serait supprimée. On utilisera la méthode Throwable.getSuppressed() sur l'exception rattrapée afin de récupérer celles qui ont été supprimées (pour permettre à celle-ci d'être lancée).

L'ordre d'exécution des close() est l'inverse le l'ordre de leur création.

5 Les Generics

5.1 Algorithme

5.1.1 Exercice 1

Développer une classe qui se comporte comme suit :

IntegerContainer empty = new IntegerContainer();
System.out.println(empty); // ""

IntegerContainer b = new IntegerContainer(42);
System.out.println(b); // "42"

IntegerContainer z = new IntegerContainer(0);
System.out.println(z); // "0"

IntegerContainer n = new IntegerContainer(null); // IllegalArgumentException

5.1.2 Exercice 2

Ajouter une méthode first() :

IntegerContainer empty = new IntegerContainer();
System.out.println(empty.first()); // IllegalStateException: empty container

IntegerContainer b = new IntegerContainer(42);
System.out.println(b.first()); // 42

5.1.3 Exercice 3

Modifier la classe IntegerContainer de sorte de prendre un nombre arbitraire d'entiers. Ajouter une méthode countEqual(Integer value) qui compte le nombre d'entiers égaux au paramètre.

IntegerContainer a = new IntegerContainer(1020, 10);
System.out.println(a.countEqual(10)); // 2
System.out.println(a); // {10, 20, 10}

5.1.4 Exercice 4

Développer une classe DoubleContainer qui prend un nombre arbitraire de nombres réels. Implémenter la méthode toString(), first() et countEqual(Double value).

5.1.5 Exercice 5

Mettre en commun le code de IntegerContainer et DoubleContainer en utilisant le type commun (Object).

5.2 Paramètre de valeur

Quand 2 méthodes définissent un code similaire on peut paramétrer la méthode.

Par exemple :

double bookDiscount() {
    return applyDiscount ? 0.8 : 1;
}

double postcardDiscount() {
    return applyDiscount ? 0.9 : 1;
}

Peut se mettre en commun comme suit :

double bookDiscount() {
    return discount(0.8);
}

double postcardDiscount() {
    return discount(0.9);
}

double discount(double discount) {
    return applyDiscount ? discount : 1;
}

La méthode discount(double) est paramétrée pour mettre en commun le code commun à bookDiscount() et postcardDiscount().

Pour mettre en commun une constante (ou une variable),

void foo() {
    System.out.println(42);
}

on introduit un paramètre :

void foo(int value) {
    System.out.println(value);
}

5.3 Paramètre de type

De manière similaire quand on veut mettre en commun non pas une valeur mais un type, on utilise un paramètre de type.

On transforme les deux classes :

class IntegerContainer {
    ...
    Integer first() {
        return values[0];
    }
}

class DoubleContainer {
    ...
    Double first() {
        return values[0];
    }
}

En une seule classe paramétrée :

class GenericContainer<T> {
    ...
    T first() {
        return values[0];
    }
}

Pour utiliser cette classe paramétrée :

GenericContainer<Integer> ic = new GenericContainer<Integer>();
GenericContainer<Double> dc = new GenericContainer<Double>();

On peut également déclarer un paramètre de type sur une méthode :

class Foo {
    <T> T get(T value) {
        return null;
    }
}

Pour appeler la méthode on doit fournir un argument (ici Integer) :

Foo foo = new Foo();
Integer x = foo.<Integer>get(new Integer(42));

La syntaxe dans sa forme générale est particulièrement verbeuse. En revanche, grâce à l'inférence de type améliorée on pourra écrire en pratique :

Integer x = foo.get(new Integer(42));

Le compilateur a déduit que l'argument de type devait forcément être Integer puisqu'il est spécifié en paramètre.

5.4 Exercice 6

Définir la méthode <T> T failIfNull(T object) qui lance une IllegalArgumentException si l'argument est null ou bien retourne l'argument sinon.

5.5 Exercice 7

Définir une classe CheckedBox qui peut stocker un objet dont le type est paramétré. Le constructeur vide sera utilisé pour créer une boîte vide. Ajouter une méthode get() qui renvoie ledit objet et qui lance une IllegalStateException si la boîte est vide.

CheckedBox<Integer> integerBox = new CheckedBox<>(42); // <=> new CheckedBox<Integer>(42) grâce à l'inférence de type
Integer boxContent = integerBox.get();
System.out.println(boxContent);
CheckedBox<String> stringBox = new CheckedBox<>("hello");
String stringContent = stringBox.get();
System.out.println(stringContent);
CheckedBox<?> emptyBox = new CheckedBox<>();
Object emptyBoxContent = emptyBox.get(); // IllegalStateException: null

5.6 Exercice 8

Créer une classe Pair<T> générique. Elle doit stocker 2 éléments de même type. Redéfinir la méthode toString() et définir la méthode reverse() qui crée une nouvelle paire dont les éléments sont inversés.

Pair<String> pair = new Pair<>("a", "b");
System.out.println(pair); // (a, b)

Pair<String> reversedPair = pair.reverse();
System.out.println(reversedPair); // (b, a)

5.7 Exercice 9

Écrire une classe générique qui stocke 2 objets dont les types sont paramétrés et peuvent être différents.

La syntaxe pour définir 2 paramètres de type T et U sur une classe est :

class Foo<T, U> {
    
}

Voici un exemple d'usage de la classe :

Tuple2<Integer, String> ok = new Tuple2<>(200, "OK");
System.out.println(ok); // (200, OK)

Tuple2<String, Integer> reversedOk = ok.reverse();
System.out.println(reversedOk); // (OK, 200)

Tuple2<Tuple2<Integer, String>, Tuple2<Integer, String>> forbiddenError =
        new Tuple2<>(new Tuple2<>(403, "Forbidden"), new Tuple2<>(500, "Internal Server Error"));
System.out.println(forbiddenError); // ((403, Forbidden), (500, Internal Server Error))

Tuple2<Tuple2<Integer, String>, Tuple2<Integer, String>> errorForbidden =
        forbiddenError.reverse();
System.out.println(errorForbidden); // ((500, Internal Server Error), (403, Forbidden))

5.8 Exercice 10

Ajouter une 2 getters getA() et getB() à la classe Tuple2<T, U>. Puis réécrire la classe Pair à l'aide de la classe Tuple2.

5.9 Exercice 11

Écrire la classe GenericContainer<T> : la version générique de ObjectContainer.

Elle doit implémenter les méthodes String toString(), Object first() et int countEqual(Object element).

Puis, ajouter une méthode GenericContainer<T> repeat(int count) qui a le comportement suivant :

GenericContainer<Integer> a = new GenericContainer<>();
GenericContainer<Integer> ad = a.repeat(2);
System.out.println(ad); // "{}"

GenericContainer<Integer> b = new GenericContainer<>(42);
GenericContainer<Integer> bd = b.repeat(2);
System.out.println(bd); // "{42, 42}"

GenericContainer<Integer> c = new GenericContainer<>(42);
GenericContainer<Integer> cd = c.repeat(3);
System.out.println(cd); // "{42, 42, 42}"

GenericContainer<Integer> d = new GenericContainer<>(42, 100);
GenericContainer<Integer> dd = d.repeat(3);
System.out.println(dd); // "{42, 42, 42, 100, 100, 100}"

GenericContainer<Integer> e = new GenericContainer<>(10, 20, 30);
GenericContainer<Integer> ed = e.repeat(1);
System.out.println(ed); // "{10, 20, 30}"

GenericContainer<Integer> f = new GenericContainer<>(10, 20, 30);
GenericContainer<Integer> fd = f.repeat(0);
System.out.println(fd); // "{}"

5.10 Exercice 12

Ajouter une méthode de fabrique static à la classe Pair<T> qui crée une paire d'éléments identiques.

Pair<Integer> pair = Pair.<Integer>duplicate(1);
System.out.println(pair); // (1, 1)

5.11 Tableaux

Les tableaux sont typés :

int[] intArray = new int[] {10, 20};
double[] doubleArray = new double[] {10_000d, 20_000d};
Object[] objectArray = new Object[] {new Object(), new Integer(10), "hello"};
Person[] personArray = new Person[] {new Person(), new Employee(), null};

Ils peuvent contenir des valeurs (types primitifs) et des références (types classes).

Les tableaux ("array") sont relativement sécurisés : on ne peut pas ajouter par exemple un Integer dans un tableau de String.

String[] stringArray = new String[] { new Integer(10) }; // incompatible types

De la même manière :

String[] stringArray = new String[1];
stringArray[0] = new Integer(10); // incompatible types: 
                                  // Integer cannot be converted to String

Sans surprise, on ne peut affecter un élément d'un tableau de String dans un Integer.

String[] stringArray = new String[1];
Integer i = stringArray[0]; // incompatible types:
                            // String cannot be converted to Integer

En revanche, rien n'empêche de placer dans un tableau de Person un objet Employee :

Person[] personArray = new Person[1];
personArray[0] = new Person();
person[0] = new Employee();

Avec :

class Person {
}

class Employee extends Person {
}

Ou d'affecter un élément dans une référence de type plus général :

Object object = personArray[0];

Cette situation se rapproche de la notion usuelle d'héritage :

Person person = new Person();
person = new Employee();
Object object = person;

Rien n'empêche non plus de créer un alias (c-à-d. une créer une 2ème référence pointant sur le même objet) :

Person[] personArray2 = personArray;

On pourra également créer un alias (c-à-d. une affectation) de tableau avec un type plus général :

Object[] objectArray = personArray;

De ce dernier exemple, on peut tirer la conclusion qu'en Java un tableau de personnes est un tableau d'objets. Ceci parait naturel puisqu'une Person est un Object.

On pourra donc écrire aussi :

Employee[] employeeArray = new Employee[1];
Person[] personArray = employeeArray;

Un Employee étant un sous type de Person, le compilateur Java en déduit qu'un tableau d'Employee sera alors une sorte de sous-type de tableau de Person.

Cette déduction, qu'une relation entre 2 classes (ici extends) se généralise au tableau, s'appelle la « covariance ».

Ainsi, et plus généralement : A extends B implique A[] extends B[]. Bien que le programmeur n'ait pas explicitement écrit A[] extends B[] tout se passe comme si c'était le cas :

println(employeeArray instanceof Person[]); // true

Quand on disait que les tableaux étaient relativement sécurisés, tout est dans le « relativement ». En effet, le typage des tableaux a une faille (de taille).

Revenons à notre exemple d'alias :

Person[] personArray = new Person[1];
Object[] objectArray = personArray;

objectArray[0] = "booom!"; // ArrayStoreException: java.lang.String

Donc le compilateur nous autorise à créer un alias de notre tableau de personnes. Il est donc naturel de pouvoir caser un String dans un tableau d'objets (un String étant un Object). Or ici, le tableau créé est un tableau de personnes. La machine virtuelle connaît (à l'exécution du programme) le type du tableau. Celui-ci est fixé à sa création (new) et n'est pas simplement le dénominateur commun de tous ses éléments.

Par exemple :

Object[] objectArray = new Object[] {new Cat(), new Dog()};

Le type dynamique du tableau est bien Object en non pas Animal.

Ainsi la propriété de covariance pose problème dans certains cas. Mais dans d'autres situations elle peut-être utile :

Person[] personArray = new Person[1];
personArray[0] = new Person();
Object[] objectArray = personArray;
for (int i = 0; i < objectArray.length; i++) {
    System.out.println(objectArray[i]);
}

En extrayant une méthode responsable de l'affichage :

Person[] personArray = new Person[1];
personArray[0] = new Person();
printObjects(personArray);

avec

void printObjects(Object[] objectArray) {
    for (int i = 0; i < objectArray.length; i++) {
        System.out.println(objectArray[i]);
    }
}

On a découplé l'affichage de la façon de créer et d'écrire dans le tableau. La fonction printObjects(Object[]) est maintenant indépendante et pourra être réutilisée dans de nombreuses situations. En effet, cette méthode expose en paramètre seulement Object[] ce qui est très général. Pour rendre notre code réutilisable on cherche toujours à être le plus général possible.

Pour preuve, on pourra sans problème réutiliser cette méthode comme ceci :

BigDecimal[] amounts = new BigDecimal[] {BigDecimal.ZERO, BigDecimal.ONE};
printObjects(amounts);
// 0
// 1

5.12 Generics Java 5

Avant Java 5, le typage des collections n'était pas statiquement vérifié (dynamiquement seulement en castant ce qu'on a récupéré de la collection). On écrivait donc :

ArrayList numbers = new ArrayList();

Ou bien en profitant du polymorphisme de List :

List numbers = new ArrayList();

On pouvait ajouter n'importe quoi dans la collection :

numbers.add(1);
numbers.add("hello");

Au moment de récupérer un élément, on n'avait qu'une seule garantie : que l'élément est un objet.

Object object = numbers.get(0);

Quand on savait que la collection contenait des Integer il fallait caster :

Integer number = (Integer) numbers.get(0);
Integer number1 = (Integer) numbers.get(1); // ClassCastException

Depuis la version 5 du langage, on peut spécifier le type des éléments (grâce aux nouveau mécanisme de paramètre de types) :

List<Integer> numbers = new ArrayList<Integer>();

Pour récupérer un élément, nul besoin de cast, le type est garanti :

Integer number = numbers.get(0);

Maintenant, le compilateur interdit d'ajouter un élément du mauvais type :

numbers.add("fail"); // no suitable method found for add(String)
// method java.util.Collection.add(java.lang.Integer) is not applicable
// (argument mismatch; java.lang.String cannot be converted to java.lang.Integer)

On peut donc comme avec les tableaux ajouter et récupérer des éléments.

En revanche le mécanisme de création d'alias polymorphe n'est plus permis :

List<Integer> list = new ArrayList<Integer>();
List<Object> objects = list; // incompatible types: 
// List<Integer> cannot be converted to List<Object>

Les collections génériques ne sont plus covariantes. Une collection de personnes n'est pas considérée comme étant un sous-type d'une collection d'objets.

5.13 Bornes

On peut attribuer une borne supérieure au type des éléments. Ainsi dans l'exemple qui suit il est autorisé d'effectuer une affectation car Integer extends Object.

List<Integer> integerList = new ArrayList<Integer>();
List<? extends Object> extendsObjects = integerList;

On pourra comme d'habitude ajouter des éléments via la référence integerList :

integerList.add(1);
integerList.add(2);

En revanche, on ne pourra pas appeler la méthode add sur la liste possédant Object comme borne supérieure :

extendsObjects.add(new Object()); // no suitable method found for add(Object)
extendsObjects.add(new Integer(1)); //  no suitable method found for add(Integer)

Quand la borne supérieure est Object on peut lire mais on ne peut pas écrire.

Prenons un exemple avec une borne supérieure mais positionnée à Person.

List<Employee> employeeList = new ArrayList<Employee>();
List<? extends Person> personList = employeeList;

On pourra bien évidement écrire via la 1ère référence.

employeeList.add(new Employee());

En revanche, il est toujours impossible d'écrire via la 2ème référence.

personList.add(new Person()); // no suitable method found for add(Person)

Mais il est toujours possible de lire une Person :

Person person0 = personList.get(0)

5.14 Wildcard

Faisons une petite expérience de pensée. Attention le pseudo-code qui suit n'est pas du Java valide, il permet simplement de mieux se représenter les choses.

Object object = new Object()
Person person = new Person()
Employee employee = new Employee()

object = person
object = employee

person = object // fail
person = employee

employee = object // fail
employee = person // fail

Corsons un peu les choses (toujours en pseudo-code) :

class Foo<T> {
    void foo(T x) {}
}
Foo<Integer> a = new Foo<Integer>();
Foo<?> b = new Foo<Integer>();
Object object = new Object()
? jokerObject

jokerObject = new Object() // fail
Object object = jokerObject // OK
<? extends Object> extendsObject
<? extends Person> extendsPerson
<? extends Employee> extendsEmployee

extendsObject = object // fail
extendsPerson = person // fail
extendsEmployee = employee // fail

On a pas le droit d'écrire (c-à-d. d'affecter) dans une référence typée avec une borne supérieure.

La raison est que le type ? extends Person (où "?" est le type joker) peut-être par exemple un Person, un Employee, ou n'importe quelle autre classe qui étend Person. Comme on peut sous-classer indéfiniment, le type inconnu ? pourra être aussi bas que possible dans la hiérarchie des types.

Pour que extendsObject = object soit possible, il faudrait que toutes les substitution possible du joker (?) soient valides. Ici cela donnerait :

Object o = object
Person p = object
Employee e = object

5.15 Exercice 13

Ajouter à la classe Pair une méthode static sort() qui place toujours à gauche l'élément le plus petit.

Pour cela il va falloir définir une borne au type qui paramètre Pair.

Pair<Integer> sorted = Pair.sort(new Pair<>(2, 1));
System.out.println(sorted); // (1, 2)

Pair<Integer> sorted2 = Pair.sort(new Pair<>(10, 20));
System.out.println(sorted2); // (10, 20)

5.16 Exercice 14

Appeler la méthode Pair.sort() avec new Pair<Long>(1_000_000_000, 2_000_000_000), new Pair<Double>(1_000d, 2_000d). Essayer avec new Pair<String>("abc", "def").

5.17 Exercice 15

On utilise un wildcard quand on a pas besoin d'utiliser le paramètre de type.

Écrire une méthode static void printSum(Pair<? extends Number> pair) qui affiche la somme les 2 éléments de la paire.

Écrire une méthode static CharSequence concatenate(Pair<? extends CharSequence> pair) affiche la concaténation des 2 éléments.

5.18  Exercice 16

Écrire une méthode static <U> Pair<U> fromList(List<? extends U> input) :

Pair<Integer> pair = Pair.fromList(Arrays.asList(1, 2)); // (1, 2)
Pair<String> pair = Pair.fromList(Arrays.asList("abc", "def")); // ("abc", "def")

5.19 Exercice 17

Écrire une méthode void writeToList(List<? super T> output).

List<Object> output = new ArrayList<>();
new Pair<>("hello", "world").writeToList(output);
System.out.println(output); // ("hello", "world")

Créer la hiérarchie de classe suivante : LiveBeing, Animal extends LiveBeing, Cat extends Animal.

Créer une paire de Animal.

Essayer d'appeler writeToList() avec une List<LiveBeing> puis avec une List<Cat>.

5.20 Correction exercices

5.20.1 Exercice 1

On écrit le programme de l'exercice 1 :

public class Main {
    public static void main(String[] args) {
        IntegerContainer empty = new IntegerContainer();
        System.out.println(empty); // ""

        IntegerContainer b = new IntegerContainer(42);
        System.out.println(b); // "42"

        IntegerContainer z = new IntegerContainer(0);
        System.out.println(z); // "0"

        IntegerContainer n = new IntegerContainer(null); // IllegalArgumentException
    }
}

On crée la classe IntegerContainer et les 2 constructeurs pour pouvoir compiler (mais le comportement à l'exécution est encore à travailler) :

public class IntegerContainer {

    public IntegerContainer(Integer value) {

    }

    public IntegerContainer() {

    }
}

On implémente la méthode toString() pour que le println() affiche ce que l'on veut.

public class IntegerContainer {

    private Integer value;

    public IntegerContainer(Integer value) {
        this.value = value;
    }

    public IntegerContainer() {

    }

    @Override
    public String toString() {
        return String.valueOf(value);
    }
}

5.20.2 Exercice 2

public class IntegerContainer {

    private Integer value;

    public IntegerContainer(Integer value) {
        this.value = value;
    }

    public IntegerContainer() {

    }

    @Override
    public String toString() {
        return String.valueOf(value);
    }

    public Integer first() {
        if (value == null) {
            throw new IllegalArgumentException("Empty container");
        }
        return value;
    }
}

5.20.3 Exercice 3

On écrit le programme dans une classe dédiée :

public class Main {
    public static void main(String[] args) {
        IntegerContainer a = new IntegerContainer(10, 20, 10);
        System.out.println(a.countEqual(10)); // 2
        System.out.println(a); // {10, 20, 10}
    }
}

Puis on corrige toutes les erreurs de compilations sans s'occuper pour le moment du comportement à l'exécution :

public class IntegerContainer {

    private Integer value;

    public IntegerContainer(Integer value) {
        this.value = value;
    }

    public IntegerContainer() {

    }

    public IntegerContainer(Integer i, Integer  i1, Integer i2) {

    }

    @Override
    public String toString() {
        return String.valueOf(value);
    }

    public Integer first() {
        if (value == null) {
            throw new IllegalArgumentException("Empty container");
        }
        return value;
    }

    public int countEqual(Integer element) {
        return 0;
    }
}

Remarquer le nouveau constructeur à 3 paramètres : IntegerContainer(Integer i, Integer i1, Integer i2). Du point du vue de celui qui l'appelle, on doit fournir 3 arguments (par ex. new IntegerContainer(10, 20, 10)). Le point de vue du constructeur appelé (qui manipule les paramètres) est le même : 3 paramètres i, i1 et i2.

On va introduire un constructeur prenant un nombre variable de paramètres (on parle d'arité variable). On écrira : IntegerContainer(Integer... values). Le point de vue du constructeur est changé. Il manipule maintenant un tableau de Integer : tout ce passe pour le constructeur comme s'il avait définit IntegerContainer(Integer[] values).

En revanche, du point de vue de l'appelant tout se passe comme si le constructeur était défini avec autant de paramètres que l'on souhaite. Il pourra donc appeler le constructeur comme suit :

new IntegerContainer();
new IntegerContainer(1);
new IntegerContainer(1, 2);
new IntegerContainer(1, 2, 3);
new IntegerContainer(1, 2, 3, 4, 5, 6, 7);
// ...

Utiliser la forme « varargs » est donc une politesse pour l'appelant qui n'aura pas besoin de mettre ses arguments dans un tableau.

Sans « varargs » l'appelant est obligé de créer un tableau pour appeler le constructeur :

new IntegerContainer(new Integer[] {1, 2, 3});

On peut créer aussi des méthodes à arité variable (pas seulement les constructeurs).

Sortons de cette digression pour terminer l'exercice 3 :

public class IntegerContainer {

    private Integer[] values;

    public IntegerContainer(Integer... values) {
        this.values = values;
    }

    @Override
    public String toString() {
        CharSequence[] stringValues = new CharSequence[values.length];
        for (int i = 0; i < values.length; i++) {
            stringValues[i] = String.valueOf(values[i]);
        }
        return "{" + String.join(", ", stringValues) + "}";
    }

    public Integer first() {
        if (values.length == 0) {
            throw new IllegalArgumentException("Empty container");
        }
        return values[0];
    }

    public int countEqual(Integer element) {
        int count = 0;
        for (Integer value : values) {
            if (value.equals(element)) {
                count++;
            }
        }
        return count;
    }
}

5.20.4 Exercice 4

Comme d'habitude, on écrit un Main pour voir comment on utilisera notre classe. La classe DoubleContainer prend un nombre arbitraire de nombres réels et doit implémenter la méthode toString(), first() et countEqual(Double value).

public class Main {
    public static void main(String[] args) {
        DoubleContainer a = new DoubleContainer(10.0, 20.0, 10.0);
        System.out.println(a); // {10.0, 20.0, 10.0}
        System.out.println(a.countEqual(10.0)); // 2
        System.out.println(a.first()); // 10.0

        DoubleContainer empty = new DoubleContainer();
        System.out.println(empty); // {}
        System.out.println(empty.countEqual(10.0)); // 0
        System.out.println(empty.first()); // IllegalArgumentException: Empty container
    }
}
public class DoubleContainer {

    private Double[] values;

    public DoubleContainer(Double... values) {
        this.values = values;
    }

    @Override
    public String toString() {
        CharSequence[] stringValues = new CharSequence[values.length];
        for (int i = 0; i < values.length; i++) {
            stringValues[i] = String.valueOf(values[i]);
        }
        return "{" + String.join(", ", stringValues) + "}";
    }

    public Double first() {
        if (values.length == 0) {
            throw new IllegalStateException("Empty container");
        }
        return values[0];
    }

    public int countEqual(Double element) {
        int count = 0;
        for (Double value : values) {
            if (value.equals(element)) {
                count++;
            }
        }
        return count;
    }
}

5.20.5 Exercice 5

public class Main {
    public static void main(String[] args) {
        ObjectContainer doubleContainer = new ObjectContainer(0.1, 0.2, 0.3);

        System.out.println(doubleContainer); // {0.1, 0.2, 0.3}
        System.out.println(doubleContainer.countEqual(0.0)); // 0
        Object firstOfDouble = doubleContainer.first();
        System.out.println(firstOfDouble); // 0.1

        ObjectContainer integerContainer = new ObjectContainer(500, 300, 500, 500);

        System.out.println(integerContainer); // {500, 300, 500, 500}
        System.out.println(integerContainer.countEqual(500)); // 3
        Object firstOfIntegers = integerContainer.first();
        System.out.println(firstOfIntegers); // 500
    }
}
public class ObjectContainer {

    private Object[] values;

    public ObjectContainer(Object... values) {
        this.values = values;
    }

    @Override
    public String toString() {
        CharSequence[] stringValues = new CharSequence[values.length];
        for (int i = 0; i < values.length; i++) {
            stringValues[i] = String.valueOf(values[i]);
        }
        return "{" + String.join(", ", stringValues) + "}";
    }

    public Object first() {
        if (values.length == 0) {
            throw new IllegalStateException("Empty container");
        }
        return values[0];
    }

    public int countEqual(Object element) {
        int count = 0;
        for (Object value : values) {
            if (value.equals(element)) {
                count++;
            }
        }
        return count;
    }
}

5.20.6 Exercice 6

Une première version non générique pourrait être celle-ci :


public class Main {
    public static void main(String[] args) {
        printStringSafely("hello");
        //printStringSafely(null);

        printIfPositiveIntegerSafely(5);
        //printIfPositiveIntegerSafely(null);
    }

    private static void printStringSafely(String name) {
        String checkedName = (String) Checks.failIfNull(name);
        System.out.println(checkedName);
    }
    
    private static void printIfPositiveIntegerSafely(Integer value) {
        Integer checkedValue = (Integer) Checks.failIfNull(value);
        if (checkedValue >= 0) {
            System.out.println(checkedValue);
        }
    }
}

Avec

public class Checks {
    public static Object failIfNull(Object object) {
        if (object == null) {
            throw new IllegalArgumentException("null");
        }
        return object;
    }
}

Le problème de cette implémentation de failIfNull() est qu'en passant un String en argument on récupère tout de même un Object. Donc on a perdu de l'information. D'où la nécessité du cast.

On aimerait donc éliminer le cast :

void printIfPositiveIntegerSafely(Integer value) {
    Integer checkedValue = Checks.failIfNull(value);
    if (checkedValue >= 0) {
        System.out.println(checkedValue);
    }
}

void printStringSafely(String name) {
    String checkedName = Checks.failIfNull(name);
    System.out.println(checkedName);
}

On extrait un paramètre de type nommé T :

public class Checks {
    public static <T> T failIfNull(T object) {
        if (object == null) {
            throw new IllegalArgumentException("null");
        }
        return object;
    }
}

5.20.7 Exercice 7

Dans l'exercice on a définit un paramètre de type sur une méthode. Ici, on définit un paramètre de type (T) sur une classe.

public class CheckedBox<T> {
    private T object;

    public CheckedBox() {
    }

    public CheckedBox(T object) {
        this.object = object;
    }

    public T get() {
        if (object == null) {
            throw new IllegalStateException("null");
        }
        return object;
    }
}

5.20.8 Exercice 8

public class Pair<T> {
    private final T left;
    private final T right;

    public Pair(T left, T right) {
        this.left = left;
        this.right = right;
    }

    public Pair<T> reverse() {
        return new Pair<>(right, left);
    }

    @Override
    public String toString() {
        return "(" + left + ", " + right + ")";
    }
}

5.20.9 Exercice 9

public class Tuple2<T, U> {

    private final T a;
    private final U b;

    public Tuple2(T a, U b) {
        this.a = a;
        this.b = b;
    }

    @Override
    public String toString() {
        return "(" + a + ", " + b + ")";
    }

    public Tuple2<U, T> reverse() {
        return new Tuple2<>(b, a);
    }
}

5.20.10 Exercice 10

public class Tuple2<T, U> {

    private final T a;
    private final U b;

    public Tuple2(T a, U b) {
        this.a = a;
        this.b = b;
    }

    @Override
    public String toString() {
        return "(" + a + ", " + b + ")";
    }

    public Tuple2<U, T> reverse() {
        return new Tuple2<>(b, a);
    }

    public T getA() {
        return a;
    }

    public U getB() {
        return b;
    }
}

Et :

public class Pair<T> {
    private final Tuple2<T, T> tuple2;

    public Pair(T left, T right) {
        tuple2 = new Tuple2<>(left, right);
    }

    public Pair<T> reverse() {
        Tuple2<T, T> reversed = tuple2.reverse();

        return new Pair<>(reversed.getA(), reversed.getB());
    }

    @Override
    public String toString() {
        return tuple2.toString();
    }
}

5.20.11 Exercice 11

public class GenericContainer<T> {

    private T[] values;

    public GenericContainer(T... values) {
        this.values = values;
    }
    
    public T first() {
        if (values.length == 0) {
            throw new IllegalStateException("Empty container");
        }
        return values[0];
    }

    public int countEqual(T element) {
        int count = 0;
        for (T value : values) {
            if (value.equals(element)) {
                count++;
            }
        }
        return count;
    }
    
    public GenericContainer<T> repeat(int count) {
        @SuppressWarnings("unchecked")
        T[] newValues = (T[]) new Object[count * this.values.length];

        for (int i = 0; i < values.length; i++) {
            for (int j = 0; j < count; j++) {
                newValues[(i * count) + j] = values[i];
            }
        }
        return new GenericContainer<>(newValues);
    }

    @Override
    public String toString() {
        CharSequence[] stringValues = new CharSequence[values.length];
        for (int i = 0; i < values.length; i++) {
            stringValues[i] = String.valueOf(values[i]);
        }
        return "{" + String.join(", ", stringValues) + "}";
    }
}

La ligne T[] newValues = (T[]) new Object[count * this.values.length]; mérite une petite explication.

Les tableaux de Java ne peuvent pas être créés génériquement. C'est à dire qu'on ne peut écrire :

T[] x = new T[0]; // Error: generic array creation

Ceci vient du fait que les types paramétrés ne sont pas connus à l'exécution du programme. On parle de « type erasure ». Ici à l'exécution le type T est effacé pour devenir Object. C'est pour cela qu'on crée un tableau d'objets (new Object[count * this.values.length]). Pour compiler, on est obligé de caster de tableau d'objets en tableau de T.

5.20.12 Exercice 12

public class Pair<T> {
    private final T left;
    private final T right;

    public Pair(T left, T right) {
        this.left = left;
        this.right = right;
    }
    
    @Override
    public String toString() {
        return "(" + left + ", " + right + ")";
    }

    public static <U> Pair<U> duplicate(U element) {
        return new Pair<>(element, element);
    }
}

5.20.13 Exercice 13

public class Pair<T> {
    private final T left;
    private final T right;

    public Pair(T left, T right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public String toString() {
        return "(" + left + ", " + right + ")";
    }

    public static <U extends Number> Pair<U> sort(Pair<U> pair) {
        if (pair.left.doubleValue() > pair.right.doubleValue()) {
            return new Pair<>(pair.right, pair.left);
        }
        return new Pair<>(pair.left, pair.right);
    }
}

5.21 Exercice 15

public static void printSum(Pair<? extends Number> pair) {
    System.out.println(pair.left.doubleValue() + pair.right.doubleValue());
}

5.22 Exercice 16

public static <U> Pair<U> fromList(List<? extends U> input) {
    return new Pair<>(input.get(0), input.get(1));
}

Il faut retenir qu'on utilise une borne supérieure parce que input produit des données (dont le type est la borne, ici U). Ici on a appelé uniquement la méthode U get(int) qui retourne un objet.

5.23 Exercice 17

public void writeToList(List<? super T> output) {
    output.add(left);
    output.add(right);
}

Il faut retenir qu'on utilise une borne inférieure parce que output consomme des données (dont le type est la borne, ici T). Ici on a appelé uniquement la méthode void add(T) qui prend en paramètre un objet.

6 Les fils d'exécutions

En Java, les d'instructions d'un bloc sont exécutées en séquence.

int x = 10; // 1
int y = 20; // 2
int z = x + y; // 3

La dernière instruction est toujours exécutée en dernier.

Toute instruction est exécutée dans un fil d'exécution qu'on appelle "thread".

Instruction * -- 1 Thread
public static void main(String[] args) {
    System.out.println(Thread.currentThread()); // Thread[main,5,main]
}

Dans certains cas de figure on préfère que deux instructions soient exécutées en parallèle plutôt qu'en série.

Ça pourrait être le cas dans :

int x = readFromInput1();
int y = readFromInput2();
int z = x + y;

Comme les 2 premières lignes sont indépendantes. Nul besoin d'attendre que la 1ère soit terminée pour commencer la 2e. En revanche, la 3e ligne a besoin de x et y. Elle doit donc attendre que les deux premières instructions soient terminées.

Ici on a traitement en Y :

x     y
 \   /
  \ /
   z

Pour pouvoir exécuter 2 traitements en parallèle on devra avoir 2 threads disponibles.

Ici, on aura un thread qui calculera x, un thread qui calculera y. Le thread qui calcule z pourra réutiliser un des 2 threads précédant étant donné que son code devra être exécuté après x et y.

Si le thread qui calcule z est le même que celui qui calcule x, celui ci devra attendre explicitement le thread qui calcule y. Inversement, si z est calculé dans le thread de y, ce thread devra attendre explicitement que le thread qui calcule x finisse. Enfin, si le thread calculant z est un 3e thread, il devra explicitement attendre les deux premiers.

Commençons par la manière naïve et systématique.

On va créer 3 Thread que l'on va démarrer.

Par défaut, on a déja vu que main() tourne déjà dans un thread.

Pour en créer un nouveau on instancie un objet Thread. On doit spécifier à la construction de l'objet le code que le thread exécutera une fois lancé.

public static void main(String[] args) {
    System.out.println(Thread.currentThread());
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread());
        }
    });
    thread.start();
}

Si on appelle pas thread.start(), le fil d'exécution n'est pas lancé. On aura juste un objet Java instancié pour rien.

Le programme précédant affichera :

Thread[main,5,main]
Thread[Thread-0,5,main]

En revanche inverser :

public static void main(String[] args) {
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread());
        }
    });
    thread.start();
    System.out.println(Thread.currentThread());
}

Pourra afficher :

Thread[main,5,main]
Thread[Thread-0,5,main]

que (lignes inversées) :

Thread[Thread-0,5,main]
Thread[main,5,main]

Ceci car la méthode start() n'est pas blocante. Elle indique simplement que le code spécifié dans le constructeur pourra être appelé à n'importe quel moment par le scheduler de la JVM.

6.1 Classe anonyme

La construction suivante mérite une petite explication :

Thread thread = new Thread(new Runnable() {
    public void run() {
        @Override
        System.out.println(Thread.currentThread());
    }
});

Étudions plus précisément l'expression :

new Runnable() {
    @Override
    public void run() {
        // ...
    }
}

Elle définit une classe sans nom qui implémente Runnable. C'est comme si on avait écrit :

new Anonymous();

avec la classe imbriquée suivante :

class Anonymous implements Runnable {
    public void run() {
        // ...
    }
}

Il y a en fait toute une famille de classes imbriquées (nested) : classes internes (inner), classes locales (dont fait partie les classes anonymes). Pour aller plus loin : https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html

6.2 Exercice 1

On veut générer 10 millions (10M) de nombres entiers aléatoires.

  1. Écrire un programme qui sauvegarde séquentiellement ces nombres dans un fichier texte.

6.3 Correction 1

public static void main(String[] args) throws FileNotFoundException {
    PrintStream printStream = new PrintStream("random.csv");
    for (int i = 0; i < 10_000_000; i++) {
        if (i % 1_000_000 == 0) {
            System.out.println("1M");
        }
        int random = (int) (Math.random() * 100); // [0 - 99]
        printStream.println(random);
    }

    printStream.close();
}

Temps d'exécution :

337571ms

Moyenne = 30s

6.4 Exercice 2

Si l'on juge (selon le contexte) que 30s est un temps inacceptable, alors il on pourra essayer de diminuer ce temps en parallélisant les traitements coûteux.

  1. Configurer l'instance de PrintStream avec un tampon (buffer).
  2. Trouver le code long à exécuter.

6.5 Correction 2

package thread.exercice;

import java.io.BufferedOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;

public class Ex2 {
    public static void main(String[] args) throws FileNotFoundException {
        long start = System.currentTimeMillis();
        PrintStream printStream = new PrintStream(
            new BufferedOutputStream(new FileOutputStream("random.csv")));
        List<Integer> list = new ArrayList<>();

        long startRandom = System.currentTimeMillis();
        for (int i = 0; i < 10_000_000; i++) {
            int random = (int) (Math.random() * 100);
            list.add(random);
        }
        long endRandom = System.currentTimeMillis();
        for (int i = 0; i < list.size(); i++) {
            if (i % 1_000_000 == 0) {
                System.out.println("1M");
            }
            Integer random = list.get(i);
            printStream.println(random);
        }
        long endPrint = System.currentTimeMillis();

        printStream.close();
        long end = System.currentTimeMillis();
        System.out.println("Total: " + (end - start) + "ms");
        System.out.println("Random: " + (endRandom - startRandom) + "ms");
        System.out.println("Print: " + (endPrint - endRandom) + "ms");
    }
}

Rien qu'en passant à un flux bufferisé, on a divisé par 2 le temps de traitement.

Total: 16109ms
Random: 1264ms
Print: 14812ms

On voit de plus que le temps passé dans les entrées/sorties domine largement le temps passé dans la génération des nombres aléatoires.

6.6 Exercice 3

Optimiser la sauvegarde sur fichier des nombres aléatoires. On pourra pour cela essayer d'écrire dans deux fichiers plutôt qu'un.

6.7 Correction 3

Pour optimiser la sauvegarde nous avons déjà besoin de séparer les traitements de génération de nombre de leur sauvegarde, de sorte de paralléliser uniquement le code d'écriture fichier.

On sépare la liste de 10M en 2 listes de 5M. Et on écrit dans le même fichier de manière séquentielle.

Enfin on traite l'écriture des 2 listes en parallèle.

package thread.exercice;

import java.io.BufferedOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;

public class Ex3 {
    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        List<Integer> list = generateRandom();
        List<Integer> a = list.subList(0, list.size() / 2);
        List<Integer> b = list.subList(list.size() / 2, list.size());
        assert a.size() + b.size() == list.size();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    print(a, "random1.csv");
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    print(b, "random2.csv");
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                }
            }
        });
        thread1.start();
        thread2.start();
        Thread.yield();
        thread1.join();
        thread2.join();
        long end = System.currentTimeMillis();
        System.out.println((end - start) + "ms");
    }

    private static void print(List<Integer> a, String fileName) throws FileNotFoundException {
        PrintStream printStream1 = new PrintStream(new BufferedOutputStream(new FileOutputStream(fileName)));
        int i = 0;
        for (Integer random : a) {
            if (i % 1_000_000 == 0) {
                System.out.println("1M");
            }
            printStream1.println(random);
            i++;
        }
        printStream1.close();
    }

    private static List<Integer> generateRandom() {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 10_000_000; i++) {
            int random = (int) (Math.random() * 100);
            list.add(random);
        }
        return list;
    }
}

Sur ma machine, le programme s'exécute en 10s en moyenne.

On a gagnés 1/3 sur le temps d'écriture total.

6.8 Exercice 4

Écrire un programme qui affiche la somme et le nombre d'éléments du fichier de 10M nombre : random.csv.

Par exemple si random.csv contient :

10
20

alors on devra afficher :

30 2

6.9 Correction 4

package thread.exercice;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

public class Ex4 {
    public static void main(String[] args) throws IOException {
        List<String> strings = Files.readAllLines(Paths.get("random.csv"));
        int sum = 0;
        int count = 0;
        for (String string : strings) {
            int n = Integer.parseInt(string);
            sum += n;
            count += 1;
        }
        System.out.printf("%d %d%n", sum, count);
    }
}

6.10 Exercice 5

Transformer le programme 4 pour afficher la également la somme et le compte les 2 fichiers random1.csv et random2.csv.

Par exemple si random1.csv contient :

10
20

et random2.csv contient :

30
40

Alors le programme devra afficher :

100 4

6.11 Correction 5

package thread.exercice;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

public class Ex5 {
    public static void main(String[] args) throws IOException {
        Average average1 = computeAverage("random1.csv");
        Average average2 = computeAverage("random2.csv");

        System.out.printf("%d %d%n", 
            average1.getSum() + average2.getSum(), 
            average1.getCount() + average2.getCount());
    }

    private static Average computeAverage(String filename) throws IOException {
        List<String> strings = Files.readAllLines(Paths.get(filename));
        int sum = 0;
        int count = 0;
        for (String string : strings) {
            int n = Integer.parseInt(string);
            sum += n;
            count += 1;
        }
        return new Average(sum, count);
    }

}

class Average {
    private final int sum;
    private final int count;

    Average(int sum, int count) {
        this.sum = sum;
        this.count = count;
    }

    int getSum() {
        return sum;
    }

    int getCount() {
        return count;
    }
}

Les expressions average1.getSum() + average2.getSum() et average1.getCount() + average2.getCount() permettent de définir la moyenne d'une somme. Elle n'est pas la somme des moyennes.

On pourra réécrire :

package thread.exercice;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

public class Ex5 {
    public static void main(String[] args) throws IOException {
        Average average1 = computeAverage("random1.csv");
        Average average2 = computeAverage("random2.csv");

        print(Average.sum(average1, average2));
    }

    private static void print(Average average) {
        System.out.printf("%d %d%n", average.getSum(), average.getCount());
    }

    private static Average computeAverage(String filename) 
        throws IOException {
        List<String> strings = Files.readAllLines(Paths.get(filename));
        int sum = 0;
        int count = 0;
        for (String string : strings) {
            int n = Integer.parseInt(string);
            sum += n;
            count += 1;
        }
        return new Average(sum, count);
    }

}

class Average {
    private final int sum;
    private final int count;

    Average(int sum, int count) {
        this.sum = sum;
        this.count = count;
    }

    static Average sum(Average average1, Average average2) {
        return new Average(average1.getSum() + average2.getSum(), 
            average1.getCount() + average2.getCount());
    }

    int getSum() {
        return sum;
    }

    int getCount() {
        return count;
    }
}

Ce programme tourne en 5s en moyenne.

6.12 Exercice 6

Paralléliser la lecture des deux fichiers.

6.13 Correction 6

package thread.exercice;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

public class Ex6 {
    public static void main(String[] args) throws InterruptedException {
        final Average[] average1 = new Average[1];
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    average1[0] = computeAverage("random1.csv");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
        final Average[] average2 = new Average[1];
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    average2[0] = computeAverage("random2.csv");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
        thread1.start();
        thread2.start();
        Thread.yield();
        thread1.join();
        thread2.join();

        print(Average.sum(average1[0], average2[0]));
    }

    private static void print(Average average) {
        System.out.printf("%d %d%n", average.getSum(), 
            average.getCount());
    }

    private static Average computeAverage(String filename) 
        throws IOException {
        List<String> strings = Files.readAllLines(Paths.get(filename));
        int sum = 0;
        int count = 0;
        for (String string : strings) {
            int n = Integer.parseInt(string);
            sum += n;
            count += 1;
        }
        return new Average(sum, count);
    }

    static class Average {
        private final int sum;
        private final int count;

        Average(int sum, int count) {
            this.sum = sum;
            this.count = count;
        }

        static Average sum(Average average1, Average average2) {
            return new Average(average1.getSum() + average2.getSum(), 
                average1.getCount() + average2.getCount());
        }

        int getSum() {
            return sum;
        }

        int getCount() {
            return count;
        }
    }
}
495150079 10000000

Sur ma machine, le programme se termine en 9543ms 4615ms 6533ms...

6.14 Exercice 7

Afficher la moyenne arithmétique des nombres plutôt que leur somme et leur compte.

moyenne = somme / compte

6.15 Exercice 8

Lancer le programme de calcul de la moyenne précédant en ayant préalablement supprimé le fichier random1.csv.

  1. Qu'observe t'on ?

On souhaite conditionner l'affichage de la moyenne à la réussite des 2 fils d'exécutions.

  1. Comment transmettre l'information de succès entre Thread ?

6.16  Correction 8

public static void main(String[] args) throws InterruptedException {
    long start = System.currentTimeMillis();
    final Average[] average1 = new Average[1];
    final Exception[] exception1 = new Exception[1];
    Thread thread1 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                average1[0] = computeAverage("random1.csv");
            } catch (IOException e) {
                exception1[0] = e;
            }
        }
    });
    final Average[] average2 = new Average[1];
    final Exception[] exception1 = new Exception[1];
    Thread thread2 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                average2[0] = computeAverage("random2.csv");
            } catch (IOException e) {
                exception2[0] = e;
            }
        }
    });
    thread1.start();
    thread2.start();
    Thread.yield();
    thread1.join();
    thread2.join();
    long end = System.currentTimeMillis();
    System.out.println(end - start + "ms");
    
    if (exception1[0] == null) {
        throw new Exception(exception1[0]);
    }
    if (exception2[0] == null) {
        throw new Exception(exception2[0]);
    }
    
    print(Average.sum(average1[0], average2[0]));
}

6.17 Exercice 9

Réécrire le programme précédant en utilisant cette fois la méthode Thread.setUncaughtExceptionHandler() pour conditionner l'affichage de la moyenne au succès des deux threads.

6.18 Correction 9

public static void main(String[] args) throws InterruptedException {
    long start = System.currentTimeMillis();
    final Average[] average1 = new Average[1];
    final Exception[] exception1 = new Exception[1];
    Thread thread1 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                average1[0] = computeAverage("random1.csv");
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    });
    final Average[] average2 = new Average[1];
    final Exception[] exception2 = new Exception[1];
    Thread thread2 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                average2[0] = computeAverage("random2.csv");
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    });
    
    thread1.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            exception1[0] = e;
        }
    });
    
    thread2.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            exception2[0] = e;
        }
    });
    
    thread1.start();
    thread2.start();
    Thread.yield();
    thread1.join();
    thread2.join();
    long end = System.currentTimeMillis();
    System.out.println(end - start + "ms");
    
    if (exception1[0] == null) {
        throw new Exception(exception1[0]);
    }
    if (exception2[0] == null) {
        throw new Exception(exception2[0]);
    }
    
    print(Average.sum(average1[0], average2[0]));
}

6.19 Exercice 10

Écrire une classe Counter et une fonction qui incrémente 100 000 fois le compteur.

6.20 Correction 10

package thread.exercice;

public class Counter {

    public static void main(String[] args) {
        Counter counter = new Counter();
        for (int i = 0; i < 100_000; i++) {
            counter.increment();
        }
        System.out.println(counter);
    }

    private int i;

    private void increment() {
        i++;
    }

    @Override
    public String toString() {
        return String.valueOf(i);
    }
}

6.21 Exercice 11

Partager le travail d'incrémentation entre 2 fils d'exécuctions concurrents. 50 000 fois chacun. Observer le résultat.

6.22 Correction 12

package thread.exercice;

public class Counter {

    private static final int TIMES = 50_000;

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                incrementUntil(counter, TIMES);
            }
        });
        thread.start();
        Thread.yield();
        incrementUntil(counter, TIMES);
        thread.join();
        System.out.println(counter);
    }

    private static void incrementUntil(Counter counter, int times) {
        for (int i = 0; i < times; i++) {
            counter.increment();
        }
    }

    private int i;

    private void increment() {
        i++;
    }

    @Override
    public String toString() {
        return String.valueOf(i);
    }
}

On observe les résultats suivant :

96810 
100000 
53682 
100000 
75031 
52817 
100000
100000
98012

On s'attendait à avoir 100% de 100000. Soit moins de 50% de bons résultats.

6.23 Synchronisation

Quand plusieurs Thread partagent un objet, tout se gatte. Effectuer des lectures ou écritures en concurrence nécessite beaucoup de rigueur. En effet, l'exemple précédant montre une "race condition". L'opération i++ effectuée en concurrence n'est pas atomique comme on pourrait le penser.

Une opération est atomique quand tous les threads peuvent dire si l'opération est exécutée ou non. Si un thread peut observer l'état intermédiaire et interne d'une opération alors elle n'est pas atomique.

L'instruction i++ consiste en 3 étapes :

  1. lecture de la variable i ;
  2. incrémentation ;
  3. écriture de la nouvelle valeur.

Il est donc tout à fait possible que le thread 1 lise i (i = 0), qu'au même moment le thread 2 lise aussi i (i = 0). Le deuxième thread travaille donc avec des données déjà obsolète. Le résultat dans ce cas est que deux opérations concurrentes donnent le même résultat qu'une seule.

Le premier mécanisme fournit par le langage Java permet de vérouiller l'accès à un objet partagé pendant une opération que l'on souhaite atomique.

private synchronized void increment() {
    i++;
}

Cette syntaxe indique qu'un seul thread pourra accéder à cet objet à la fois. Plus précisément, un seul thread peut appeler les méthodes synchronized de l'objet. Il est donc possible qu'un premier thread accède à l'objet counter en appelant increment(), et qu'un deuxième y accède aussi en même temps à via toString() qui n'est pas synchronized.

En revanche, si on définit la méthode :

private synchronized int get() {
    return i;
}

Alors, il ne pourra pas y avoir le thread 1 appelant increment() en même temps que thread 2 qui appelle get().

Tous les objets Java ont un verrou intrinsec qui est ouvert et fermé par le thread qui appelle la méthode synchronized. On dit que le thread acquiert le verrou au début de la méthode et le relâche la fin.

Une syntaxe alternative plus souple et verbeuse met ce verrou intrinsec en lumière (this) :

private synchronized void increment() {
    synchronized (this) {
        i++;
    }
}

6.24 Dîner des philosophes (13)

Cinq philosophes dînent le soir. Chacun partage sa fourchette droite avec la fourchette gauche de son voisin de droite. Il y a donc cinq fourchettes (représentées par |). Chaque philosophe réfléchit un moment puis mange pendant un certain temps.

... 1 | 2 | 3 | 4 | 5 | 1...

Pour manger, le philosophe commence par prendre sa fourchette gauche puis la droite puis mange puis repose la fourchette droite puis la gauche.

Précision : les philosophes ne se parlent pas. Écrire un programme qui simule un repas entre ces 5 philosophes.

6.25 Correction (13)

package thread.exercice;

import java.util.ArrayList;
import java.util.List;

public class DinerPhilosophe {
    public static void main(String[] args) throws InterruptedException {
        List<Object> fourchettes = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Object fourchette = i;
            fourchettes.add(fourchette);
        }
        List<Philosophe> philosophes = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            philosophes.add(new Philosophe(fourchettes.get(i), fourchettes.get((i + 1) % 5), String.valueOf(i)));
        }
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            int finalI = i;
            Runnable target = new Runnable() {
                @Override
                public void run() {
                    Philosophe philosophe = philosophes.get(finalI);
                    for (int j = 0; j < 10_000; j++) {
                        philosophe.manger();
                    }
                }
            };
            threads.add(new Thread(target));
        }
        for (Thread thread : threads) {
            thread.start();
        }
        Thread.yield();
        for (Thread thread : threads) {
            thread.join();
        }
        System.out.println("Fin du dîner");
    }

    private static class Philosophe {
        private final Object fourchetteGauche;
        private final Object fourchetteDroite;
        private final String nom;

        Philosophe(Object fourchetteGauche, Object fourchetteDroite, String nom) {
            this.fourchetteGauche = fourchetteGauche;
            this.fourchetteDroite = fourchetteDroite;
            this.nom = nom;
        }

        void manger() {
            System.out.printf("%s attend la fourchette gauche %s%n", nom, fourchetteGauche);
            synchronized (fourchetteGauche) {
                System.out.printf("%s attend la fourchette droite %s%n", nom, fourchetteDroite);
                synchronized (fourchetteDroite) {
                    System.out.printf("%s mange%n", nom);
                    try {
                        Thread.sleep(((long) (Math.random() * 500)));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

En lancant ce programme j'obtiens un blocage mutuel (aussi appelé "deadlock") :

0 attend la fourchette gauche 0
0 prend la fourchette gauche 0
0 attend la fourchette droite 1
0 prend la fourchette droite 1
0 mange
3 attend la fourchette gauche 3
3 prend la fourchette gauche 3
3 attend la fourchette droite 4
3 prend la fourchette droite 4
3 mange
4 attend la fourchette gauche 4
1 attend la fourchette gauche 1
2 attend la fourchette gauche 2
2 prend la fourchette gauche 2
2 attend la fourchette droite 3
0 repose la fourchette droite 1
1 prend la fourchette gauche 1
1 attend la fourchette droite 2
0 repose la fourchette gauche 0
0 attend la fourchette gauche 0
0 prend la fourchette gauche 0
0 attend la fourchette droite 1
3 repose la fourchette droite 4
4 prend la fourchette gauche 4
4 attend la fourchette droite 0
3 repose la fourchette gauche 3
3 attend la fourchette gauche 3
3 prend la fourchette gauche 3
3 attend la fourchette droite 4

Ici le philosophe 3 attend la fourchette droite 4. Or la fourchette droite 4 est toujours entre les mains du philosophe 4. Mais le philosophe 4 attend aussi sa fourchette droite 0. Et ainsi de suite jusqu'à avoir un inter-blocage (comme dans certaines intersections routières).

6.26 Exercice 14

Pour éviter cet interblocage, une méthode consiste à toujours vérrouiller les fourchettes dans le même ordre global. On peut ordonner les fourchette facilement de façon croissante : chaque philosophe doit prendre la fourchette qui a un numéro plus petit en premier.

Par exemple, si le philosophe 0 a la fourchette 0 et 1 alors il devra prendre la 0 en premier. Si le philosophe 4 a la fourchette 4 et 0 il devra prendre la 0 en premier. Ainsi les attentes mutuelles ne sont plus possibles.

  1. Corriger le programme de simulation du dîner en imposant un ordre global.

6.27 Correction 14

if (fourchetteGauche.ordre > fourchetteDroite.ordre) {
    this.fourchetteGauche = fourchetteDroite;
    this.fourchetteDroite = fourchetteGauche;
} else {
    this.fourchetteGauche = fourchetteGauche;
    this.fourchetteDroite = fourchetteDroite;
}
assert this.fourchetteGauche.ordre < this.fourchetteDroite.ordre;

Le programme entier :

package thread.exercice;

import java.util.ArrayList;
import java.util.List;

public class OrdreDinerPhilosophe {
    public static void main(String[] args) throws InterruptedException {
        List<Fourchette> fourchettes = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            fourchettes.add(new Fourchette(i));
        }
        List<Philosophe> philosophes = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            philosophes.add(new Philosophe(fourchettes.get(i), fourchettes.get((i + 1) % 5), String.valueOf(i)));
        }
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            int finalI = i;
            Runnable target = new Runnable() {
                @Override
                public void run() {
                    Philosophe philosophe = philosophes.get(finalI);
                    for (int j = 0; j < 1000; j++) {
                        philosophe.manger();
                    }
                }
            };
            threads.add(new Thread(target));
        }

        for (Thread thread : threads) {
            thread.start();
        }
        for (Thread thread : threads) {
            thread.join();
        }
        System.out.println("Fin du dîner");
    }

    private static class Philosophe {
        private final Fourchette fourchetteGauche;
        private final Fourchette fourchetteDroite;
        private final String nom;

        Philosophe(Fourchette fourchetteGauche, Fourchette fourchetteDroite, String nom) {
            if (fourchetteGauche.ordre > fourchetteDroite.ordre) {
                this.fourchetteGauche = fourchetteDroite;
                this.fourchetteDroite = fourchetteGauche;
            } else {
                this.fourchetteGauche = fourchetteGauche;
                this.fourchetteDroite = fourchetteDroite;
            }
            assert this.fourchetteGauche.ordre < this.fourchetteDroite.ordre;
            this.nom = nom;
        }

        void manger() {
            System.out.printf("%s attend la fourchette gauche %s%n", nom, fourchetteGauche);
            synchronized (fourchetteGauche) {
                System.out.printf("%s prend la fourchette gauche %s%n", nom, fourchetteGauche);
                System.out.printf("%s attend la fourchette droite %s%n", nom, fourchetteDroite);
                synchronized (fourchetteDroite) {
                    System.out.printf("%s prend la fourchette droite %s%n", nom, fourchetteDroite);
                    System.out.printf("%s mange%n", nom);
                    try {
                        Thread.sleep(((long) (Math.random() * 500)));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.printf("%s repose la fourchette droite %s%n", nom, fourchetteDroite);
            }
            System.out.printf("%s repose la fourchette gauche %s%n", nom, fourchetteGauche);
        }
    }

    private static class Fourchette {
        private int ordre;

        Fourchette(int ordre) {
            this.ordre = ordre;
        }
    }
}

Maintenant, le programme ne s'arrête plus.

6.28  Plus de verrous

Le mécanisme de verrou intrisec est limité : on ne doit forcément relacher un verrou dans la même méthode qu'on l'a acquis.

Pour pouvoir programmer de manière plus libre on pourra utiliser ReentrantLock

class X {
    private final ReentrantLock lock = new ReentrantLock();

    public void m() {
        lock.lock();  // block until condition holds
        try {
            // ...
        } finally {
            lock.unlock()
        }
    }
    
    public void fermer() {
        lock.lock();
    }
    
    public void ouvrir() {
        try {
            // ...
        } finally {
            lock.unlock()
        }
    }
}

6.29 Exercice 15

Réécrire le simulateur de diné de philosophe avec des verrous réentrants.

6.30 Correction 15

package thread.exercice;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantDinerPhilosophe {
    public static void main(String[] args) throws InterruptedException {
        List<Fourchette> fourchettes = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            fourchettes.add(new Fourchette(i));
        }

        List<Philosophe> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            list.add(new Philosophe(fourchettes.get(i), fourchettes.get((i + 1) % 5), String.valueOf(i)));
        }
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            int finalI = i;
            Runnable target = new Runnable() {
                @Override
                public void run() {
                    Philosophe philosophe = list.get(finalI);
                    for (int j = 0; j < 1000; j++) {
                        philosophe.manger();
                    }
                }
            };
            threads.add(new Thread(target));
        }

        for (Thread thread : threads) {
            thread.start();
        }
        for (Thread thread : threads) {
            thread.join();
        }
        System.out.println("Fin du dîner");
    }

    private static class Philosophe {
        private final Fourchette fourchetteGauche;
        private final Fourchette fourchetteDroite;
        private final String nom;

        Philosophe(Fourchette fourchetteGauche, Fourchette fourchetteDroite, String nom) {
            if (fourchetteGauche.ordre > fourchetteDroite.ordre) {
                this.fourchetteGauche = fourchetteDroite;
                this.fourchetteDroite = fourchetteGauche;
            } else {
                this.fourchetteGauche = fourchetteGauche;
                this.fourchetteDroite = fourchetteDroite;
            }
            assert this.fourchetteGauche.ordre < this.fourchetteDroite.ordre;
            this.nom = nom;
        }

        void manger() {
            System.out.printf("%s attend la fourchette gauche %s%n", nom, fourchetteGauche);
            fourchetteGauche.prendre();
            try {
                System.out.printf("%s prend la fourchette gauche %s%n", nom, fourchetteGauche);
                System.out.printf("%s attend la fourchette droite %s%n", nom, fourchetteDroite);
                fourchetteDroite.prendre();
                try {
                    System.out.printf("%s prend la fourchette droite %s%n", nom, fourchetteDroite);
                    System.out.printf("%s mange%n", nom);
                    try {
                        Thread.sleep(((long) (Math.random() * 500)));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } finally {
                    fourchetteDroite.relacher();
                    System.out.printf("%s repose la fourchette droite %s%n", nom, fourchetteDroite);
                }
            } finally {
                fourchetteGauche.relacher();
                System.out.printf("%s repose la fourchette gauche %s%n", nom, fourchetteGauche);
            }
        }
    }

    private static class Fourchette {
        private ReentrantLock lock = new ReentrantLock();
        private int ordre;

        Fourchette(int ordre) {
            this.ordre = ordre;
        }

        void prendre() {
            lock.lock();
        }

        void relacher() {
            lock.unlock();
        }

        @Override
        public String toString() {
            return String.valueOf(ordre);
        }
    }
}

6.31 Concurrence et parallèlisme

Jusqu'ici on a présenté la programmation multithread comme une technique d'optimisation : on parallélise pour gagner du temps.

La concurrence est plus que cela, c'est un paradigme de programmation dans lequel on peut modéliser des problèmes de manière élégante (c'est à dire simple et efficace).

7 Programation modulaire

On peut arguer que le niveau d'abstraction le plus simple est la fonction : en Java la méthode. Le niveau d'abstraction supérieur est la classe. Une classe pouvant être vue comme une collection de méthodes.

On peut également regrouper les classes entre elles dans des packages. Mais un package est une structure est assez pauvre. En effet, elle crée un espace de nommage pour les types (classes, interfaces, enumérations, annotations...) et un espace de contrôle.

7.1 Espace de nommage

Deux classes qui ont le même nom pourront être distinguées grace à leur nom qualifié (nom du package + nom de la classe). Par exemple :

com.example.a.Foo x;
com.example.b.Foo y;

Dans ce cas x et y sont de classes différentes. Car leur nom qualifié est unique.

7.2 Espace de contrôle

On pourra encapsuler des classes dans un package les rendant package-private.

7.3 Archive Java (JAR)

Quand on démmare une machine virtuelle Java, on déclare un classpath dans lequel les .class sont physiquement situés.

Par exemple :

java -classpath lib/aaa/classes/ com.example.a.Foo

Si lib/aaa/classes/com/example/a/Foo.class existe, la classe Foo sera chargée à partir de ce fichier .class.

Le classpath comme le PATH est une liste de chemins. On peut ainsi définir plusieurs dossiers où chercher les classes compilées :

java -classpath lib/aaa/classes/:lib/bbb/classes/ com.example.a.Foo

Si une classe de même nom qualifié existe à plusieurs endroits dans le classpath. Que se passe t'il ?

$ ls -R lib
aaa/classes/com/example/a/Foo.class
bbb/classes/com/example/a/Foo.class

Plusieurs choses pourraient se passer :

Il se trouve que la spécification indique qu'elle prend la première classe trouvée.

Donc on peut avoir deux définitions différente pour le même nom de classe

Examinons le cas où deux classes du même package sont stockées physiquement dans 2 dossiers séparés :

lib/aaa/classes/com/example/a/Foo.class
lib/aaa/classes/com/example/a/Bar.class

Cette considération physique est totalement transparente pour le programme Java. Il considérera que Foo et Bar appartiennent bien au même package.

Un package est donc une abstraction logique et le classpath (collection de chemins) une abstraction physique.

En pratique les dossier qui regroupent les fichiers .class sont nombreux et sont téléchargés (réseau) et déplacés (système de fichiers). On regroupe donc tous ces fichiers dans une archive Java nommé JAR.

On nomme par convention : aaa.jar.

7.4 Chargeur de classe

On pourra pallier au problème des classes dupliquées en utilisant un class loader.

Chaque classe loader choisit comment charger une classe.

On pourra donc implémenter un mécanisme de protection d'accès entre unité arbitraire (module).

7.5 Exercice

Développer un CopyPasta qui copie un fichier texte dans un autre fichier.

7.6 Correction

package com.mathieupauly.copypasta;

import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

public class CopyPasta {

    public static void main(String[] args) throws IOException {
        if (args.length != 2) {
            System.out.println("Arguments requis : " +
                    "<entrée> <sortie>");
            System.exit(1);
        }
        Path in = Paths.get(args[0]);
        List<String> lines = Files.readAllLines(in);
        Path out = Paths.get(args[1]);
        BufferedWriter bufferedWriter = Files.newBufferedWriter(out);
        System.out.println("Copie de " + in + " vers " + out);
        for (String line : lines) {
            bufferedWriter.write(line);
            bufferedWriter.newLine();
        }
        bufferedWriter.close();
    }

}

7.7 Exercice

Modifier le programme de sorte à proposer à l'utilisateur une sorte de "grep" (filtrage de ligne) à la place de "cp" (copie ligne à ligne).

7.8 Correction

package com.mathieupauly.copypasta;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

public class CopyPasta {

    public static void main(String[] args) throws IOException {
        if (args.length != 2) {
            System.out.println("Arguments requis : " +
                    "<entrée> <sortie>");
            System.exit(1);
        }
        Path in = Paths.get(args[0]);
        List<String> lines = Files.readAllLines(in);
        Path out = Paths.get(args[1]);
        BufferedWriter bufferedWriter = Files.newBufferedWriter(out);
        System.out.println("Copie de " + in + " vers " + out);

        System.out.println("Liste des plugins :");
        System.out.println("[0] cp");
        System.out.println("[1] grep");
        BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
        String selectedPluginIndex = stdin.readLine();

        switch (selectedPluginIndex) {
            case "0":
                for (String line : lines) {
                    bufferedWriter.write(line);
                    bufferedWriter.newLine();
                }
                break;
            case "1":
                System.out.println("Motif ?");
                String motif = stdin.readLine();

                for (String line : lines) {
                    if (line.matches(motif)) {
                        bufferedWriter.write(line);
                        bufferedWriter.newLine();
                    }
                }
                break;
        }

        bufferedWriter.close();
    }

}

7.9 Exercice

Réécrire le programme pour rendre similaire le traitement de "cp" et "grep".

7.10 Correction

package com.mathieupauly.copypasta;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

public class CopyPasta {

    public static void main(String[] args) throws IOException {
        if (args.length != 2) {
            System.out.println("Arguments requis : " +
                    "<entrée> <sortie>");
            System.exit(1);
        }
        Path in = Paths.get(args[0]);
        List<String> lines = Files.readAllLines(in);
        Path out = Paths.get(args[1]);
        BufferedWriter bufferedWriter = Files.newBufferedWriter(out);
        System.out.println("Copie de " + in + " vers " + out);

        System.out.println("Liste des plugins :");
        System.out.println("[0] cp");
        System.out.println("[1] grep");
        BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
        String selectedPluginIndex = stdin.readLine();

        switch (selectedPluginIndex) {
            case "0":
                String[] motif = cpReadArguments(stdin);

                for (String line : lines) {
                    cpProcessInput(bufferedWriter, line, motif);
                }
                break;
            case "1":
                String[] motifs = grepReadArguments(stdin);

                for (String line : lines) {
                    grepProcessInput(bufferedWriter, line, motifs);
                }
                break;
        }

        bufferedWriter.close();
    }

    private static String[] cpReadArguments(BufferedReader stdin) throws IOException {
        return new String[]{};
    }

    private static void cpProcessInput(BufferedWriter bufferedWriter, String line, String[] parameters) throws IOException {
        bufferedWriter.write(line);
        bufferedWriter.newLine();
    }

    private static String[] grepReadArguments(BufferedReader stdin) throws IOException {
        System.out.println("Motif ?");
        return new String[]{stdin.readLine()};
    }

    private static void grepProcessInput(BufferedWriter bufferedWriter, String line, String[] parameters) throws IOException {
        String motif = parameters[0];
        if (line.matches(motif)) {
            bufferedWriter.write(line);
            bufferedWriter.newLine();
        }
    }

}

7.11 Exercice

Remplace le switch par du polymorphisme.

Bouger les méthodes cpReadArguments et cpProcessInput (resp. grepReadArguments et grepProcessInput) dans une classe séparer CpPlugin (resp. GrepPlugin).

En extraire une interface commune (Plugin contenant readArguments et processInput).

7.12 Correction

package com.mathieupauly.copypasta;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

public class CopyPasta {

    public static void main(String[] args) throws IOException {
        if (args.length != 2) {
            System.out.println("Arguments requis : " +
                    "<entrée> <sortie>");
            System.exit(1);
        }
        Path in = Paths.get(args[0]);
        List<String> lines = Files.readAllLines(in);
        Path out = Paths.get(args[1]);
        BufferedWriter bufferedWriter = Files.newBufferedWriter(out);
        System.out.println("Copie de " + in + " vers " + out);

        System.out.println("Liste des plugins :");
        System.out.println("[0] cp");
        System.out.println("[1] grep");
        BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
        String selectedPluginIndex = stdin.readLine();

        Plugin plugin = null;
        switch (selectedPluginIndex) {
            case "0":
                plugin = new CpPlugin();
                break;
            case "1":
                plugin = new GrepPlugin();
                break;
        }
        if (plugin != null) {
            String[] motif = plugin.readArguments(stdin);

            for (String line : lines) {
                plugin.processInput(bufferedWriter, line, motif);
            }
        }

        bufferedWriter.close();
    }

}
package com.mathieupauly.copypasta;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;

public interface Plugin {
    String[] readArguments(BufferedReader stdin) throws IOException;

    void processInput(BufferedWriter bufferedWriter, String line, String[] parameters) throws IOException;
}
package com.mathieupauly.copypasta;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;

public class CpPlugin implements Plugin {
    @Override
    public String[] readArguments(BufferedReader stdin) throws IOException {
        return new String[]{};
    }

    @Override
    public void processInput(BufferedWriter bufferedWriter, String line, String[] parameters) throws IOException {
        bufferedWriter.write(line);
        bufferedWriter.newLine();
    }
}
package com.mathieupauly.copypasta;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;

public class GrepPlugin implements Plugin {
    public String[] readArguments(BufferedReader stdin) throws IOException {
        System.out.println("Motif ?");
        return new String[]{stdin.readLine()};
    }

    public void processInput(BufferedWriter bufferedWriter, String line, String[] parameters) throws IOException {
        String motif = parameters[0];
        if (line.matches(motif)) {
            bufferedWriter.write(line);
            bufferedWriter.newLine();
        }
    }
}

7.13 Exercice

Charger les classes CpPlugin et GrepPlugin de manière dynamique. Utiliser Class.forName(String className).

7.14 Correction

package com.mathieupauly.copypasta2;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

public class CopyPasta {

    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        if (args.length != 2) {
            System.out.println("Arguments requis : " +
                    "<entrée> <sortie>");
            System.exit(1);
        }
        List<Plugin> plugins = new ArrayList<>();
        plugins.add((Plugin) Class.forName("com.mathieupauly.copypasta2.CpPlugin").getDeclaredConstructor().newInstance());
        plugins.add((Plugin) Class.forName("com.mathieupauly.copypasta2.GrepPlugin").getDeclaredConstructor().newInstance());
        for (int i = 0; i < plugins.size(); i++) {
            Plugin plugin = plugins.get(i);
            System.out.println("[" + i + "] " + plugin.getName());
        }
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        String input = bufferedReader.readLine();
        int selectedPluginIndex = Integer.parseInt(input);
        Plugin selectedPlugin = plugins.get(selectedPluginIndex);
        System.out.println("Plugin choisi : " + selectedPlugin.getName());
        selectedPlugin.readArguments();
        Path in = Paths.get(args[0]);
        List<String> lines = Files.readAllLines(in);
        Path out = Paths.get(args[1]);
        BufferedWriter bufferedWriter = Files.newBufferedWriter(out);
        selectedPlugin.setWriter(bufferedWriter);
        System.out.println("Traitement de " + in + " vers " + out);
        for (String line : lines) {
            selectedPlugin.processInput(line);
        }
        bufferedWriter.close();
    }

}

7.15 Exercice

Spécifier le nom de la classe dans une property en ligne de commande.

7.16  Correction

Dans le programme Java on utilisera System.getProperty().

String grepPluginClassName = System.getProperty("grepPluginClassName");
plugins.add((Plugin) Class.forName(grepPluginClassName).getDeclaredConstructor().newInstance());

Et en lançant le programme on injectera les properties comme la JVM nous l'impose :

java -DcpPluginClassName=com.mathieupauly.copypasta2.CpPlugin -DgrepPluginClassName=com.mathieupauly.copypasta2.GrepPlugin com.mathieupauly.copypasta2.CopyPasta a.txt b.txt

7.17 Exercice

Sortir la classe compilée du classpath

7.18 Correction

Quand on compile avec Maven, par défaut les classes sont générées dans le dossier target/classes. Pour indexer les classes, chacune est stockée dans un répertoire lié à son package.

+ target
    + classes
        + com
            + mathieupauly
                + copypasta2
                    - CopyPasta.class
                    - CpPlugin.class
                    - GrepPlugin.class
                    - Plugin.class

Si on ne spécifie pas de classpath, il vaut le répertoire courant (CWD).

On peut exclure GrepPlugin du classpath en déplacant le fichier dans un autre répertoire. Cette opération est purement pédagogique. Le but est de comprendre ce qu'est le classpath. On ne cherche jamais dans un travail de développement d'application à procéder de la sorte.

Je déplace GrepPlugin.class de sorte d'avoir les deux arborescence suivantes :

+ target
    + classes
        + com
            + mathieupauly
                + copypasta2
                    - CopyPasta.class
                    - CpPlugin.class
                    - GrepPlugin.class
                    - Plugin.class
+ target2
    + classes
        + com
            + mathieupauly
                + copypasta2
                    - GrepPlugin.class

En relançant le programme (à partir de target/classes), j'obtiens maintenant une exception :

java -DcpPluginClassName=com.mathieupauly.copypasta2.CpPlugin -DgrepPluginClassName=com.mathieupauly.copypasta2.GrepPlugin com.mathieupauly.copypasta2.CopyPasta a.txt b.txt
Exception in thread "main" java.lang.ClassNotFoundException: com.mathieupauly.copypasta2.GrepPlugin
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(Unknown Source)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(Unknown Source)
        at java.base/java.lang.ClassLoader.loadClass(Unknown Source)
        at java.base/java.lang.Class.forName0(Native Method)
        at java.base/java.lang.Class.forName(Unknown Source)
        at com.mathieupauly.copypasta2.CopyPasta.main(CopyPasta.java:26)

Quand on se place cette fois non pas dans le répertoire target/classes mais plus classiquement dans le répertoire du projet (qui contient target) on doit spécifier l'option -classpath target/classes/.

$ java -classpath target/classes/ -DcpPluginClassName=com.mathieupauly.copypasta2.CpPlugin -DgrepPluginClassName=com.mathieupauly.copypasta2.GrepPlugin com.mathieupauly.copypasta2.CopyPasta a.txt b.txt

On obtient la même erreur qu'avant.

7.19 Exercice

Réinclure explicitement la classe avec l'option -classpath.

7.20 Correction

Premier essai :

$ java -classpath target/classes/ -classpath target2/classes/ -DcpPluginClassName=com.mathieupauly.copypasta2.CpPlugin -DgrepPluginClassName=com.mathieupauly.copypasta2.GrepPlugin com.
mathieupauly.copypasta2.CopyPasta a.txt b.txt
Erreur : impossible de trouver ou de charger la classe principale com.mathieupauly.copypasta2.CopyPasta
Causé par : java.lang.ClassNotFoundException: com.mathieupauly.copypasta2.CopyPasta

J'ai essayé de spécifier deux classpath. Apparement seul le dernier est pris en compte puisque la CopyPasta.class n'est pas trouvé et qu'elle appartient à target/classes.

Il faut spécifier le classpath sous forme d'une liste de chemins séparé par des ; sous Windows et par des : sous Linux.

$ java -classpath 'target/classes/;target2/classes/' -DcpPluginClassName=com.mathieupauly.copypasta2.CpPlugin -DgrepPluginClassName=com.mathieupauly.copypasta2.GrepPlugin com.mathieupa
uly.copypasta2.CopyPasta a.txt b.txt
[0] cp
[1] grep
1
Plugin choisi : grep
Motif ?
bépo
Traitement de a.txt vers b.txt

Créer un JAR pour le plugin GrepPlugin.

Créer un nouveau WcPlugin (compte les lignes)

package com.example.foo;

class X {} // privé
package com.example.bar;

class Y {
    com.example.foo.X x; // Error:(4, 21) java: cannot find symbol
                         // symbol:   class X
                         // location: package com.example
}

8 Annexes et précis

8.1 Florilège à ne pas écrire

8.1.1 Pas de méthode statique abstraite dans une interface

interface Foo {
    static void bar(); // Error: missing method body, or declare abstract
}

class BarFoo implements Foo {
    @Override // oops
    public static Object get() {
        return null;
    }
}

Les interfaces sont destinées à contenir des méthodes abstraites et publiques (bien qu'on omette les modificateurs public et abstract).

En effet :

interface Foo {
    void bar();
}

Est équivalent à :

interface Foo {
    public abstract void bar();
}

On ne peut pas hériter d'une méthode statique.

Bien qu'il soit possible de placer un main dans une interface. Ce n'est pas recommandé :

public interface MainInInterface {
    static void main(String[] args) { // bizzarerie
        System.out.println("hello");
    }
}

8.1.2 Pas de function

De plus, la syntaxe suivante n'est pas du Java. Elle ressemble à ce qu'on pourrait avoir en JavaScript.

interface Foo {
    function bar();
}

8.1.3 Pas de return avec parenthèses

Éviter les parenthèses inutiles :

return (x);

Préférer :

return x;

L'opérateur return n'est pas une fonction (comme println par ex.). On écrira donc println(x); mais return x;

8.1.4 Pas de switch sans parenthèses

switch (x) {
}

et non pas comme en Swift :

switch x {
}

8.1.5 Pas de duplication

Éviter :

for (int i = 0; i < digitLength; i++) {
    int digit = n % 16;
    switch (digit) {
        case 0:
            out.append("0");
            break;
        case 1:
            out.append("1");
            break;
        case 2:
            out.append("2");
            break;
        ...
        case 15:
            out.append("F");
            break;
    }
    n /= 16;
}

Préférer :

String[] printedDigits = {"0", "1", "2", ..., "F"};
for (int i = 0; i < digitLength; i++) {
    int digit = n % 16;
    out.append(printedDigits[digit]);
    n /= 16;
}

Un switch qui contient du code qui se ressemble est une indication que le code est plus complexe qu'il ne devrait.

8.1.6 Pas de cast entre type primitif et référence

Attention le type int n'est pas castable en String. De manière générale, un type primitif n'est jamais castable en type référence. On pourrait le croire dans le cas de Integer n = 10;. Mais c'est un autre mécanisme que le cast qui est en jeu ici : l'auto-boxing.

Ainsi :

int n = 10;
String s = (String) n; // Error: incompatible types: int cannot be converted to java.lang.String

Pour avoir la représentation décimale du nombre n on pourra utiliser :

int address = 0xA;

String.valueOf(address); // 10
Integer.toString(address); // 10

8.1.7 Pas de méthode dans une méthode

En Java on ne peut pas définir une méthode dans le corps d'une une méthode.

class MethodInMethod {
    void foo() {
        void bar() { // Error illegal start of expression
                     // Error ';' expected

        }
    }
}

On observe le même principe pour 0 == false.

8.1.8 Faute de frappe et confusion

On écrit le type chaîne String et non pas string. La classe String en Java prend une capitale. C'est l'équivalent C# qui peut s'écrire : string.

On n'écrit pas Class Foo {} en majuscule, mais class Foo {}. Tous les mots-clés du langage sont en minuscule.

On écrit @Override et non pas @Overide. C'est l'annotation qui indique une méthode est héritée.

On écrit length et non pas lenght. C'est le champ de classe pour qui stocke la taille d'un tableau.

int[] array = {};
System.out.println(array.length);

8.2 Valeur par défaut

La valeur par défaut d'un type primitif est un élément neutre (pour la somme ou la disjonction logique ||) : une sorte de zéro.

class ValeurType {
    int a;
    double x;
    boolean ok;
    Object o;
    String s;
    
    void print() {
        System.out.println(a); // 0
        System.out.println(x); // 0.0
        System.out.println(ok); // false
        System.out.println(o); // null
        System.out.println(s); // null
    }
}

Pour les types référence c'est null. Cette référence spéciale qui est affectable à n'importe quelle type est assimilable à un zéro mais n'est pas neutre (cas de la NullPointerException).

Attention : String est un type référence. La valeur par défaut d'une String n'est pas la chaîne vide : "". C'est null. Contrairement à JavaScript il n'y a pas de conversion implicite de null à "".

La valeur par défaut n'est pas une valeur aléatoire qui dépend de l'état du système comme en C.

En revanche, une valeur par défaut est uniquement affectée pour les champs de classe mais pas pour les variables locales.

Ainsi, utiliser une variable locale qui peut être non initialisée est une erreur de compilation.

class ValeurDefaut {
    public static void main(String[] args) {
        int a;
        
        System.out.println(a); // Error: variable a might not have been initialized
    }
}

8.2.1 Opérateur ==

L'opérateur == vérifie à la compilation que les deux opérandes sont de types comparables (c-à-d. cast possible). Et il vérifie l'égalité des valeurs à l'exécution.

System.out.println(0 == 1);
System.out.println(0 == 0.0);
System.out.println(null == 0); // Error: incomparable types: <nulltype> and int
System.out.println(0 == false); // Error: incomparable types: int and boolean

L'expression null == 0 ne compile pas. L'erreur est comparable à une erreur de cast :

int i = (int) null; // Error: incompatible types: <nulltype> cannot be converted to int

8.2.2 Types primitifs

Type Valeur par défaut
byte 0
short 0
int 0
long 0L
float 0.0f
double 0.0d
char '000'
Object null
boolean false

https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html

Il n'existe pas de type qui soit la combinaison de types primitifs comme en C. Ainsi, ni long long ni long double par exemple n'existent en Java. En C, la taille d'un long dépend de l'implémentation. Ainsi on ne sait de long long qu'il n'est pas plus petit que long (c'est tout). En Java, la taille est normalisée dans la spécification (32 bits pour int).

https://en.wikipedia.org/wiki/C_data_types

8.2.3 Décalage de bits

class DecalageBits {
    public static void main(String[] args) {
        int a = 1;
        int b = a << 8;

        System.out.println(a); // 1
        System.out.println(b); // 256

        b = b >> 8;

        System.out.println(b); // 1
    }
}

L'expression a << 8 ne modifie pas a. Elle vaut a dont les bits ont été décalés de 8 places vers la gauche. Ansi, décaler un nombre d'un bit vers la gauche revient à le multiplier par 2. Décaler de deux bits multiplie par 4. Décaler de n bits multiplie par 2 puissance n.

L'opérande de droite indique le nombre de bits à décaler :

int source = 0x1;
int decalage = 3;

println(source << decalage); // 8
println(source >> decalage); // 0

Donc l'opérateur binaire << (resp. >>) n'est pas commutatif comme l'opérateur + peut l'être.

int mi = 2;
int sina = 3;
System.out.println(mi << sina); // 16
System.out.println(sina << mi); // 12

Enfin l'opérateur >> qui est l'inverse de << décale vers la droite. Ainsi : 2 >> 1 vaut 1.

Les opérateur << et >> sont associatifs. Ainsi l'expression :

1 << 2 >> 2 << 3 >> 3 // 1

S'associe de gauche à droite (comme tous les opérateurs binaires sauf l'affectation =).

(((1 << 2) >> 2) << 3) >> 3 // 1

8.3 Références

https://docs.oracle.com/javase/tutorial/

https://docs.oracle.com/javase/specs/