image/svg+xml $ $ ing$ ing$ ces$ ces$ Res Res ea ea Res->ea ou ou Res->ou r r ea->r ch ch ea->ch r->ces$ r->ch ch->$ ch->ing$ T T T->ea ou->r

Programmation objet en Java (résumé)

  1. Introduction
    1. Bibliographie et sources
    2. Dualité de Java
    3. Évolution de Java
    4. L'inévitable Hello World en Java
      1. Génération du bytecode à partir de la classe Java
      2. Exécution du bytecode
      3. A propos du classpath
    5. Utilisation de Maven
      1. A propos de Maven
      2. Création d'un projet Maven
      3. Utilisation de bibliothèques
      4. Génération de fat jar
    6. Langages de programmation
      1. Spécifité des langages
      2. Différents styles
      3. Quel langage choisir ?
    7. Problématique d'évolutivité de projet
    8. Programmation par classe
  2. Types
    1. Quelques généralités sur les types
    2. Typage en Java
    3. Déclarons des variables locales
    4. Deux types de types en Java
      1. Les types valeur (types primitifs)
      2. Les types référence (type objet)
      3. Les types valeur réifiés
        1. Présentation
        2. Consommation mémoire
        3. Un (malheureux) exemple d'unboxing
  3. Les méthodes
    1. Définition d'une méthode
    2. Code idiomatique (boilerplate)
      1. En-tête de fichier
        1. Déclaration de package
        2. Importations
          1. Importations de classes
          2. Importation de membres statiques
      2. Méthodes
        1. Getter
        2. Setter
        3. equals et hashCode
          1. equals
          2. hashCode
        4. Méthode toString()
      3. Vers moins de code idiomatique
      4. Classes Record
    3. Passage de paramètres à une méthode
      1. Différence entre types valeur et référence
      2. Passons des paramètres
  4. Le sous-typage
  5. Héritage de classe
    1. Forçage ou interdiction de l'héritage en Java
    2. Classes scellées
    3. Pattern matching
    4. Utilité de l'héritage
  6. Ajout de champ et méthode
  7. Redéfinition de méthode
    1. Principe
    2. Un exemple de redéfinition problématique
  8. Masquage et résolution de champ
  9. Interface
  10. Membre statique
  11. Visibilité en Java
    1. Règles d'or de la visibilité
  12. Allocation mémoire
  13. Désallocation mémoire
  14. Paquetages
  15. Modularisation (Jigsaw)
  16. Quelques bonnes conventions javanaises
  17. Patterns créationnels
    1. Construction d'une instance
    2. Singleton
      1. Principe du singleton
      2. Exemple de singleton pour calculer des nombres de Fibonacci
    3. Fabrique (factory)
      1. Principe de la fabrique
      2. Construction d'une Map avec une fabrique
    4. Construction par prototype
      1. Clonons des brebis
    5. Objets immuables
    6. Injection de dépendances
  18. Polymorphisme : surcharge
    1. Exemple de polymorphisme avec surcharge
    2. Résolution ambigüe
  19. Coercition (cast)
    1. Coercitions ascendantes
    2. Coercitions descendantes
  20. Styles de classes en Java
    1. Classe fichier
    2. Classe et interface interne
    3. Classe anonyme
      1. Classe anonyme avec Swing
    4. Lambda
      1. Liste filtrante avec lambda
      2. Interfaces fonctionnelles de l'API
      3. Un peu de programmation fonctionnelle...
  21. Exceptions
    1. Principe
    2. Try-catch-finally
    3. Pour en savoir plus sur les exceptions...
  22. Assertions
  23. JShell
  24. java.util.Collection
    1. Iterable et Iterator
    2. Comparaison d'éléments
    3. Table de hachage HashSet
    4. Ensemble ordonné SortedSet
    5. D'un Set à une Map
    6. Exemple : ensemble des fichiers d'un répertoire

Duke fait du surf

Source de l'image

💾 Les exemples de code Java présentés sur cette page peuvent être téléchargés sous la forme d'une archive ZIP ici

Introduction

Bibliographie et sources

Dualité de Java

Désigne à la fois :

  1. Un langage de programmation (orienté objet)
  2. Une spécification de machine virtuelle (Java Virtual Machine)
    • Existence de nombreux compilateurs permettant de compiler du code dans des langages divers (Python, Ruby...) en bytecode Java
  3. Une bibliothèque standard (API) fournie par Java Standard Edition (JSE) permettant :
    • de manipuler des fichiers et des flots d'octets et de caractères (java.io, java.nio),
    • de gérer des collections d'éléments (java.util)
    • de réaliser des communications réseau (java.net), de mener des opérations cryptographiques (javax.crypto)
    • ... et bien plus en utilisant Java Enterprise Edition qui introduit des APIs pour créer des applications web

Évolution de Java

Évolutions notables du langage :

Évolutions notables de la Java Virtual Machine (JVM) :

Evolutions notables de l'API :

Évolutions notables des outils du JDK :

Processus d'évolution de Java :

L'inévitable Hello World en Java

// @run: HelloWorld UPEM
public class HelloWorld
{
	public static void main(String[] args)
	{
		System.out.println(String.format("Hello World %s", args[0]));
	}
}

Quelques remarques :

Génération du bytecode à partir de la classe Java

Obtention du fichier bytecode HelloWorld.class à partir du code source HelloWorld.java
Utilisation du compilateur Javac livré avec le Java Development Kit (JDK) :

  1. javac HelloWorld.java : génération de HelloWorld.class dans le répertoire courant
  2. javac -d bin src/HelloWorld.java : génération de HelloWorld.class dans le répertoire bin (bonne pratique de séparer répertoires de source et de bytecode)

Décompilation de HelloWorld.class avec javap -c HelloWorld.class afin de visualiser le bytecode :

Compiled from "HelloWorld.java"
public class HelloWorld {
  public HelloWorld();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String Hello World %s
       5: iconst_1
       6: anewarray     #4                  // class java/lang/Object
       9: dup
      10: iconst_0
      11: aload_0
      12: iconst_0
      13: aaload
      14: aastore
      15: invokestatic  #5                  // Method java/lang/String.format:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
      18: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      21: return

Remarque :

Exécution du bytecode

Exécution du bytecode avec la machine virtuelle Java en utilisant java :

