Package et Imports

Principes de base

Java est livré avec des milliers de classes intégrées, et il en existe d’innombrables autres créées par des développeurs comme vous. Avec toutes ces classes, Java a besoin d’un moyen de les organiser. Il gère cela d’une manière similaire à une armoire de classement. Vous mettez tous vos documents dans des dossiers. Java met les classes dans des packages. Ce sont des groupements logiques pour les classes.

Nous ne vous mettrions pas devant une armoire de classement en vous disant de trouver un document spécifique. Au lieu de cela, nous vous dirions dans quel dossier chercher. Java fonctionne de la même manière. Il a besoin que vous lui disiez dans quels packages chercher pour trouver le code.

Supposons que vous essayiez de compiler ce code :

public class SelecteurNombre {
    public static void main(String[] args) {
        Random a = new Random(); // NE COMPILE PAS
        System.out.println(a.nextInt(10));
    }
}

Le compilateur Java vous donne utilement une erreur qui ressemble à ceci : error: cannot find symbol

Cette erreur pourrait signifier que vous avez fait une faute de frappe dans le nom de la classe. Vous vérifiez à nouveau et découvrez que ce n’est pas le cas. L’autre cause de cette erreur est l’omission d’une déclaration d’import nécessaire. Une déclaration est une instruction, et les déclarations d’import indiquent à Java dans quels packages chercher les classes. Puisque vous n’avez pas dit à Java où chercher Random, il n’en a aucune idée.

En réessayant avec l’import, le code compile :

import java.util.Random; // l'import nous dit où trouver Random
public class SelecteurNombre {
    public static void main(String[] args) {
        Random a = new Random();
        System.out.println(a.nextInt(10)); // un nombre entre 0-9
    }
}

Maintenant le code s’exécute ; il imprime un nombre aléatoire entre 0 et 9. Comme pour les tableaux, Java aime commencer à compter à partir de 0.

Note Perso

N’essayez pas d’apprendre les packages/imports par cœur, les IDE modernes vous assistent suffisamment pour insérer les imports manquants automatiquement.

Packages

Analogie avec les adresses

Comme vous l’avez vu dans l’exemple précédent, les classes Java sont regroupées en packages. La déclaration import indique au compilateur dans quel package chercher une classe. C’est similaire à la façon dont fonctionne l’envoi d’une lettre. Imaginez que vous envoyez une lettre au 123 rue Jean Jaurès, Appartement 9. Le facteur apporte d’abord la lettre au 123 rue Jean Jaurès. Ensuite, le facteur cherche la boîte aux lettres de l’appartement numéro 9. L’adresse est comme le nom du package en Java. Le numéro d’appartement est comme le nom de la classe en Java. Tout comme le facteur ne regarde que les numéros d’appartement dans l’immeuble, Java ne cherche que les noms de classe dans le package.

Les noms de packages sont hiérarchiques comme le courrier. Le service postal commence par le niveau supérieur, en regardant d’abord votre pays. Vous commencez aussi à lire un nom de package au début. Par exemple, s’il commence par java, cela signifie qu’il est venu avec le JDK. S’il commence par autre chose, il montre probablement d’où il vient en utilisant le nom du site Web à l’envers. Par exemple, com.apple.javabook nous indique que le code est associé au site Web ou à l’organisation apple.com. Après le nom du site Web, vous pouvez ajouter ce que vous voulez. Par exemple, com.apple.java.mon.nom vient aussi de apple.com. Java appelle les packages plus détaillés des packages enfants. Le package com.apple.javabook est un package enfant de com.apple. Vous pouvez le dire parce qu’il est plus long et donc plus spécifique.

Syntax des packages

La règle pour les noms de packages est qu’ils sont principalement composés de lettres ou de chiffres séparés par des points (.). Techniquement, vous êtes autorisé à utiliser quelques autres caractères entre les points (.). Vous pouvez même utiliser des noms de packages de sites Web que vous ne possédez pas si vous le souhaitez, comme com.apple, bien que les personnes lisant votre code puissent être confuses ! Les règles sont les mêmes que pour les noms de variables, que vous verrez plus tard dans ce chapitre.

