La boîte à outils de Java
Génériques
Java a introduit un autre type de polymorphisme depuis Java 1.5, dit paramétrique, qui permet de typer et contraindre les classes. Ces contraintes s’expriment au moyen d’une notation entre chevrons.
En reprenant l’exemple des animaux, et en supposant que l’application concerne un zoo, la modélisation inclura des enclos regroupant différents animaux. Dans ces enclos, il s’agit d’absolument éviter de mélanger des carnivores et des herbivores. Les génériques permettent de mettre en place une telle sécurité.
Exemple
public class Enclos<T extends Animal> {
private T[] animaux;
...
public ajouter(T animal) {
...
}
}
L’exemple précédent définit une notion d’enclos pour toutes sortes d’animaux, grâce à la notation entre chevrons (< >). Cette notation définit un type particulier, avec l’alias T qui sera utilisé pour tous les objets étendant la classe Animal. Cet alias peut alors être utilisé à l’intérieur de la classe en substitution du type.
La classe Enclos peut ensuite être instanciée avec l’opérateur diamant depuis Java 1.7 comme ceci :
Exemple d’utilisation...
Collections
Quand un logiciel est modélisé à l’aide de classes, il est nécessaire de créer des abstractions de données. Néanmoins, il est tout aussi important de pouvoir utiliser ces données en tant qu’ensemble.
Il est possible d’utiliser des tableaux pour cela, comme :
Animal[] animaux = new Animal[42];
Mais les tableaux ne sont pas très flexibles : ils ne se redimensionnent pas (on dit qu’ils ont une taille fixe), il n’y a pas de méthode… Pour bénéficier de toutes ces caractéristiques, une collection est un outil très puissant.
Le concept de collection est simple. Il correspond aux collections de timbres, de coquillages… Des objets de même nature sont placés à l’intérieur d’une collection. Java a créé depuis sa première version dans le package java.util des classes et des interfaces permettant d’aider à modéliser ces ensembles de données, et met à disposition plusieurs types de collections qui diffèrent par leurs caractéristiques : ordonnée ou non, doublons autorisés ou non, valeur nulle admise ou non, tri par défaut ou non… Toutes ont la capacité de se redimensionner automatiquement lors de l’ajout ou de la suppression d’objets.
Une instance d’une collection...
Gestion des erreurs
Le quotidien d’un programme et de ses développeurs est de gérer les erreurs. Il est même correct de dire que dans un contexte industriel, la gestion des erreurs représente plus de temps et de code que le fonctionnement nominal.
Il faut donc dans tout programme apprendre à gérer les mauvaises données saisies par un utilisateur (il rentre des lettres à la place de chiffres pour un prix), à gérer les erreurs techniques (la connexion à la base de données ne se fait plus ou le réseau est tombé), à créer des stratégies de compensation (retenter une connexion au maximum trois fois avant d’abandonner...).
Afin de gagner en clarté, Java propose un mécanisme de gestion des erreurs par exception. Il s’agit d’exécuter une partie de code susceptible de remonter des exceptions (donc des erreurs) en l’entourant et en fournissant des instructions alternatives si une exception est levée. Une connexion peut alors être retentée, des valeurs par défaut sont fournies…
Une exception est donc un événement qui interrompt la séquence normale de l’application et qui permet d’exécuter un code alternatif.
Ceci se fait à l’aide de blocs try-catch-finally, comme dans l’exemple ci-dessous :
try {
//...
Boxing/Unboxing
À l’intérieur de Java, tout est objet… ou presque : il existe ce que l’on appelle les types primitifs, tels que int, double, float, char, boolean.
Chacun de ces types primitifs a un équivalent objet : Integer, Double, Float, Character, Boolean.
Afin de faciliter le codage, le langage Java peut faire une conversion automatique du type primitif en son équivalent objet : il s’agit du mécanisme de boxing.
private Integer transformer(int entier) {
return entier; // convertit le type primitif int
// en objet de type Integer
}
Pour le faire à la main, utilisez la méthode statique valueOf() des classes Boolean, Integer, Double, Float ou Character.
private Boolean transformer(boolean booleen) {
// convertit le type primitif boolean
// en objet de type Boolean
return Boolean.valueOf(booleen);
}
L’opération inverse, dite d’unboxing, permet quant à elle de transformer les objets Java en leur type primitif.
private double convertir(Double decimal) {
return decimal; // convertit l'objet de type Double
// en valeur...
Enums
Un enum est un type spécial de données qui fournit un jeu de constantes prédéfinies.
Il est tout à fait possible de créer un enum personnalisé : il se définit comme une classe ou une interface dans son propre fichier, avec le mot-clé enum.
Par exemple :
public enum Direction {
NORD, SUD, EST, OUEST
}
On peut également lui donner des attributs, des méthodes et un constructeur. La contrainte principale est alors que le constructeur doit être privé ou package privé : il est impossible de créer une valeur d’enum directement, ces valeurs ne peuvent être que celles déclarées dans l’enum. Cette contrainte interdit aussi tout héritage de l’enum.
public enum Direction {
NORD("septentrion"),
SUD("midi"),
EST("levant"),
OUEST("ponant"); // le point-virgule est maintenant nécessaire
// car des membres existent dans l'enum
private String ancienNom;
private Direction(String nom) {
this.ancienNom = nom;
}
// public Direction(String...
Gestion du temps et des dates
La gestion des dates a été historiquement un point faible de Java.
Avant Java 8, créer une date, c’est-à-dire un instant précis sur une ligne temporelle, se faisait avec l’aide de la classe java.util.Date. Par exemple :
Date maintenant = new Date();
Le problème était avant tout conceptuel : un objet Date est véritablement un instant précis à la milliseconde, on pourrait dire un temps-machine, calculé par le nombre de millisecondes écoulées depuis l’epoch Java : le 1er janvier 1970 à minuit UTC (Coordinated Universal Time). Or, si l’on utilise la méthode toString() de l’objet Date :
String description = new Date().toString();
System.out.println(description); // Mon Jun 01 14:09:38 CEST 2017
on obtient une description de la date faite pour être lue par des humains, notamment avec la présence d’un fuseau horaire (CEST signifie Central European Summer Time, soit l’heure d’été sur le fuseau horaire de Paris).
Ces deux notions d’un temps-machine et d’un temps-humain sont conceptuellement différentes, car un temps-humain utilise toujours le concept de fuseau horaire et d’heure d’été/heure d’hiver alors qu’un temps-machine est essentiellement un horodatage (un timestamp en anglais). De la même manière, un enfant né à Paris le 1er juillet 2017 à 15h55 et un enfant né le même jour à la même heure et la même minute à Katmandou n’ont pas tout à fait le même âge : ils ont quelques heures d’écart (3 heures et 45 minutes pour être précis).
La classe Date était tellement erronée que quasiment toutes ses méthodes ont été marquées dépréciées dès la sortie suivante de Java, avec l’introduction d’une classe Calendar, qui souffrait également de problèmes conceptuels.
Pour obtenir l’année à partir d’un objet Date, il faut lui ajouter 1900, et le mois numéro 0 est le mois de janvier !
Java, dans sa version 8, a introduit une refonte complète sur la gestion du temps, et propose les classes du package java.time. Ses classes...
Événements
Toute application comportant des interfaces graphiques doit être en mesure de gérer des événements : il s’agit d’un concept essentiel des interfaces homme-machine (ou IHM).
Un événement est un signal qui est envoyé par les couches basses du système d’exploitation ou de Java pour notifier le programme que quelque chose de notable vient de survenir : l’utilisateur a cliqué sur un bouton, il tape sur le clavier, il survole avec la souris une zone particulière de l’interface graphique…
Du point de vue du développeur, les composants graphiques vont générer des événements de plusieurs natures, et il va falloir programmer des séquences en réaction à l’apparition de ces événements.
Il existe véritablement beaucoup de types d’événements. Voici par exemple une partie de ceux qui sont disponibles pour un simple composant JButton (un bouton cliquable).
Ne serait-ce que pour la souris, il existe cinq événements possibles de base, plus deux pour les déplacements de la souris, et un pour la molette.
Afin de programmer des actions en réponse à ces événements, il faut ajouter ce que l’on appelle un écouteur d’événements, un listener dans la terminologie Java.
Pour rappel...
Lambdas
Les expressions lambdas sont un des autres grands ajouts de Java depuis la version 8.
Dans une application et surtout dans sa partie graphique, il est très habituel de programmer des petites classes dites internes anonymes (car elles sont tellement petites qu’elles ne prennent pas de nom, et elles sont codées à l’intérieur d’une classe).
En reprenant l’exemple de l’écouteur d’événements :
JButton btnValider = new Jbutton("Valider");
btnValider.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("Action!");
}
});
Vous aviez en fait créé une classe anonyme héritant de l’interface ActionListener et codé l’action résultant du clic du bouton à l’intérieur de la méthode actionPerformed.
Cette interface ActionListener est très simple : elle n’a qu’une seule méthode à implémenter. Elle est ce que l’on appelle une interface fonctionnelle.
L’écriture peut alors être simplifiée grâce aux lambdas comme ceci :
btnValider.addActionListener(
(ActionEvent e) -> { ...
Streams
Il est extrêmement habituel de travailler sur des ensembles de données quand on code un logiciel. Un exemple caractéristique est de recevoir une liste d’objets, de traverser cette liste et d’effectuer des traitements pour chacun des objets de cette liste.
Cela se traduit habituellement par une boucle for (ou une boucle while) :
List<String> valeurs = Arrays.asList("a", "b", "c", "b1", "c1");
for (String valeur: valeurs) {
traitement(valeur);
}
Iterator<Integer> iterator = valeurs.iterator();
while (iterator.hasNext()) {
String valeur = iterator.next();
traitement(valeur);
}
traitement(valeur) est l’appel de la méthode où le traitement est effectué.
Depuis la version 8, Java permet de simplifier cette écriture par l’utilisation de streams.
Un stream en Java est une monade. Sans rentrer dans la définition mathématique formelle, il s’agit d’une séquence d’éléments depuis une source, qui permet d’effectuer des opérations de manière chaînée.
Plus concrètement, écrire le code suivant :
valeurs.stream()
.filter(val -> val.startsWith("c"))
.map(String::toUpperCase)
.distinct() ...
Optional
La classe Optional est présente depuis Java 8. Il s’agit d’un conteneur qui contient ou non une valeur de n’importe quel type.
Imaginons le cas suivant. Un ordinateur a une carte son. Cette carte est présente dans la plupart des cas, mais pas forcément tout le temps.
Une modélisation possible de cette caractéristique est de créer un membre de classe, qui vaut null quand il n’y a pas de carte et a une valeur quand la carte son est présente.
Cela impose de vérifier constamment dans le code la valeur du membre, car si le code effectue une opération comme :
laCarte.getVolume().augmenter();
dans le cas d’une carte absente, il y aura une erreur NullPointerException lors de l’exécution.
Une manière de protéger le code est de le modifier ainsi :
if (laCarte!= null) {
Volume volume = laCarte.getVolume();
if (volume!= null) {
volumer.augmenter();
}
}
C’est possible, mais fastidieux...
La classe Optional permet de résoudre plus élégamment ce souci :
Optional.ofNullable(laCarte)
.map(carte -> carte.getVolume())
.ifPresent(volume -> volume.augmenter())
Ce code crée un Optional à partir d’une carte potentiellement absente, transforme cette carte en Volume...
Classes graphiques
Java fournit plusieurs composants graphiques de base, grâce aux packages java.awt et javax.swing. Les composants AWT sont historiquement les premiers à être apparus et ont été enrichis plus tard avec les composants Swing.
D’une manière générale, préférez les composants Swing. Ils ont plus de possibilités d’interaction et d’enrichissement graphique.
Ces composants sont mis en évidence dans leur version par défaut : l’habillage graphique n’est peut-être pas le plus plaisant, mais Java propose un mécanisme de changement de rendu graphique par le système Look and Feel, qui sera expliqué un peu plus loin.
Les sections suivantes font un petit tour guidé des composants les plus usuels : ceux que l’on retrouve dans pratiquement chaque application.
Sachez tout d’abord que tous les composants graphiques présentés ici ont un statut activé ou non (en anglais : enabled). Il n’est pas possible d’interagir avec un composant désactivé.
1. Boutons
Les boutons permettent à l’utilisateur d’effectuer des actions ou de faire des choix.
Chaque type de bouton a un état : sélectionné ou non (en anglais : selected).
a. JButton
La classe JButton permet d’afficher un bouton très simple avec un texte et éventuellement une icône.
Il s’agit d’un des types les plus simples de composant graphique d’interaction avec l’utilisateur : il clique sur le bouton (c’est-à-dire qu’il le sélectionne), une action est effectuée, le bouton revient à son état initial (non sélectionné).
b. JCheckBox
La classe JCheckBox permet à l’utilisateur d’effectuer un choix binaire, c’est-à-dire un choix entre deux options mutuellement exclusives : oui ou non, vrai ou faux, chaud ou froid…
La case à cocher reste dans l’état dans lequel l’utilisateur l’a sélectionnée. Elle a un effet mémoire, comme les boutons radio.
c. JRadioButton
La classe JRadioButton permet à l’utilisateur de choisir parmi des options multiples (donc potentiellement plus de deux) mutuellement exclusives : choisir...
Threads
Un programme est codé pour que des instructions soient exécutées séquentiellement.
Créez le code suivant :
public class Sequentiel {
public static void main(String[] args) {
System.out.println("1ere ligne");
System.out.println("2eme ligne");
System.out.println("3eme ligne");
}
}
Dans ce code, la première ligne sera toujours affichée avant la seconde. La troisième ligne sera toujours exécutée en dernier.
Ceci est garanti par Java car il exécute ces instructions dans une file d’exécution, un Thread.
Placez un point d’arrêt (un breakpoint) en double cliquant sur la marge gauche de l’éditeur à la deuxième ligne de la méthode main(). Un point bleu doit apparaître. Sinon, placez le curseur sur cette ligne et faites la combinaison de touches [Ctrl][Shift] B.
Lancez le programme en mode débogage en appuyant sur la touche [F11]. La perspective de débogage s’affiche.
La file d’exécution principale de ce programme est visualisée dans la vue Debug. Cette file, ou thread, a un nom : main.
À l’intérieur de cette file, les instructions sont exécutées séquentiellement, les unes après les autres.
Néanmoins, ce n’est généralement pas ce qu’un utilisateur attend d’une application : il veut souvent pouvoir télécharger des fichiers en même temps qu’il imprime un document tout en écoutant sa chanson préférée…
Grâce à Java, il est possible de créer de telles applications : chaque partie de l’application aura un thread dédié, hors de la file d’exécution principale, qui exécutera ses propres instructions séquentiellement. Cela donnera au final à l’utilisateur la possibilité d’effectuer des tâches en parallèle.
Il existe plusieurs manières de lancer des files parallèles. Elles impliquent pour la plupart d’encapsuler le code à exécuter à l’intérieur...
Annotations
Les annotations sont des métadonnées qui peuvent être ajoutées directement dans le code d’une application Java, à quasiment tous les niveaux : sur les packages, les classes, les méthodes, les attributs, les paramètres et les variables locales.
Les annotations sont toujours déclarées en commençant par le caractère @.
Des exemples d’annotations ont déjà été vus, notamment @Override sur certaines méthodes.
@Override est une annotation d’aide au compilateur pour indiquer que la méthode annotée surcharge une méthode de la super-classe ou d’une interface. Si le développeur fait une erreur de typographie dans le nom de la classe, le compilateur signalera une erreur.
Pour annoter un code, il faut écrire l’annotation immédiatement avant la partie du code concerné.
Par exemple, si vous voulez noter une méthode comme @Deprecated (elle apparaîtra alors barrée lors de toute utilisation), faites comme ceci :
@Deprecated
public void methodePasTerrible() {
...
}
Les annotations sont utiles lors de deux phases distinctes du logiciel : lors de sa compilation et lors de son exécution.
Lors de sa compilation, le compilateur peut émettre des avertissements ou directement arrêter la compilation....
Autres notions
Le tour des classes disponibles dans Java n’est pas terminé, loin de là !
Il reste à explorer les possibilités et subtilités des packages :
-
java.net
-
java.io
-
java.nio
-
java.util.concurrent
-
java.util.logging
-
java.util.regex
-
java.util.zip
-
javax.accessibility
-
javax.print
-
javax.imageio
pour ne citer que ceux-là !
Java propose aux développeurs un environnement riche et très puissant permettant de faire énormément de choses. De plus, beaucoup de librairies (libres ou commerciales) sont disponibles pour compléter et enrichir encore plus les possibilités de programmation.
Le projet qui sert de fil rouge tout au long de cet ouvrage ne fait pas appel aux classes de ces packages, ces notions ne seront donc pas approfondies.