  1. java HelloWorld arg0 arg1... si la classe HelloWorld est dans le répertoire courant
  2. java -cp bin HelloWorld arg0 arg1... si la classe HelloWorld est dans le répertoire bin/

Raccourci possible depuis Java 11 en une seule commande pour la compilation et l'exécution (ne fonctionne que pour les classes solitaires) : java HelloWorld.java arg0 arg1...

Possibilité de rendre une classe Java exécutable depuis Java 11 (systèmes Unix) :

  1. Ajout d'un shebang en tête du fichier (1ère ligne) : #!/path/to/java --source version
  2. Ajout de la permission d'exécution (x) sur le fichier : chmod u+x HelloWorld
  3. Exécution de la classe depuis un shell : ./HelloWorld

A propos du classpath

Utilisation de Maven

A propos de Maven

Création d'un projet Maven

Utilisation de bibliothèques

Rajout d'une dépendance (bibliothèque téléchargée depuis Maven Central) dans le pom.xml (exemple avec jollyday) :

<dependency>
        <groupId>de.jollyday</groupId>
        <artifactId>jollyday</artifactId>
        <version>0.5.10</version>
</dependency>

Génération de fat jar

Génération d'un fat jar avec Maven :

Langages de programmation

Spécifité des langages

Différents styles

Quelques exemples de tâches de programmation classiques réalisées en utilisant différents langages sur Rosetta Code (site de chrestomathie de langages).

Quel langage choisir ?

Cela dépend des applications et contraintes :

Mixer des langages dans un même projet ?

Un peu d'amusement : code golf

Problématique d'évolutivité de projet

Un projet naît, vit, évolue, grandit... mais on souhaiterait éviter qu'il meure. Comment faire ?

Programmation par classe

Types

Quelques généralités sur les types

Typage statique en Java... mais la JVM supporte les langages à typage dynamique (notamment avec l'instruction de bytecode invokedynamic pour appeler une méthode dont on ne connaît pas à la compilation le type des arguments)

Typage en Java

Déclarons des variables locales

// @run: VarTest
// This class cannot be tested with Doppio JVM (using features from the JDK 10)
public class VarTest
{
	public static void main(String[] args)
	{
		int i = 42; // déclaration avec initialisation immédiate
		int j; // déclaration avec initialisation retardée (valeur par défaut: 0)
		System.out.println("i=" + i + ",j=" + j);
		ArrayList<Integer> l1 = new ArrayList<>(); // déclaration d'une liste (initialisée comme une ArrayList)
		List<Integer> l2 = l1; // possible car ArrayList hérite de l'interface List (coercition ascendante)
		
		// Inférence automatique du type depuis Java 10 en utilisant le type de l'initialisation
		var set = new TreeSet<String>(); // déclaration et initialisation d'un TreeSet vide de String
		var set2 = new TreeSet<>(); // impossible car le type des éléments dans le set n'est pas indiqué
		var k1 = 128; // déclaration et initialisation d'un int
		var k2 = 128L; // déclaration et initialisation d'un long
		var k3 = 128.0; // déclaration et initialisation d'un double
		var k4 = 128.0f; // déclaration et initialisation d'un float
		var s; // impossible car le type n'est pas inférable (pas d'initialisation)
	}
}

Deux types de types en Java

Les types valeur (types primitifs)

42 dans tous ses états

Les types référence (type objet)

Les types valeur réifiés

Présentation
Consommation mémoire

Les types réifiés sont plus consommateurs de mémoire que les types valeur :

Consommation mémoire type réifié

Petit exercice : comment stockeriez-vous en mémoire le génôme complet d'un être humain (génôme ~ 4 milliards de bases pouvant prendre 4 valeurs A, C, G ou T) ?

Un (malheureux) exemple d'unboxing
List<Integer> l = new ArrayList<>();
... // code ajoutant des éléments dans la liste: l.add(null); l.add(1); l.add(new Integer(7)); ...
Integer i =  l.get(0); // pour obtenir le 1er élément de la liste
int v = i * i; // on calcule le carré de l'élément (unboxing automatique)
// l'unboxing échoue avec NullPointerException si i est une référence nulle

Les méthodes

Définition d'une méthode

Une méthode se définit par les informations suivantes :

Code idiomatique (boilerplate)

Le code et méthodes présentés ci-après peuvent être (plus ou moins intelligemment) générées automatiquement par un environnement de développement tel qu'Eclipse ou IntelliJ IDEA.

En-tête de fichier

Déclaration de package

Une classe peut être :

  1. dans le paquetage par défaut (par d'organisation en paquetages)
  2. dans une hiérarchie de paquetages

Dans le second cas, la déclaration du paquetage en tête de fichier est obligatoire.
Par exemple si HelloWorld est dans le paquetage fr.upem.greeter, nous crééons le fichier HelloWorld.java dans le répertoire src/fr/upem/greeter.
Le fichier HelloWorld.java doit commencer par la ligne :

package fr.upem.greeter;

Importations

Les directives d'importation sont placées en tête du fichier après l'éventuelle déclaration de paquetage.

Importations de classes

Une classe manipulée doit être importée :

Deux types d'importations :

Il faut éviter les importations wildcard car pouvant provoquer des conflits de noms potentiels.
☞ On peut laisser l'environnement de développement réaliser automatiquement les imports (avec l'autocomplétion)

Exemple de conflit : comment utiliser java.util.List et java.awt.List dans le même fichier ?
On peut importer une des deux classes et utiliser le nom complet de l'autre

import java.util.List;

public class NameClash
{
	public static void main(String[] args)
	{
		List<String> l = ...;
		java.awt.List l2 = ...;
		...
	}
}

⚠ Pour limiter les ambiguïtés, on évitera de nommer ses propres classes avec des noms déjà utilisés dans la bibliothèque standard.

Importation de membres statiques

On peut importer des membres statiques (champs, méthodes) avec import static.

Par exemple pour importer la constante Pi : import static java.lang.Math.PI
ou si l'on veut importer toutes les méthodes statiques et constantes de Math : import static java.lang.Math.*
ce qui est pratique pour faire des calculs mathématiques :

import static java.lang.Double.parseDouble;
import static java.lang.Math.*;

// @run: Haversine 36.12 -86.67 33.94 -118.40
/** Copied from https://rosettacode.org/wiki/Haversine_formula#Java */
public class Haversine {
    public static final double R = 6372.8; // In kilometers, radius of the Earth
    
    public static double haversine(double lat1, double lon1, double lat2, double lon2) {
        double dLat = toRadians(lat2 - lat1);
        double dLon = toRadians(lon2 - lon1);
        lat1 = toRadians(lat1);
        lat2 = toRadians(lat2);
 
        double a = pow(sin(dLat / 2),2) + pow(sin(dLon / 2),2) * cos(lat1) * cos(lat2);
        double c = 2 * asin(sqrt(a));
        return R * c;
    }
    
    public static void main(String[] args) {
        System.out.println(haversine(parseDouble(args[0]), parseDouble(args[1]), parseDouble(args[2]), parseDouble(args[3])));
    }
}

Méthodes

Getter

Un getter permet de récupérer un attribut de la classe.

Stratégies d'implantation de getter :

package fr.upem.jacosa.general;

// @run: Point
public class Point
{
	private final int x;
	private final int y;
	
	public Point(int x, int y)
	{
		this.x = x;
		this.y = y;
	}
	
	public int getX() { return x; }
	
	public int getY() { return y; }
	
	private int maxCoord = Integer.MIN_VALUE;
	
	public int getMaxCoord()
	{
		if (maxCoord == Integer.MIN_VALUE) // Integer.MIN_VALUE is a forbidden value
			maxCoord = Math.max(this.x, this.y);
		return maxCoord;
	}
}

Passage possible d'une stratégie à une autre sans modifier le code externe (avantage par rapport à l'utilisation directe d'un champ).

Setter

Un setter permet de modifier un attribut de la classe pour lui attribuer une nouvelle valeur.

Créer un setter n'a de sens que pour un champ modifiable (pas de setter pour un champ final).

Remarque : certains langages permettent d'appeler des getters/setters avec une notation de champs (Kotlin, Python...) mais ce n'est pas (encore) le cas de Java.

equals et hashCode
equals

Contrat demandé pour la méthode equals() :

Pièges courants :

Comparaison avec d'autres langages :

Exemple 1 : comparons deux personnes

package fr.upem.jacosa.general;

import java.util.Objects;

class GenericPerson
{
	private final String name;
	private final int birthYear;
	
	public GenericPerson(String name, int birthYear)
	{
		this.name = name;
		this.birthYear = birthYear;
	}
	
	@Override
	public boolean equals(Object other)
	{
		if (this == other) return true; // pas la peine de se fatiguer plus longtemps si les références sont égales
		if (other == null) return false; // forcément this != null
		// le test qui suit garantit le respect de la symétrie en cas de descendance
		
		// if (! other instanceof Person) return false; // à éviter car cela brise la symétrie en cas d'héritage
		if (! getClass().equals(other.getClass())) return false; // les classes des deux objets doivent être identiques
		
		// si les classes des deux objets sont les mêmes, on est sûr de la symétrie : la méthode equals de this est la même que celle de other
		GenericPerson other2 = (GenericPerson)other; // cast nécessaire, aucun risque d'exception car on a préalablement testé l'égalité des classes
		return Objects.equals(name, other2.name) && birthYear == other2.birthYear; // on compare le contenu
	}
}
	
class Student extends GenericPerson
{
	private final String curriculum;
	
	public Student(String name, int birthYear, String curriculum)
	{
		super(name, birthYear);
		this.curriculum = curriculum;
	}
	
	@Override
	public boolean equals(Object other)
	{
		// Exploitons l'implantation de la classe ancêtre... et ajoutons la comparaison du curriculum
		return super.equals(other) && Objects.equals(curriculum, ((Student)other).curriculum);
	}
}

public class StudentTest
{
	public static void main(String[] args)
	{
		Person p = new Person("John Doe", 1995);
		Student s = new Student("John Doe", 1995, "DUT2info");
		System.out.println("p.equals(s) ? " + p.equals(s));
		System.out.println("s.equals(p) ? " + s.equals(p));
	}
}

Exemple 2 : comparons deux chaînes de caractères

package fr.upem.jacosa.general;

public class StringEquality {
	public static final String S1 = "toto";
	public static final String S2 = "toto";
	public static final String S3 = "tototo";
	
	public static void main(String[] args) {
		System.out.println(S1 == S2); // true car une seule chaîne conservée pour S1 et S2 (même référence)
		System.out.println(S1.equals(S2)); // true
		String s4 = S3.substring(2); // contenu de s4: toto
		System.out.println(S1 == s4); // false car la chaîne s4 a été créé dynamiquement
		System.out.println(S1.equals(s4)); // true car même contenu des chaînes
	}
}

hashCode

Contrat de la méthode public int hashCode() :

Comment calculer la valeur de hachage d'un objet ?

Exemple sur Person (auto-généré par le brave Eclipse) :

public class Person
{
	...
	
	@Override
	public int hashCode()
	{
		int v = birthYear;
		v = v * 65539;
		v += name.hashCode();
		return v;
	}
}

Méthode toString()

La méthode toString() permet d'avoir une représentation textuelle de l'objet.
Elle est surtout utile pour les sorties sur System.{out,err}, le débuggage.
Une méthode toString() par défaut est implantée dans Object et affiche le nom de la classe avec le code de hachage.

// @run: ToStringTest
public class ToStringTest
{
	public static void main(String[] args)
	{
		Object obj1 = new Object();
		Object obj2 = new Object();
		System.out.println("obj1=" + obj1);
		System.out.println("obj2=" + obj2);
	}
}

On redéfinit la méthode toString() pour un affichage plus explicite. On peut inclure le nom de la classe ainsi que la valeur des champs.

package fr.upem.jacosa.general;

import java.util.Objects;

class VerbosePerson
{
	private final String name;
	private final int birthYear;
	
	public VerbosePerson(String name, int birthYear)
	{
		this.name = name;
		this.birthYear = birthYear;
	}
	
	public String getFieldString()
	{
		return String.format("name=%s,birthYear=%d", name, birthYear);
	}
	
	@Override
	public final String toString()
	{
		return getClass().getSimpleName() + "[" + getFieldString() + "]";
	}
}
	
class VerboseStudent extends VerbosePerson
{
	private final String curriculum;
	
	public VerboseStudent(String name, int birthYear, String curriculum)
	{
		super(name, birthYear);
		this.curriculum = curriculum;
	}
	
	@Override
	public String getFieldString()
	{
		return String.format("%s,curriculum=%s", super.getFieldString(), curriculum);
	}
}

public class VerboseStudentTest 
{
	public static void main(String[] args)
	{
		VerbosePerson p = new VerbosePerson("Harry Cover", 2000);
		VerbosePerson p2 = new VerboseStudent("Jane Doe", 2001, "DUT2 info");
		System.out.println("p=" + p);
		System.out.println("p2=" + p2);
	}
}

☞ Pour un projet conséquent, il sera plus avantageux d'utiliser un Logger beaucoup plus puissant et paramétrable que System.err.println.

Vers moins de code idiomatique

Comment limiter le code idiomatique ?

Classes Record

Introduction depuis Java 14 de la structure record :

Exemple : un point avec un Record :

public record Point(int x, int y) { }

Passage de paramètres à une méthode

Différence entre types valeur et référence

Passons des paramètres

Définissons une classe Box avec un champ entier :

package fr.upem.jacosa.general;

// @run: IncTest

public class IncTest 
{
	// Voici une classe pour conserver une valeur
	
	public static class Box implements Cloneable 
	{ 
		public int field; // should implement getter/setter, just for testing
		
		/** Clone the current box */
		public Box clone()
		{
			try {
				return (Box)super.clone();
			} catch (CloneNotSupportedException e) {
				throw new RuntimeException(e);
			}
		}
	}

	// Écrivons différentes méthodes statiques d'incrémentation :

	/** Increment the given integer parameter
	 * This method has no outside effect since the value of the parameter is copied on the stack
	 * The new incremented value is then lost when we exit from the method 
	 */
	public static void inc1(int i)
	{
		i++;
	}

	/** We increment the field inside the Box object given as parameter
	 * We note that Box object reference is final (not its content)
	 */
	public static void inc2(final Box b)
	{
		b.field++;
	}

	/** We increment the first cell of the integer array passed as parameter */
	public static void inc3(final int[] a)
	{
		assert(a.length >= 1) ; // Check that the array contains at least one element
		a[0]++;
	}
	
	// Écrivons une méthode main pour tester ces différentes méthodes d'incrémentation :

	public static void main(String[] args)
	{
		final Box box = new Box(); // box.field == 0
		box.field = 1;
		inc1(box.field);
		System.out.println(box.field); // 1
		inc2(box);
		System.out.println(box.field); // 2
		final int[] array = new int[]{box.field};
		inc3(array);
		System.out.println(array[0]); // 3
		System.out.println(box.field); // 2
		inc2(box.clone());
		System.out.println(box.field); // 2
	}
}

Le sous-typage

Héritage de classe

Forçage ou interdiction de l'héritage en Java

Classes scellées

public abstract sealed class Student permits ApprenticeStudent, InitialStudent {
	
	public Student(String name, int birthYear) {
		this.name = name;
		this.birthYear = birthYear;
	}
	
	public String getName() { return name; }
	public int getBirthYear() { return birthYear; }
}

// Inherited classes are forbidden
public final class ApprenticeStudent extends Student {
	private String company;
	
	public ApprenticeStudent(String name, int birthYear, String company) {
		super(name, birthYear);
		this.company = company;
	}
	
	public String getCompany() {
		return this.company;
	}
}

// Inherited classes are authorized
public non-sealed class InitialStudent extends Student {
	public InitialStudent(String name, int birthYear) {
		super(name, birthYear);
	}
}

Une classe héritière d'une classe scellée peut être :

Pattern matching

Exemple : des étudiants doivent-il avoir des devoirs supplémentaires ?

public static boolean shouldHaveExtraWork(Student s) {
	switch (s) {
		case ApprenticeStudent as -> { return as.getName().length() % 2 == 0; /* because we don't like students whose name contains an even number of letters */ }
		case InitialStudent is -> { return true; }
		// no need to add a default -> { ... }, since all the cases are treated
		// Student is sealed with only two permitted children: ApprenticeStudent and InitialStudent
	}
}

L'ajout d'un cas traitant une référence nulle peut être plus prudent. Si s est null, le premier cas est considéré ApprenticeStudent (avec un NullPointerException assuré) :

public static boolean shouldHaveExtraWork(Student s) {
	switch (s) {
	  case null -> { return false; }
		case ApprenticeStudent as -> { return as.getName().length() % 2 == 0; /* because we don't like students whose name contains an even number of letters */ }
		case InitialStudent is -> { return true; }
	}
}

La JEP 420 prévoit des guarded patterns (encore expérimentaux) permettant de faire accompagner le test de type avec un test booléen sur l'objet. On peut ainsi réécrire l'exemple précédent :

public static boolean shouldHaveExtraWork(Student s) {
	switch (s) {
	  case null -> { return false; }
		case ApprenticeStudent as && (as.getName().length() % 2 == 0) -> { return true; }
		case ApprenticeStudent as -> { return false; }
		case InitialStudent is -> { return true; }
	}
}

Utilité de l'héritage

Ajout de champ et méthode

Ajout de champ et méthode

Redéfinition de méthode

Principe

Exemple de redéfinition de la méthode protected clone() throws CloneNotSupportedException de la classe Object :

package fr.upem.jacosa.general;

/** A simple box containing an integer
 *  demonstrating the change of visibility of the method {@link Object#clone()},
 *  the change of the return type (covariance)
 *  and the capture of the exception possibly thrown (by transforming it into a {@link RuntimeException}).
 */
public class CloneableBox implements Cloneable
{
	private int value;
	
	public int getValue()
	{
		return value;
	}
	
	@Override
	public CloneableBox clone()
	{
		try {
			return (CloneableBox) super.clone();
		} catch (CloneNotSupportedException e) {
			throw new RuntimeException(e); // never reached in fact
		}
	}
}

Un exemple de redéfinition problématique

Objectif : dériver ArrayList pour empêcher la présence de références nulles

public class NotNullArrayList<T> extends ArrayList<T>
{
	// ...
	
	@Override
	public boolean add(T object)
	{
		if (object == null) return false ; // do nothing if the argument is null
		else return super.add(object) ;
	}
	
	// ...
}

Problème : cela ne suffit pas car de nombreuses autres méthodes peuvent ajouter des éléments qu'il faudrait redéfinir :

  1. 1ère solution
    • Avoir une ArrayList avec une méthode abstraite boolean checkElementToAdd(T e) appelée systématiquement avant d'ajouter un élément (pour filtrer les éléments à ajouter)
    • Implantation possible ensuite de classes dérivées avec tests personnalisés
    • Malheureusement, on ne peut modifier ArrayList de l'API Java !
  2. 2ème solution
    • Encapsuler une ArrayList dans une autre classe avec une méthode add déléguant son travail à l'ArrayList uniquement après filtrage
    • On implante le minimum de méthodes dont a besoin en exposant un sous-ensemble des fonctionnalités d'ArrayList
    • Malheureusement, on ne peut pas utiliser cette nouvelle classe là où des ArrayList étaient utilisées dans le code (car elle n'est pas un sous-type de ArrayList)

Masquage et résolution de champ

Exemple :

package fr.upem.jacosa.general;

// @run: MaskingField
public class MaskingField
{
	public static class Ancestor { public int x = 0 ; }
	public static class Child extends Ancestor { public double x = 1.0 ; }
	
	public static void main(String[] args)
	{
		Ancestor a = new Child();
		System.out.println(a.x); // 0
		System.out.println(((Child)a).x); // 1.0
	}
}

Interface

package fr.upem.jacosa.general;

interface TaxableItem
{
	/** Return the VAT rate of the item */
	public float getVATRate();
	
	/** Return the raw price (tax excluded) of the article */
	public int getRawPrice();
	
	/** Return the price with the tax included */
	public default int getTaxIncludedPrice() 
	{ 
		return (int)(getRawPrice() * (1.0f + getVATRate())); 
	}
}

interface Nameable
{
	public String getName();
}

class Food implements TaxableItem, Nameable
{
	public static final float FOOD_VAT_RATE = 0.055f; // rate in France
	
	private final String name;
	private final int rawPrice;
	
	public Food(String name, int rawPrice)
	{
		this.name = name;
		this.rawPrice = rawPrice;
	}
	
	@Override
	public String getName()
	{
		return this.name;
	}
	
	@Override
	public float getVATRate() 
	{
		return FOOD_VAT_RATE;
	}
	
	@Override
	public int getRawPrice()
	{
		return rawPrice;
	}
}

// @run: FoodTester
public class FoodTester
{
	public static void main(String[] args)
	{
		Food apple = new Food("apple", 1000);
		System.out.println("Price of " + apple + " (with tax): " + apple.getTaxIncludedPrice());
	}
}

Membre statique

Un exemple avec la création de plaques d'immatriculation française (avec le système SIV) :

package fr.upem.jacosa.general;

// @run FrenchCarPlate 1500
/** A registration plate from a French car */
public final class FrenchCarPlate
{
	private static FrenchCarPlate nextPlate = new FrenchCarPlate("AA", 1, "AA");
	
	private static String LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
	
	private final String prefix;
	private final int number;
	private final String suffix;
	
	private FrenchCarPlate(String prefix, int number, String suffix)
	{
		this.prefix = prefix;
		this.number = number;
		this.suffix = suffix;
	}
	
	public String toString()
	{
		return String.format("%s-%03d-%s", prefix, number, suffix);
	}
	
	private static String findNextLetters(String s)
	{
		char letter1 = s.charAt(0);
		char letter2 = s.charAt(1);
		int pos1 = LETTERS.indexOf(letter1);
		int pos2 = LETTERS.indexOf(letter2);
		if (pos2 < LETTERS.length()-1)
			pos2++;
		else if (pos1 < LETTERS.length()-1) {
			pos2 = 0;
			pos1++;
		} else
			return null; // cannot find a successor for ZZ
		return "" + LETTERS.charAt(pos1) + LETTERS.charAt(pos2);
	}
	
	/** Get the plate following a given plate */
	private FrenchCarPlate getNextPlate()
	{
		String newPrefix = prefix;
		String newSuffix = suffix;
		int newNumber = number + 1;
		if (newNumber == 1000)
		{
			newNumber = 1;
			newSuffix = findNextLetters(suffix);
			if (newSuffix == null) 
			{
				newSuffix = "AA";
				newPrefix = findNextLetters(prefix);
				if (newPrefix == null) newPrefix = "AA"; // we cycle
			}
		}
		return new FrenchCarPlate(newPrefix, newNumber, newSuffix);
	}
	
	public static FrenchCarPlate createNewPlate()
	{
		try {
			return nextPlate;
		} finally {
			nextPlate = nextPlate.getNextPlate();
		}
	}
	
	public static void main(String[] args)
	{
		int n = Integer.parseInt(args[0]);
		for (int k = 0; k < n; k++)
			System.out.println(createNewPlate());
	}
}

Visibilité en Java

private défaut protected public
Classe Intra-paquetage Partout
Classe interne Classe englobante Intra-paquetage Classe et enfants, classe englobante et enfants, intra-paquetage Identique à la classe englobante
Membre de classe Classe Intra-paquetage Classe et enfants, intra-paquetage Identique à la classe
Interface partout

Règles d'or de la visibilité

Allocation mémoire

Désallocation mémoire

package fr.upem.jacosa.general;

/** A class testing the finalize method */
// @run: Finalizer 3000
public class Finalizer
{
	/** Keep the number of instances of the class */
	private static int INSTANCES = 0;
	
	public Finalizer()
	{
		INSTANCES++; // we initialize a new instance
	}
    
    public static int getInstanceNumber()
    {
		return INSTANCES;
	}
	
	/** This method is called before the destruction of the object by the garbage collector */
	protected void finalize() throws Throwable
	{
		super.finalize();
		INSTANCES--;
	}
	
	/** Let's test the class */
	public static void main(String[] args) throws Exception
	{
		int sleepTime = Integer.parseInt(args[0]); // sleep time is in args[0] in milliseconds
		Finalizer[] finalizers = new Finalizer[2];
		System.out.println(getInstanceNumber()); // should print 0
		finalizers[0] = new Finalizer();
		System.out.println(getInstanceNumber()); // should print 1
		finalizers[1] = new Finalizer();
		System.out.println(getInstanceNumber()); // should print 2
		finalizers = null; // the array is not reachable now (and transitively the finalizers)
		System.gc();
		Thread.sleep(sleepTime);
		System.out.println(getInstanceNumber()); // has the garbage collector destroyed the finalizers?
	}
}

Paquetages

Exemple de compilation de classe :

Modularisation (Jigsaw)

Pour exporter toutes les classes (publiques) du paquetage fr.upem.zoo.api (dans un fichier module-info.java à la racine des sources) :

module fr.uge.zoo
{
	exports fr.uge.zoo.api;
} 

Et si un module fr.uge.vivarium a besoin de notre paquetage api de zoo, il peut déclarer une dépendance :

module fr.uge.vivarium
{
	requires fr.uge.zoo.api;
}

Différentes variantes de l'instruction requires existent :

Quelques bonnes conventions javanaises

Patterns créationnels

Construction d'une instance

Singleton

Principe du singleton

Exemple de singleton pour calculer des nombres de Fibonacci

package fr.upem.jacosa.general;

import java.util.ArrayList;
import java.util.List;

// @run: FiboSequence

/** A Fibonacci sequence computer */
public class FiboSequence
{
	private static FiboSequence defaultInstance;
	
	/** Return the default instance (and create it if required for the first time */
	public static FiboSequence getDefaultInstance()
	{
		if (defaultInstance == null)
			defaultInstance = new FiboSequence(1, 1);
		return defaultInstance;
	}
	
	private final List<Long> fiboList =
		new ArrayList<Long>();
		
	/** Build the FiboSequence object, 
	  * note that the constructor is private 
	  */
	private FiboSequence(long first, long second)
	{
		fiboList.add(first);
		fiboList.add(second);
	}
		
	/** Compute the n-th number of Fibonacci */
	public long getValue(int rank)
	{
		// first, compute the missing numbers
		while (rank >= fiboList.size())
		{
			int n = fiboList.size();
			fiboList.add(fiboList.get(n-1) + fiboList.get(n-2));
		}
		return fiboList.get(rank);
	}
	
	/** Main method to compute the Fibonacci sequence term whose index is passed as args[0] */
	public static void main(String[] args)
	{
		int rank = Integer.parseInt(args[0]);
		System.out.println(getDefaultInstance().getValue(rank));
	}
}

Fabrique (factory)

Principe de la fabrique

Construction d'une Map avec une fabrique

package fr.upem.jacosa.general;

import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

// @run: MapFactory
public class MapFactory
{
	private boolean sorted ;

	void setSorted(boolean sorted)
	{
		this.sorted = sorted ;
	}

	public <K, V> Map<K, V> create()
	{
		if (sorted)
			return new TreeMap<K, V>();
		else
			return new HashMap<K, V>();
	}
	
	/** We test the factory */
	public static void main(String[] args)
	{
		MapFactory mf = new MapFactory();
		Map<Integer, Integer> m1 = mf.create(); // Création d'une HashMap
		System.out.println(m1.getClass()); // should print HashMap
		mf.setSorted(true);
		Map<Integer, Integer> m2 = mf.create(); // Création d'une SortedMap
		System.out.println(m2.getClass()); // should print TreeMap
	}
}

Construction par prototype

Clonons des brebis

package fr.upem.jacosa.general;

import java.util.Arrays;

public class SheepCloning
{
	public interface Animal
	{
		public char[] getDNA();
	}
	
	public static class Sheep implements Animal
	{
		protected char[] dna;
		public char[] getDNA() { return dna; }
		
		public Sheep(String bases)
		{
			this.dna = bases.toCharArray();
		}
		
		@Override
		public String toString()
		{
			return getClass().getSimpleName() + "[dna=" + Arrays.toString(dna) + "]";
		}
	}
	
	public static class CloneableSheep extends Sheep implements Cloneable
	{
		public CloneableSheep(String bases)
		{
			super(bases);
		}
		
		public CloneableSheep shallowClone()
		{
			try {
				return (CloneableSheep)super.clone();
			} catch (CloneNotSupportedException e) {
				throw new RuntimeException(e);
			}
		}

		public CloneableSheep clone()
		{
			CloneableSheep sheep = shallowClone();
			sheep.dna = new char[this.dna.length];
			System.arraycopy(this.dna, 0, sheep.dna, 0, this.dna.length);
			return sheep;
		}
	}
	
	public static class Virus
	{
		public void attack(Animal animal)
		{
			char[] dna = animal.getDNA();
			for (int i = 0; i < dna.length; i+=2)
				dna[i] = 'A'; // replace all the pair bases with Adenosine
		}
	}
	
	public static void main(String[] args)
	{
		Sheep dolly = new CloneableSheep("ACGT");
		Sheep dolly2 = ((CloneableSheep)dolly).shallowClone();
		Sheep dolly3 = ((CloneableSheep)dolly).clone();
		
		System.out.println("Dolly before virus attack: " + dolly);
		System.out.println("Dolly2 before virus attack: " + dolly2);
		System.out.println("Dolly3 before virus attack: " + dolly3);
		
		Virus virus = new Virus(); // We create a nasty virus
		virus.attack(dolly2); // The virus modifies the shared DNA of dolly2 and dolly
		
		System.out.println("Dolly after virus attack: " + dolly);
		System.out.println("Dolly2 after virus attack: " + dolly2);
		System.out.println("Dolly3 after virus attack: " + dolly3);
	}
}

Objets immuables

Un objet immuable (immutable) ne peut pas changer d'état (champs figés).
On peut rendre un objet immuable en déclarant tous ses champs final.
Pour tout changement, il faut créer une nouvelle version de l'objet

Exemples de classes produisant des objets immuables dans l'API :

Par exemple pour incrémenter un BigInteger, il faut créer un nouveau BigInteger :

BigInteger bi = new BigInteger("41");
BigInteger bi2 = bi.add(BigInteger.ONE);

Injection de dépendances

Java sait déjà détruire automatiquement des objets (ramasse-miettes) mais peut-il automatiquement les instancier ?

Quelques systèmes d'injection de dépendance :

Injectons un calculateur de suite de Fibonacci :

public class FiboRatio
{
	@Inject
	FiboSequence sequence;
	
	/** Get the Fibonacci ratio at a given rank (should converge to the golden number for high ranks) */
	public double getRatio(int rank)
	{
		if (rank < 1) return Double.NaN;
		return (double)sequence.getValue(rank) / (double)sequence.getValue(rank-1);
	}
}

Polymorphisme : surcharge

Exemple de polymorphisme avec surcharge

package fr.upem.jacosa.general;

// @run: PolymorphismTester
public class PolymorphismTester
{
	static abstract public class Animal
	{
		protected void speak(Animal other, String message)
		{
			other.receive(this, message) ;
		}

		public void speak(Animal other)
		{
			speak(other, "default speech");
		}

		protected void receive(Animal sender, String message)
		{
			System.out.println(sender + " says to " + this + ": " + message) ;
		}
		
		public void run()
		{
			System.out.println("Running...");
		}
	}
	
	static class Cat extends Animal
	{
		public void speak(Animal other) { speak(other, "miaou") ; }
		public void speak(Human other) { speak(other, "miiiiaou") ; }
		public void speak(Dog other) { speak(other, "") ; run() ; }
	}
	
	static class Dog extends Animal
	{
		public void speak(Animal other) { speak(other, "ouaf") ; }
		public void speak(Human other) { speak(other, "ouaf ouaf") ; }
		public void speak(Cat other) { speak(other, "OUAF") ; run() ; }
	}
	
	static class Human extends Animal {}
	
	public static void main(String[] args)
	{
		Animal cat = new Cat() ;
		Cat cat2 = (Cat)cat ;
		Animal dog = new Dog() ;
		Human human = new Human() ;
		// we consider cat as an animal (that has only a method speak(Animal)
		cat.speak(human) ; // miaou
		cat.speak(dog) ; // miaou
		cat.speak((Dog)dog) ; // miaou
		// we consider cat2 as a cat (that has methods to speak with an animal, human and dog)
		cat2.speak(human) ; // miiiiaou
		cat2.speak(dog) ; // miaou
		cat2.speak((Dog)dog) ; // stay mute and run
		cat2.speak((Human)dog) ; // ClassCatException!
		// cat2.speak((Cat)human) ; // do not compile (statically detected cast error) !
	}
}

Résolution ambigüe

package fr.upem.jacosa.general;

// @run: AmbiguousResolution
public class AmbiguousResolution
{
	int x(int a, byte b) { return 0; }
	int x(byte a, int b) { return 1; } 

	public static void main(String[] args)
	{
		// System.out.println(x(0, 0)) ;
		// System.out.println(x((byte)0, (byte)0));
	}
}

Coercition (cast)

long a = 1234567891234L;
int b = (int)a; // cast avec perte de précision
long c = b; // pas besoin de cast (conversion automatique)
...
List<Integer> l1 = new ArrayList<>(); // cast automatique ascendant
ArrayList<Integer> l2 = (ArrayList<Integer>)l1; // cast manuel descendant

Coercitions ascendantes

❕ Coercition ascendante explicite généralement inutile :

Coercitions descendantes

❕ Coercition descendante évitable :

En Java < 5 sans generic :

List l = new ArrayList();
l.add(new Integer(0));
l.add(new Integer(1));
...
Integer firstInt = (Integer)l.get(0);

En Java ≥ 5 avec generics (et auto-boxing et unboxing) :

List<Integer> l = new ArrayList<>();
l.add(0);
l.add(1);
...
int firstInt = l.get(0);

Conclusion : généralement la coercition peut être limitée (utile dans de rares cas)

☞ Toujours vérifier avant de faire une coercition descendante si l'objet est issu de la classe attendue avec instanceof.

Exemple : collecte de toutes les plaques d'immatriculation d'un tableau de Vehicle.

Définissons les classes (seule la classe Car a une méthode String getPlate()) :

abstract class Vehicle { ... }
class Bicycle extends Vehicle { ... }
class Car extends Vehicle { public String getPlate() }

Ecrivons la méthode de collecte de plaques :

List<String> collectPlates(Vehicle[] vehicles) {
	var list = new ArrayList<String>();
	for (var v: vehicles) {
		// type of v: Vehicle
		// one cannot call v.getPlate() since Vehicle has not a method getPlate()
		// we must cast v to a car (only if it is possible)
		if (v instanceof Car c)
			list.add(c.getPlate());
	}
}

Avant Java 16, l'affectation doit être faite à la main avec le cast :

if (v instanceof Car) {
	Car c = (Car)v; // one can use also: var c = (Car)v;
	list.add(c.getPlate());
}

Depuis Java 18, un switch peut être utilisé pour faire des tests sur le type d'un objet :

public sealed interface Expr permits IntExpr, OpExpr {}
public record IntExpr(int value) implements Expr {}
public sealed interface OpExpr permits AddExpr, SubExpr {}
public record AddExpr(int a, int b) implements OpExpr { }
public record SubExpr(int a, int b) implements OpExpr { }

....
public static int eval(Expr expr) {
	switch(expr) {
		case IntExpr ie -> return ie.value();
		case AddExpr ae -> return ae.a() + ae.b();
		case SubExpr se -> return se.a() + se.b();
	}
}

Styles de classes en Java

Classe fichier

Classe et interface interne

Classe anonyme

Classe anonyme avec Swing

package fr.upem.jacosa.general;

import javax.swing.*;        
import java.awt.event.*;

public class HelloSwing {
    private static void showHello() {
        final JFrame frame = new JFrame("HelloSwing");
        final JButton button = new JButton("Hello Swing!");
	button.addActionListener(new ActionListener() {
		public void actionPerformed(ActionEvent e) { frame.dispose(); }
	});

        frame.getContentPane().add(button);
        frame.pack();
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
            public void run() { showHello(); }
        });
    }
}

Lambda

Liste filtrante avec lambda

package fr.upem.jacosa.general;

import java.util.AbstractCollection;
import java.util.ArrayList;
import java.util.Iterator;

// @run: FilteringArrayList
/** A list that does not add null values (we override the add method) */
public class FilteringArrayList<T> extends AbstractCollection<T>
{
	public interface Filter<T>
	{
		public boolean accept(T e) ;
	}
	
