Cours 1 - Évolution du langage Java - Langages de la JVM II

Latex_titlePage.png


situation de Java dans l'écosystème (2024)
  • langage indéniablement important
  • mais en perte de vitesse ?
où se situe Java relativement aux autres langages (importants) de programmation
  • est-il cantonné à certains usages ?
  • n'est-il là que pour du code legacy ?
où se situe Java relativement aux autres langages (importants) de la JVM
  • Java ( ~1995)
  • Groovy (~2003)
  • Scala (~2004)
  • Clojure (~2007)
  • Kotlin (~2011)

Intérêt des internautes pour les langages : formulations de questions

Intérêt des internautes pour les langages : recherche de tutoriels

Intérêt des employeurs pour les langages : publication de postes

JobOffers_Nov23.png



Introduction au module

Questions abordées dans le module
  • évolution du langage Java
  • éléments de programmation fonctionnelle en Java
  • programmation orientée données
  • patrons de conception en Java
  • JVM : interopérabilité, Kotlin
  • situation du/de la développeuse



Livres à étudier (liste non limitative)

book_BuildingGreenSoftware.jpg book_AIAssistedProgramming.jpg book_TheProgrammersBrain.jpg book_TheCreativeProgrammer.jpg
book_CleanCodeCookbook.jpg book_DevOpsToolsForJavaDevelopers.jpg book_ModernizingEnterpriseJava.jpg book_JVMPerformanceEngineering.jpg

Principales références du module

book_ModernJavaInAction.jpg book_TheWellGroundedJavaDeveloper.jpg book_JavaToKotlin.jpg book_KotlinInAction.jpg

Évolutions du système de types du langage

motivations
  • plus de commodité, de sécurité, d'expressivité
  • condition du maintien du langage


Classes d'énumération :enum Java_logo.pngJava 1.5

  • Les classes d'énumérations sont spécifiées parenum
  • Nombre limité d'instances (énumérées) : des constantes d'énumération
enum MoyenPaiement {
	ESPECES, CHEQUE, CARTE 
}
  • Les constantes d'énumération peuvent être directement comparées avec l'opérateur == (il n'en existe qu'une copie)
MoyenPaiement moyenPaiement = ...;
// ...
if (moyenPaiement == MoyenPaiement.ESPECES) { ... }
  • Elles peuvent être employées dans les cas d'une instruction ou expression switch, sans nécessité de mentionner le type d'enum
switch (moyenPaiement) {
	case ESPECES -> ...
}
  • Les classes d'énumération sont des sous-classes de la classejava.lang.Enum. Il est possible de récupérer le nom d'une constante d'énumération (toString()❶), de trouver une constante d'énumération à partir de son nom (valueOf()❷), et de trouver toutes les valeurs possibles d'une énumération (values()❸).
MoyenPaiement.CARTE.toString(); // renvoie "CARTE" ❶
MoyenPaiement mp;
mp = (MoyenPaiement) Enum.valueOf(MoyenPaiement.class, "ESPECES"); // ❷
mp = MoyenPaiement.valueOf("ESPECES"); // ❷
for (MoyenPaiement moyen : MoyenPaiement.values()) // ❸
	System.out.print(moyen + " "); // ESPECES CHEQUE CARTE

Type des classes d'énumération

  • Les classes d'énumération sont bien un type de classes :
    • il est possible de leur ajouter des champs ❶ et des méthodes ❷, ainsi que des constructeurs ❸
public enum Taille {
  SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("X");
  private String lettre; // ❶
  private Taille(String lettre) { this.lettre = lettre; } // ❸
  public String getLettre() { return lettre; } // ❷
}

// ...

Scanner in = new Scanner(System.in);
System.out.println("Taille (SMALL, MEDIUM, LARGE, EXTRA) : ");
String input = in.next().toUpperCase();
Taille taille = (Taille)Enum.valueOf(Taille.class, input); // ...
System.out.println(taille.toString() + " (" + taille.getLettre() + ")");
  • Contraintes sur les classes d'énumération
    • elles héritent implicitement de java.lang.Enum, donc uniquement d'elle
    • elles ne peuvent pas servir de super classe
    • elles peuvent implémenter des interfaces

Classes d'énumération: exemple

  • Utiles pour des ensembles de constantes dont les valeurs sont connues à la compilation (choix d'un menu, options d'une commande, etc.)
public enum Jour {
    LUNDI, MARDI, MERCREDI, JEUDI, VENDREDI, SAMEDI, DIMANCHE
}

// ...