Dans les sections suivantes, nous examinons les imports avec des caractères génériques, les conflits de noms avec les imports, comment créer votre propre package, et comment compiler et exécuter le code.

Caractères Génériques (Wildcards, ou jokers)

Les classes dans le même package sont souvent importées ensemble. Vous pouvez utiliser un raccourci pour importer toutes les classes d’un package.

import java.util.*; // importe java.util.Random parmi d'autres
public class SelecteurNombre {
    public static void main(String[] args) {
        Random a = new Random();
        System.out.println(a.nextInt(10));
    }
}

Dans cet exemple, nous avons importé java.util.Random et un tas d’autres classes. Le * est un caractère générique qui correspond à toutes les classes dans le package. Chaque classe dans le package java.util est disponible pour ce programme lorsque Java le compile. La déclaration import n’importe pas les packages enfants, les champs ou les méthodes ; elle importe uniquement les classes directement sous le package.

Disons que vous vouliez utiliser la classe EntierAtomique (AtomicInteger) dans le package java.util.concurrent.atomic. Quel(s) import(s) permettent cela ?

import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

Seul le dernier import permet à la classe d’être reconnue car les packages enfants ne sont pas inclus avec les deux premiers.

Vous pourriez penser qu’inclure autant de classes ralentit l’exécution de votre programme, mais ce n’est pas le cas. Le compilateur détermine ce qui est réellement nécessaire. L’approche que vous choisissez est une préférence personnelle – ou une préférence d’équipe, si vous travaillez avec d’autres dans une équipe. Lister les classes utilisées rend le code plus facile à lire, en particulier pour les nouveaux programmeurs. Utiliser le caractère générique peut raccourcir la liste des imports.

Imports Redondants

Attendez une minute ! Nous avons fait référence à System sans import chaque fois que nous avons imprimé du texte, et Java l’a trouvé très bien. Il y a un package spécial dans le monde Java appelé java.lang. Ce package est spécial car il est automatiquement importé. Vous pouvez taper ce package dans une déclaration d’import, mais vous n’êtes pas obligé. Dans le code suivant, combien d’imports pensez-vous sont redondants ?

import java.lang.System;
import java.lang.*;
import java.util.Random;
import java.util.*;
public class SelecteurNombre {
    public static void main(String[] args) {
        Random a = new Random();
        System.out.println(a.nextInt(10));
    }
}

La réponse est que trois des imports sont redondants. Les lignes 1 et 2 sont redondantes car tout dans java.lang est automatiquement importé. La ligne 4 est également redondante dans cet exemple car Random est déjà importé depuis java.util.Random. Si la ligne 3 n’était pas présente, java.util.* ne serait pas redondant car il couvrirait l’importation de Random.

Un autre cas de redondance implique l’importation d’une classe qui se trouve dans le même package que la classe qui l’importe. Java recherche automatiquement dans le package actuel d’autres classes.

Examinons un exemple de plus pour vous assurer que vous comprenez les cas limites pour les imports. Pour cet exemple, Files et Paths sont tous deux dans le package java.nio.file. Quelles déclarations d’import pensez-vous fonctionneraient pour que ce code compile ?

public class ImportsEntree {
    public void read(Files files) {
        Paths.get("nom");
    }
}

Il y a deux réponses possibles. La plus courte est d’utiliser un caractère générique pour importer les deux en même temps.

import java.nio.file.*;

L’autre réponse est d’importer explicitement les deux classes.

import java.nio.file.Files;
import java.nio.file.Paths;

Maintenant, considérons quelques imports qui ne fonctionnent pas.

import java.nio.*; // PAS BON - un caractère générique ne correspond
                  // qu'aux noms de classes, pas à "file.Files"