	private final ArrayList<T> list ;
	private final Filter<T> filter ; 
	
	public FilteringArrayList(Filter<T> filter)
	{
		list = new ArrayList<T>() ;
		this.filter = filter ;
	}

	public boolean add(T e)
	{
		if (filter.accept(e)) return list.add(e);
		else return false;
	}
	
	@Override
	public int size() { return list.size(); }
	
	@Override
	public Iterator<T> iterator() { return list.iterator(); }
	
	
	public static void main(String[] args)
	{
		// Instantiation of a list
		// not containing null references

		// Without lambda
		FilteringArrayList<Integer> l1 = new FilteringArrayList<Integer>(
			new Filter<Integer>()
			{
				public boolean accept(Integer e)
				{
					return e != null ;
				}
			}) ;
		l1.add(1);
		l1.add(2);
		l1.add(null);
		System.out.println("Size of l1: " + l1.size());

		// With lambda
		FilteringArrayList<Integer> l2 = 
			new FilteringArrayList<Integer>(e -> e != null) ;
		l2.add(1);
		l2.add(2);
		l2.add(null);
		System.out.println("Size of l2: " + l1.size());
	}
}

Interfaces fonctionnelles de l'API

Un peu de programmation fonctionnelle...

Philosophie de la programmation fonctionnelle : les fonctions sont des valeurs comme les autres. On peut passer une fonction en paramètre d'une fonction, une fonction peut retourner une fonction...
A partir de Java 8, l'approche fonctionnelle a été rendue syntaxiquement plus simple.

package fr.upem.jacosa.general;

import java.util.function.*;

// @run: FuncTest
public class FuncTest
{
	public static long mult(int x, int y)
	{
		return (long)x * (long)y;
	}