class TestEnum {
    Jour jour;
    public TestEnum(Jour jour) { this.jour = jour; }
    public String leDireEnVrai() {
        return switch (jour) {
            case LUNDI : yield "Les lundi sont durs.";
            case VENDREDI : yield "Les vendredi sont mieux.";
            case SAMEDI, DIMANCHE : yield "Rien ne remplace les week-ends.";
            default : yield "Les autres jours sont comme ci, comme ça.";
        };
    }    

    public static void main(String[] args) {
        TestEnum jour1 = new TestEnum(Jour.LUNDI);
        System.out.println(jour1.leDireEnVrai());
        // ...
    }
}



Classes internes (nested classes) Java_logo.pngJava 1.1

Les classes internes (nested classes)

  • sont définies à l'intérieur d'autres classes
  • peuvent accéder aux données de leur classe englobante (dont privées)
  • peuvent être cachées des autres classes du même package (private)
public class ClasseExterne {
	// ...
 	class ClasseInterne {
	 	// ...
	}
}
types particuliers de classes internes:
  • possibilité de déclarer une classe dans une méthode: classes locales
  • possibilité de définir une classe dans une méthode sans nommer la classe: classes anonymes

Implémentation des classes internes

Les classes internes sont traitées au niveau du compilateur (pas de la machine virtuelle)
  • traduites en fichiers de classes réguliers, en utilisant des signes $ ​ pour séparer les noms de classes externes et internes (ex. ClasseExterne\$ClasseInterne.class ❶).
  • Le compilateur fournit à la classe interne une référence à l'objet de sa classe externe qui l'a créé ❷.
public class ClasseExterne {
	public ClasseExterne() { }
	class ClasseInterne {
		public ClasseInterne() { }
		public void faitQuelqueChoseInterne() { }
	}
	public void faitQuelqueChoseExterne() { }
}
  • Utilisation du désassembleur Javajavap :
$ javap -private ClasseExterne\$ClasseInterne # ❶
Compiled from "ClasseExterne.java"
class ClasseExterne$ClasseInterne {
	 final ClasseExterne this$0; ❷
	 public ClasseExterne$ClasseInterne(ClasseExterne);
	 public void faitQuelqueChoseInterne();
}


Classes internes non static (inner classes)

  • ClasseInterne n'existe pas indépendamment de ClasseExterne : la portée d'une classe interne est limitée à celle de sa classe englobante
  • Un objet deClasseInterne reçoit implicitement une référence à l'objet qui l'a créé : ainsi, une méthode de ClasseInterne a directement accès aux membres de l'objet externe créateur de type ClasseExterne, comme depuis le contexte de ClasseExterne





Classes internes: exemple

  • Définition d'un itérateur particulier (implémentant java.util.Iterator<Integer>) dans une classe interne ❶ : celle-ci devient disponible pour les besoins internes de sa classe englobante ❷.
public class Test {

  // ...
  
  public void afficheIndicesPairs() {
    IterateurPair iterateur = new IterateurPair(); // ❷
    while (iterateur.hasNext())
	    System.out.print(iterateur.next() + " ");
  }

  private class IterateurPair implements java.util.Iterator<Integer> { // ❶

    private int indiceSuivant = 0;
    @Override public boolean hasNext() {
      return (indiceSuivant <= TAILLE - 1);
    }        

    @Override public Integer next() throws NoSuchElementException {
      if (!hasNext()) throw new NoSuchElementException();
      Integer valeur = Integer.valueOf(tab[indiceSuivant]);
      indiceSuivant += 2;
	  return valeur;
    }
  }
}


Classes internes locales

  • Lorsqu'une classe n'est nécessaire que très localement, il est possible de la déclarer à l'intérieur d'une méthode
  • Sa portée est alors restreinte au bloc de déclaration: ne nécessite donc pas de spécificateur d'accès
public class ClasseExterne {

  private boolean beep;
  // ...

  public void start(final String message) {

	class AfficheurTemps implements ActionListener {
      public void actionPerformed(ActionEvent event) {
        System.out.println(message + new Date());
        if (beep) Toolkit.getDefaultToolkit.beep();
      }
    }

    ActionListener listener = new AfficheurTemps();
    Timer timer = new Timer(interval, listener);
    timer.start();
  }
  // ...
}

Classes internes anonymes

  • Lorsqu'un seul objet d'une classe n'est utile que localement, il est possible d'avoir une classe interne ne portant pas de nom
    • impossible (donc) de spécifier un constructeur
  • Syntaxe générale:
new SuperType(paramètres de construction) {
  données et méthodes de la classe interne
}
  • Adapté pour des codes courts, peu adapté pour des besoins de classes (internes) relativement complexes
  • Utilisé par exemple pour la programmation d'interfaces graphiques (le champs label ​ ❶ appartient à la classe englobante et est donc directement accessible; cette classe particulière a donc bien un fonctionnement très spécifique à sa classe englobante), ex.:
final TextField tfValeurNum = new TextField() {
	@Override public void replaceText(int start, int end, String text) {
      if (!text.matches("[a-z,A-Z]")) {
        super.replaceText(start, end, text);                     
      }
      else label.setText("Enter a numeric value"); // ❶
    }
};

Classes internes statiques

  • Parfois il n'est pas nécessaire de fournir à la classe interne une référence à un objet de la classe externe
  • Possibilité d'associer une classe staticvisible comme sous-classe depuis l'extérieur
  • Une classe interne static ne peut accéder à l'état d'un objet et ne peut donc accéder qu'aux membres static de sa classe englobante (ou via un objet de ce type)-
    en anglais, on dénote l'ensemble des classes internes par nested classes, et les classes internes non static par inner classes

  • Exemple : classe interne static MinMax.PairMinMax ❶ utilisée comme type spécifique de retour par sa classe englobante ❷.
public class MinMax {

  public static class PaireMinMax { // ❶
    private double min, max;
    public PaireMinMax(double min, double max) { this.min = min; this.max = max; }
    public double getMin() { return min; }
    public double getMax() { return max; }
  }

  public static PairMinMax minmax(double[] values) {
    double min = Double.MAX_VALUE, max = Double.MIN_VALUE;
    for (double value : values) {
      if (min > value) min = value;
      if (max < value) max = value;
    }
    return new PairMinMax(min, max); // ❷
  }

  public static void main(String[] args) {
    double[] valeurs = ...; 
    MinMax.PaireMinMax résultat = MinMax.minmax(valeurs);
    System.out.println("min = " + résultat.getMin() + ...
  }
}
  • Exemple d'interface interne static des API standard
    MapEntry.png


Classes d'enregistrement : record Java_logo.pngJava 16

  • Une classe record déclare des champs (syntaxe spéciale pour une liste de composants (component list)), et laisse au compilateur la création de code pouvant être déduit de sa définition
    - toString()❶, hashCode()❷, equals()❸, accesseurs (getters) (qui portent directement le nom du champ) ❹, constructeur public ❺
    - protège notamment de la modification de ses champs (déclarés final et sans accesseurs (setters))
    cette protection est-elle totale ?

  • Exemple :
public record Achat(int nombre,
	PaireDevises paire, 
	double prix, 
	LocalDateTime dateOrdre) {}
  • classe générée :
$ javap Achat.class
Compiled from "Achat.java"
public final class Achat extends java.lang.Record {
	public Achat(int, PairDevises, double, java.time.LocalDateTime); ❺
	public java.lang.String toString(); ❶
	public final int hashCode(); ❷
	public final boolean equals(java.lang.Object); ❸
	public int nombre(); ❹
	public PaireDevises paire(); ❹
	public double prix(); ❹
	public java.time.LocalDateTime dateOrdre();}

Implémentation

  • Unrecord est un transporteur d'un ensemble fixe de données, qui correspondent à un champ final pour lequel une méthode de consultation (getter) portant le nom du champ est créée

  • Points d'implémentation

    • un record est final (donc pas de sous-classes possibles)
    • java.lang.Record est la classe parente (implicite) de tous les record
      • classe abstraite
      • redéclare equals(), hashCode()et toString()avecabstract
      • pas d'héritage explicite possible pour un record, mais possibilité d'implémenter des interfaces
  • Il est possible d'ajouter des méthodes, constructeurs et champs static à un record

    • ex. une méthode ageActuel() pour une classe Personne (par ailleurs immuable)
    • l'intention des classes d'enregistrement est de permettre un regroupement immuable de données
  • Un record peut être paramétré : record Paire<T>(T x, T y) {}


Constructeurs compacts des classes d'enregistrement

  • Un constructeur canonique est créé automatiquement pour l'initialisation de l'ensemble des champs d'une instance de record.
    • possibilité de définir des constructeurs (canoniques ou non), en réemployant pour les paramètres les noms des composants du record
  • Possibilité d'intervenir sur la procédure de construction en ne vérifiant que les valeurs de paramètres qui doivent l'être à l'aide d'un constructeur compact.
  • Cela autorise la validation lors de la construction (ce que ne permettrait pas e.g. de simples tuples), ex. :
public Achat {
	if (nombre < 1) {
		throw new IllegalArgumentException("La quantité doit être positive"); 
	}
	// ...
}
d'après le Java Language Specification

The formal parameters of a compact constructor of a record class are implicitly declared. They are given by the derived formal parameter list of the record class. The intention of a compact constructor declaration is that only validation and/or normalization code need be given in the body of the canonical constructor; the remaining initialization code is supplied by the compiler.



Modification d'objets immuables : les withers