import java.nio.*.*; // PAS BON - vous ne pouvez avoir qu'un seul caractère
                    // générique et il doit être à la fin
import java.nio.file.Paths.*; // PAS BON - vous ne pouvez pas importer des méthodes
                             // uniquement des noms de classes

Conflits de Noms

L’une des raisons d’utiliser des packages est que les noms de classes n’ont pas à être uniques dans tout Java. Cela signifie que vous voudrez parfois importer une classe qui peut être trouvée à plusieurs endroits. Un exemple courant est la classe Date. Java fournit des implémentations de java.util.Date et java.sql.Date. Quelle déclaration d’import pouvons-nous utiliser si nous voulons la version java.util.Date ?

public class Conflits {
    Date date;
    // un peu plus de code
}

La réponse devrait être facile maintenant. Vous pouvez écrire soit import java.util.*; soit import java.util.Date;. Les cas délicats surviennent lorsque d’autres imports sont présents.

import java.util.*;
import java.sql.*; // fait que la déclaration Date ne compile pas

Lorsque le nom de la classe est trouvé dans plusieurs packages, Java vous donne une erreur de compilation. Dans notre exemple, la solution est facile – supprimer l’import java.sql.* dont nous n’avons pas besoin. Mais que faisons-nous si nous avons besoin d’un tas d’autres classes dans le package java.sql ?

import java.util.Date;
import java.sql.*;

Ah, maintenant ça marche ! Si vous importez explicitement un nom de classe, il prend la précédence sur tous les caractères génériques présents. Java pense : “Le programmeur veut vraiment que j’utilise la classe java.util.Date.”

Un dernier exemple. Que fait Java avec les “égalités” de précédence ?

import java.util.Date;
import java.sql.Date;

Java est assez intelligent pour détecter que ce code n’est pas bon. En tant que programmeur, vous avez affirmé vouloir explicitement que par défaut soient utilisées à la fois les implémentations java.util.Date et java.sql.Date. Comme il ne peut pas y avoir deux valeurs par défaut, le compilateur vous dit que les imports sont ambigus.

Si Vous Avez Vraiment Besoin d’Utiliser Deux Classes avec le Même Nom

Parfois, vous voulez vraiment utiliser Date de deux packages différents. Quand cela arrive, vous pouvez en choisir un à utiliser dans la déclaration d’import et utiliser le nom de classe entièrement qualifié de l’autre. Ou vous pouvez supprimer les deux déclarations d’import et toujours utiliser le nom de classe entièrement qualifié.

public class Conflits {
    java.util.Date date;
    java.sql.Date sqlDate;
}

Création d’un Nouveau Package

Jusqu’à présent, tout le code que nous avons écrit dans ce chapitre était dans le package par défaut. C’est un package spécial sans nom que vous ne devriez utiliser que pour du code jetable. Vous pouvez dire que le code est dans le package par défaut, car il n’y a pas de nom de package. En réalité, nommez toujours vos packages pour éviter les conflits de noms et permettre aux autres de réutiliser votre code.

Maintenant, il est temps de créer un nouveau package. La structure des dossiers sur votre ordinateur est liée au nom du package. Dans cette section, lisez simplement. Nous couvrons comment compiler et exécuter le code dans la section suivante.

Supposons que nous ayons ces deux classes :

package packagea;
public class ClasseA {}

package packageb;
import packagea.ClasseA;
public class ClasseB {
    public static void main(String[] args) {
        ClasseA a;
        System.out.println("Je l'ai");
    }
}

Imaginons que vous voulez creer votre programme dans le dossier C:\temp ou cd /tmp

ÉtapeWindowsMac/Linux
Créer la classe AC:\temp\packagea\ClasseA.java/tmp/packagea/ClasseA.java
Créer la classe BC:\temp\packageb\ClasseB.java/tmp/packageb/ClasseB.java
Aller dans le dossiercd C:\tempcd /tmp