	public static <U, T, R> Function<U, R> compose(Function<U, T> f1, Function<T, R> f2)
	{
		return (x) -> f2.apply(f1.apply(x));
	}

	public static <U, V, R> Function<U,R> partialApply(BiFunction<U, V, R> bifunc, V arg2)
	{
		return arg -> bifunc.apply(arg, arg2);
	}

	public static Function<Long, Integer> logFunction(int base)
	{
		return compose(x -> (x > 0)?Long.toString(x, base):"", x -> x.length() - 1);
	}

	public static void main(String[] args)
	{
		Function<Integer, Long> mult2 = partialApply(FuncTest::mult, 2);
		System.out.println("Let's multiply 7 by 2: " + mult2.apply(7));

		Function<Long, Integer> log2 = logFunction(2);
		Function<Long, Integer> log10 = logFunction(10);
		long n = 1037L;
		System.out.println(String.format("Logs for %d: base2=%d, base10=%d", n, log2.apply(n), log10.apply(n)));
	}
}

Exceptions

Principe

Try-catch-finally

try {
	/* Code susceptible de lever une exception */
} catch (ExceptionSpecifique1 e1)
{
	/* Code de traitement de l'exception */
	/* Peut éventuellement lever une nouvelle exception avec e pour cause */
} catch (ExceptionSpecifique2|ExceptionSpecifique3 e2)
{
	/* On peut capturer des exceptions de plusieurs types à la fois
	   En les séparant par des | depuis Java 7 */
} catch (ExceptionGenerale e3)
{
	/* Code de traitement pour un type d'exception plus général */
} finally {
	/* Le block finally est systématiquement exécuté, qu'il y ait ou non une exception.
	   En cas d'exception, le catch adéquat s'il existe est préalablement exécuté
	   Un seul catch (le premier pertinent) peut être exécuté pour une exception */
}

try ( InputStream is = new FileInputStream(« monFichier ») ;
	BufferedInputStream bis = new BufferedInputStream(is))
{
	// Lecture du fichier
}

Depuis Java 7, le try-with-resources permet de libérer des ressources déclarées en appelant automatiquement leur méthode close() (classe implantant l'interface AutoCloseable).

Pour en savoir plus sur les exceptions...

Lien vers un cours plus détaillé sur les exceptions

Assertions

JShell

Exemple pratique : testons si des chaînes sont des palindromes

jshell> boolean isPalindrome(String x)
{
	for (int i = 0; i < x.length() / 2; i++)
		if (x.charAt(i) != x.charAt(x.length() - 1 - i))
			return false;
	return true;
}
|  created method isPalindrome(String)

jshell> isPalindrome("radar")
$2 ==> true
jshell> isPalindrome("sonar")
$3 ==> false

java.util.Collection<T>

Classes héritant de Collection&lt;T&gt;

Iterable<T> et Iterator<T>

Comparaison d'éléments

Table de hachage HashSet

Ensemble ordonné SortedSet

D'un Set à une Map

Exemple : ensemble des fichiers d'un répertoire

package fr.upem.jacosa.general;

import java.io.File;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

// @run: FileLister

/** List all the files of a directory (sorted by name and modification date) */
public class FileLister
{
	public static void main(String[] args)
	{
		final String dir = args[0];
		// we retrieve the directory listing into an array of files
		File[] files = new File(dir).listFiles();
		
		// we add the files into a SortedSet (the natural ordering on files is based on the lexicographic order of their names)
		SortedSet<File> sortedFiles = new TreeSet<>() ;
		for (File f: files) sortedFiles.add(f) ;
		
		// we use an other SortedSet with an explicit order based on the modification date of the file
		SortedSet<File> sortedFiles2 = new TreeSet<>( (f1, f2) -> Long.compare(f1.lastModified(), f2.lastModified()) ) ;
		for (File f : files) sortedFiles2.add(f) ;

		System.out.println("Files sorted by name: " + sortedFiles);
		System.out.println("Files sorted by modification date: " + sortedFiles2);
	}
}