  • Lorsqu'il est utile d'appliquer une modification sur un objet qu'on veut immuable, un nouvel objet (lui-même immuable) peut être créé et retourné par une méthode permettant l'altération
  • Le nouvel objet aura l'état de l'objet d'origine, avec la seule modification demandée
  • Méthodes qualifiées de "withers" (on veut tel objet, avec telle modification) ❶
class Livre {

	private String titre;
	private int édition; 
	// ...

	public Livre(String titre, int édition) {
		if (titre == null) throw new NullPointerException("titre");
		this.titre = titre;
		this.édition = édition;
	}

	public Livre withEdition(int édition) { // ❶
		return new Livre(titre, édition);
	}
	// ...
}
La création en série d'objets pour réaliser plusieurs altérations créé des objets intermédiaires qui auront vocation à être supprimés par le ramasse-miettes
Livre nouvelleEdition = ancienneEdition.withEdition(2).withFormat(FormatLivre.BROCHE).withISBN("...");

Construction d'objets par propriété : les builders

  • Parfois on est tenté de définir de nombreux constructeurs surchargés pour couvrir les différents paramètres dont on peut disposer
public Livre(String titre, int édition) { ... }
public Livre(String titre, int édition, FormatLivre livre) { ... }
  • On serait intéressé par une solution plus facile à lire, et qui pourrait en outre permettre facilement l'ajout de nouvelles propriétés aux objets
  • On peut déléguer à un objet proche de la classe (... d'une classe interne static !) la construction de nouvelles instances
    • on précisera les paramètres de construction un par un (ils seront accessibles pour la classe interne) ❶
    • pour ne pas avoir à instancier explicitement un objet de cette classe builder, on peut invoquer une méthode staticde la classe qui retourne le builder
    • à la fin de la construction, on demande la construction effective du nouvel objet, qui peut donc être immuable ❸
    • dans cette approche, on peut ne pas fournir de constructeurs pour la classe qu'on construit pour certaines visibilités ❹
public class Livre {

	private Livre() { } // ❹

	public static LivreBuilder builder() { // ❷
		return new LivreBuilder();
	}

	public static class LivreBuilder {
		private String titre;
		private int édition; 
		// ...
		LivreBuilder() { }
		public LivreBuilder titre(String titre) { // ❶
			this.titre = titre;
			return this;
		}
		// ...
		public Livre build() { // ❸
			if (titre == null || titre.isBlank()) throw new IllegalArgumentException();
			Livre livre = new Livre();
			livre.titre = titre;
			livre.édition = édition;
			// ...
			return livre;			
		}
	}

}
  • Cette approche de construction repose alors sur des appels chaînés à l'instance de constructeur, suivis de l'appel de build()
Livre nouveauLivre = Livre.builder()
	.titre("Le bug humain")
	.édition(1)
	.build();


Types scellés (sealed types) Java_logo.pngJava 17

  • Java est extensible par défaut (sinon une classe est explicitement marquée final)
    - pour des classes ouvertes, des sous-types peuvent apparaître longtemps après la création de leur type parent (...)

  • Possibilité de limiter les sous-types possibles d'un type scellé (mot-clépermits)

  • Exemple d'interface scellée autorisant uniquement 2 records ​ particuliers

public sealed interface Achat permits AchatPrixMarché, AchatPrixLimité {
	int nombre(); 
	PairDevises paire();
	LocalDateTime dateOrdre(); 
} 
	
public record AchatPrixMarché(int nombre, PaireDevises paire, LocalDateTime dateOrdre) implements Achat { ... }
public record AchatPrixLimité(int nombre, PaireDevises paire, LocalDateTime dateOrdre, double prix) implements Achat { ... }
  • précédemment, possibilité de relation IS-A et HAS-A, maintenant IS-EITHER-X-OR-Y
    • entre les classes ouvertes (par défaut) et les classes final
    • le patron des enum appliqué aux types plutôt qu'aux instances

Implémentation

  • Si les types dérivés ne sont pas listés à la déclaration du type scellé, ceux-ci doivent nécessairement apparaître comme membres internes de ce type
  • Exemple : définition de types limités InscritPolytech.Etudiant et InscritPolytech.Apprenti
public abstract sealed class InscritPolytech {
	// ...

	public static final class Etudiant extends InscritPolytech {
		// ...
	}
	
	public static final class Apprenti extends InscritPolytech {
		// ...
	}
}


Forme moderne des instruction et expression switch

  • Evolutions de l'instruction et de l'expression de branchement switch : commodité pour le/la développeuse, meilleure lisibilité et sécurité
  • Les branchements 'case L:' , qui autorisent des branchements en cascade, peuvent être utilement remplacés par des branchements 'case L ->' Java_logo.pngJava 17
    • la notation 'case L:' requiert l'utilisation de break (sortie de l'instruction) ou de yield (retour d'une valeur pour l'expression)
    • la notation 'case L1, L2 ->' permet d'énumérer les cas ❶, et les associe soit à une expression, une propagation d'exception (throw) ou un bloc d'instructions (terminé par yield pour l'expression) ❷