Quand vous exécutez un programme Java, Java sait où chercher ces noms de packages. Dans ce cas, l’exécution à partir de C:\temp ou cd /tmp fonctionne car packagea et packageb sont en dessous.

Compilation et Exécution de Code avec des Packages

Compiler avec des Caractères Génériques

Vous pouvez utiliser un astérisque pour spécifier que vous souhaitez inclure tous les fichiers Java dans un dossier. C’est pratique quand vous avez beaucoup de fichiers dans un package. Nous pouvons réécrire la commande javac précédente comme ceci :

javac packagea/*.java packageb/*.java

Cependant, vous ne pouvez pas utiliser un caractère générique pour inclure les sous-dossiers. Si vous écriviez javac *.java, le code dans les packages ne serait pas pris en compte.

Compilation vers un Autre Dossier

Par défaut, la commande javac place les classes compilées dans le même dossier que le code source. Elle fournit également une option pour placer les fichiers de classe dans un dossier différent. L’option -d spécifie ce dossier cible.

Les options Java sont sensibles à la casse. Cela signifie que vous ne pouvez pas passer -D au lieu de -d.

Si vous suivez, supprimez les fichiers ClasseA.class et ClasseB.class qui ont été créés dans la section précédente. Où pensez-vous que cette commande créera le fichier ClasseA.class ?

javac -d classes packagea/ClasseA.java packageb/ClasseB.java

La bonne réponse est dans classes/packagea/ClasseA.class. La structure du package est préservée sous le dossier cible demandé.

Pour exécuter le programme, vous spécifiez le classpath pour que Java sache où trouver les classes. Il y a trois options que vous pouvez utiliser. Toutes les trois font la même chose :

java -cp classes packageb.ClasseB
java -classpath classes packageb.ClasseB
java --class-path classes packageb.ClasseB

Notez que le dernier nécessite deux tirets (–), tandis que les deux premiers nécessitent un tiret (-). Si vous avez le mauvais nombre de tirets, le programme ne s’exécutera pas.

Trois Options de Classpath

Vous vous demandez peut-être pourquoi il y a trois options pour le classpath. L’option -cp est la forme courte. Les développeurs choisissent souvent la forme courte car nous sommes des dactylographes paresseux. Les versions -classpath et –class-path peuvent être plus claires à lire mais nécessitent plus de frappe.

Voici les options importantes à connaître :

Options javac importantes :

OptionDescription
-cp <classpath>
-classpath <classpath>
–class-path <classpath>
Emplacement des classes nécessaires pour compiler le programme
-d <dossier>Dossier dans lequel placer les fichiers de classe générés
Options importantes de la commande javac

Options java importantes :

OptionDescription
-cp <classpath>
-classpath <classpath>
–class-path <classpath>
Emplacement des classes nécessaires pour exécuter le programme
Options importantes de la commande java

Compilation avec des Fichiers JAR

Tout comme le dossier classes dans l’exemple précédent, vous pouvez également spécifier l’emplacement des autres fichiers explicitement en utilisant un classpath. Cette technique est utile lorsque les fichiers de classe sont situés ailleurs ou dans des fichiers JAR spéciaux. Un fichier Java archive (JAR) est comme un fichier ZIP contenant principalement des fichiers de classe Java.

Sous Windows, vous tapez ceci :

java -cp ".;C:\temp\autreEmplacement;c:\temp\monJar.jar" monPackage.MaClasse

Et sous macOS/Linux, vous tapez ceci :

java -cp ".:/tmp/autreEmplacement:/tmp/monJar.jar" monPackage.MaClasse

Le point (.) indique que vous voulez inclure le dossier courant dans le classpath. Le reste de la commande dit de chercher des fichiers de classe libres (ou packages) dans autreEmplacement et dans monJar.jar. Windows utilise des points-virgules (;) pour séparer les parties du classpath ; les autres systèmes d’exploitation utilisent des deux-points.

Tout comme lors de la compilation, vous pouvez utiliser un caractère générique (*) pour faire correspondre tous les JARs dans un dossier. Voici un exemple :

java -cp "C:\temp\dossierAvecJars\*" monPackage.MaClasse

Cette commande ajoutera au classpath tous les JARs qui sont dans dossierAvecJars. Elle n’inclura pas dans le classpath les JARs qui sont dans un sous-dossier de dossierAvecJars.

Création d’un Fichier JAR

Certains JARs sont créés par d’autres, comme ceux téléchargés depuis Internet ou créés par un coéquipier. Alternativement, vous pouvez créer un fichier JAR vous-même. Pour ce faire, vous utilisez la commande jar. Les commandes les plus simples créent un jar contenant les fichiers dans le dossier courant. Vous pouvez utiliser la forme courte ou longue pour chaque option.

jar -cvf monNouveauFichier.jar .
jar --create --verbose --file monNouveauFichier.jar .

Alternativement, vous pouvez spécifier un dossier au lieu d’utiliser le dossier courant.

jar -cvf monNouveauFichier.jar -C dossier .

Il n’y a pas de forme longue de l’option -C. Voici les options importantes pour utiliser la commande jar pour créer un fichier JAR :

OptionDescription
-c
–create
Crée un nouveau fichier JAR
-v
–verbose
Affiche les détails lors des opérations sur les fichiers JAR
-f <nomDuFichier>
–file <nomDuFichier>
Nom du fichier JAR
-C dossierDossier contenant les fichiers à inclure dans le JAR
options importantes pour utiliser la commande jar pour créer un fichier JAR

Ordonner les Éléments dans une Classe

Maintenant que vous avez vu les parties les plus courantes d’une classe, examinons l’ordre correct pour les taper dans un fichier. Les commentaires peuvent aller n’importe où dans le code. Au-delà de cela, vous devez mémoriser les règles suivantes :

ÉlémentExempleObligatoire ?Où va-t-il ?
Déclaration de packagepackage abc;NonPremière ligne du fichier (excluant les commentaires ou les lignes vides)
Déclarations d’importimport java.util.*;NonImmédiatement après le package (si présent)
Déclaration de type de premier niveaupublic class COuiImmédiatement après l’import (s’il y en a)
Déclarations de champsint valeur;NonN’importe quel élément de premier niveau dans une classe
Déclarations de méthodesvoid methode()NonN’importe quel élément de premier niveau dans une classe
Ordre correct des éléments d’une classe Java

Examinons quelques exemples pour vous aider à vous en souvenir. Le premier exemple contient un de chaque élément :

package structure; // le package doit être en premier (hors commentaires)
import java.util.*; // l'import doit venir après le package
public class Suricate { // puis vient la classe
    double poids; // les champs et méthodes peuvent aller dans n'importe quel ordre
    public double getPoids() {
        return poids;
    }
    double taille; // un autre champ - ils n'ont pas besoin d'être ensemble
}

Jusqu’ici, tout va bien. C’est un schéma courant avec lequel vous devriez être familier. Que dites-vous de celui-ci ?

/* en-tête */
package structure;
// classe Suricate
public class Suricate { }

Toujours bon. Nous pouvons mettre des commentaires n’importe où, les lignes vides sont ignorées, et les imports sont optionnels. Dans l’exemple suivant, nous avons un problème :

import java.util.*;
package structure; // NE COMPILE PAS
String nom; // NE COMPILE PAS
public class Suricate { } // NE COMPILE PAS

Il y a deux problèmes ici. L’un est que les déclarations de package et d’import sont inversées. Bien que les deux soient optionnels, package doit venir avant import s’il est présent. L’autre problème est qu’un champ tente une déclaration en dehors d’une classe. Ce n’est pas autorisé. Les champs et les méthodes doivent être à l’intérieur d’une classe.

Vous avez tout compris ? Pensez à l’acronyme PIC (picture) : package, import et classe. Les champs et les méthodes sont plus faciles à retenir car ils doivent simplement être à l’intérieur d’une classe.