boolean estUneVoyelle = switch (lettre) {
	case 'a', 'e', 'i', 'o', 'u', 'y' -> true; // ❶
	default -> { // ❷
		System.err.println("On a testé une non-voyelle : " + lettre);
		yield false;
	}
}
  • Les expressions switch doivent couvrir l'ensemble des cas possibles, et donc requiert une clause default
    • une expression switch doit donc soit retourner une valeur du type de l'expression pour tous cas, soit propager une exception
    • dans les cas où le type de la valeur de branchement est une classe d'énumération (enum), le compilateur peut vérifier l'exhaustivité de la couverture des cas
String messageDHumeur (jour) {
	case LUNDI -> "Les lundi sont durs.";
	case MARDI -> "Le mardi c'est chaud.";
	case MERCREDI -> "Le mercredi ça commence à tirer...";
	case JEUDI -> "Les jeudis sont interminables...";
	case VENDREDI -> "Les vendredi sont plutôt pas mal.";
	case SAMEDI, DIMANCHE ->"Rien ne remplace les week-ends.";
};

Correspondances de patrons (pattern matching) dans les instructions et expressions switch

  • Extension des possibilités de pattern matching avec switch Java_logo.pngJava 21 (JEP 441)
    • l'expression de sélection (paramètre du switch) peut être n'importe quel type de référence ou int
    • les instructions et expressions switch peuvent tester si leur expression de sélection correspond à un patron (possiblement "null")
      • précédemment, cela ne permettait que de tester des valeurs constantes
Object objet = ...;
switch (objet) {
	case null     -> System.out.println("valeur null");
	case String s -> System.out.println("chaîne de caractère : " + s);
	default       -> System.out.println("un objet autre que String");
}
  • Permet de fonder le code associé à chaque cas sur une référence finement typée de l'objet transmis
public static double calculPérimètre(Forme forme) throws IllegalArgumentException {
	return switch (forme) {
		case Rectangle r -> 2 * r.longueur() + 2 * r.largeur();
		case Cercle c -> 2 * c.rayon() * Math.PI;
		default -> throw new IllegalArgumentException("périmètre non calculable pour cette forme (si non null)");
	};
}
  • Introduction d'une clause when ​ pour spécifier des sous-cas (avec règles de précédence)
	périmètre = switch (forme) {
		case Rectangle r when r.estBienFormé() -> 2 * r.longueur() + 2 * r.largeur();
		case Rectangle r -> throw new IllegalArgumentException("impossible de calculer le périmètre de ce rectangle");
		default -> throw new IllegalArgumentException("périmètre non calculable pour cette forme (si non null)");
	};
  • Correspondance pour des classes d'enregistrement (record), avec décomposition de la classe, possibilité d'inférence de type (var) et de patron anonyme (_ ​)
record Rectangle(double largeur, double hauteur, Couleur couleur) { }

static double calculeAire(Forme forme) {
	return switch (forme) {
		case Rectangle(double largeur, double hauteur, _) -> largeur * hauteur;
		default -> throw new IllegalStateException("calcul d'aire inconnu pour : " + forme);
	}



Suivi des évolutions du langage

  • Suivi des évolutions du langage à l'aide de site tels que Java Almanach
    TheJavaVersionAlmanach.png

  • Possibilité de comparer entre deux versions des APIs (ex. Java 17 (LTS) vers Java 21 (LTS))
    TheJavaPlayground_diff21-17.png

  • Dans le cadre professionnel, on n'utilise typiquement pas des versions non LTS (Long-Term Support) en production

  • On peut toutefois installer localement un nouveau JDK pour expérimenter avec ces nouvelles possibilités (fonctionnalités, performance, correction de bugs, incompatibilités pour montée de version, etc.)

  • On peut aussi expérimenter en ligne avec The Java Playgound
    TheJavaPlayground.png