Cours Java1
Cours Java1
Cours Java1
1 Introduction à Java 5
1.1 Comparaison entre Java et C++ . . . . . . . . . . . . . . . . . . . . . . . . 5
1.2 Les points forts de Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.3 Exécution d’un programme Java . . . . . . . . . . . . . . . . . . . . . . . . 7
1.4 Qu’est-ce que le Java Development Kit ? . . . . . . . . . . . . . . . . . . . 8
1.5 Le programme "Bonjour le monde !" . . . . . . . . . . . . . . . . . . . . . 8
2 Eléments de base 10
2.1 Syntaxe Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.1.1 Les identificateurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.1.2 La constante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.1.2.1 La constante entière . . . . . . . . . . . . . . . . . . . . . 11
2.1.2.2 La constante virgule flottante . . . . . . . . . . . . . . . . 12
2.1.2.3 La constante booléenne . . . . . . . . . . . . . . . . . . . 12
2.1.2.4 La constante caractère . . . . . . . . . . . . . . . . . . . . 12
2.1.2.5 La constante chaîne . . . . . . . . . . . . . . . . . . . . . 12
2.1.3 Les séquences d’échappement . . . . . . . . . . . . . . . . . . . . . 13
2.1.4 Les mots clés . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.1.5 Les commentaires . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.1.6 Les opérateurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.1.6.1 Opérateurs arithmétiques . . . . . . . . . . . . . . . . . . 15
2.1.6.2 Opérateurs logiques . . . . . . . . . . . . . . . . . . . . . 15
2.1.6.3 Opérateurs de comparaison . . . . . . . . . . . . . . . . . 16
2.1.6.4 Opérateurs d’affectation . . . . . . . . . . . . . . . . . . . 16
2.2 Types de données Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
3 Objets, classes 28
3.1 Notion d’objet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
3.1.1 Encapsulation, méthodes et invocation . . . . . . . . . . . . . . . . 30
3.1.2 Niveaux d’abstraction et Classes d’objets . . . . . . . . . . . . . . . 31
3.1.3 Le polymorphisme . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.1.4 Pourquoi utiliser l’approche objet ? . . . . . . . . . . . . . . . . . . 33
3.2 Les classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
3.2.1 Déclaration et instanciation des classes . . . . . . . . . . . . . . . . 34
3.2.2 Modificateur d’accès : public . . . . . . . . . . . . . . . . . . . . . . 35
3.2.3 Modificateur d’accès : final . . . . . . . . . . . . . . . . . . . . . . . 35
3.3 Données membre ou attributs . . . . . . . . . . . . . . . . . . . . . . . . . 35
4 L’héritage 45
4.1 Principes généraux, le mot clé extends . . . . . . . . . . . . . . . . . . . . 45
4.2 Le Polymorphisme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
4.3 Accès à la super-classe d’une classe : super (. . .) . . . . . . . . . . . . . . . 48
4.4 Méthodes et classes abstraites : abstract . . . . . . . . . . . . . . . . . . . 49
4.5 Object : la superclasse cosmique . . . . . . . . . . . . . . . . . . . . . . . . 50
5 Les interfaces 51
5.1 Définitions, les mots clés interface et implements . . . . . . . . . . . . . . 51
5.2 Interface et héritage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
5.3 Les références de type interface, l’opérateur instanceof . . . . . . . . . . . 53
Introduction à Java
Java est un langage de programmation orientée objet qui est axé sur la création d’objets
qui peuvent être répartis et manipulés par le programme.
Comme d’autres langages de programmation, Java prend en charge la lecture et l’écri-
ture des données dans différents dispositifs d’entrée et de sortie.
Java examine le programme au fil de l’exécution et libère automatiquement la mémoire
qui n’est plus nécessaire. Cela signifie qu’il n’est pas nécessaire de suivre les pointeurs de
mémoire ni de libérer manuellement la mémoire. Cette fonctionnalité évite tout risque de
la mauvaise des pointeurs par les programmeurs.
Les autres langages de programmation sont en général interprétés (Basic ou JavaS-
cript) ou compilés (Pascal ou C). Pour pouvoir être un langage multi-plateforme, Java
est un mélange de ces deux possibilités : les fichiers sources ".java" doivent être compilés
pour fournir un fichier ".class" qui sera interprété. Donc programmer en java suppose que
l’on dispose d’un compilateur et d’un interpréteur java. Ceux-ci sont fournis par le JDK
(Kit de Développement en Java) disponible gratuitement sur le site de la société SUN.
– La surcharge d’opérateurs
– L’héritage multiple
– La libération de mémoire est transparente pour l’utilisateur (il n’est plus nécessaire
de créer de destructeurs)
– Une meilleure gestion des erreurs
– Les chaînes et les tableaux sont des objets faisant partie intégrante du langage
Toutefois Java est beaucoup moins rapide que le langage C++, il perd en rapidité ce
qu’il gagne en portabilité.
/*
Premier programme java : afficher bonjour.
*/
public class bonjour
{
public static void main(String args[])
{
System.out.println("Bonjour le monde !") ;
}
}
Quelques explications
1. Les parties de code situées entre /* et */ sont des commentaires qui ne seront pas
compilés.
2. La première instruction "public class bonjour" déclare qu’on crée une classe publique
(accessible par tous) qui se nomme bonjour. Cette classe doit être enregistrée dans un
fichier de même nom et d’extension ".java". Attention, java différencie les majuscules
Eléments de base
2.1.2 La constante
Un littéral, ou constante, représente une valeur qui ne change jamais. Par exemple,
le nombre 35 est un littéral or l’identificateur "age" représente un nombre qui peut être
égal à 35. Dans Java, une constante peut être un nombre (entier ou virgule flottante), un
booléen, un caractère ou une chaîne.
Les constantes entières peuvent prendre trois formats : décimal (base 10), hexadécimal
(base 16) et octal (base 8).
– Les constantes décimales sont écrites comme des nombres ordinaires,
– Les constantes hexadécimales commencent toujours par 0X,
– Les constantes octales commencent par 0.
Par exemple, le nombre décimal 10 s’écrit 0xA ou 0XA en format hexadécimal et 012
en format octal. Une constante entière peut être stockée dans les types de données byte,
short, int ou long.
Pour stocker une constante entière dans le type de données long, qui peut recevoir des
valeurs de 64 bits, il faut ajouter le caractère l ou L à la fin du littéral. Par exemple, la
constante 9999L est stockée dans le type long. Les lignes de code suivantes utilisent des
littéraux entiers :
int x = 12345 ; //12345 est un littéral
int y = x * 4 ; //4 est un littéral
Dans la première ligne, la constante 12345 est stockée directement dans la variable x de
type int. Dans la deuxième ligne, la constante 4 sert à calculer une valeur qui est ensuite
stockée dans la variable y de type int.
Une constante virgule flottante est un nombre contenant un point décimal et/ou un
exposant. Une constante virgule flottante suit une notation standard ou scientifique. Par
exemple, 123.456 est la notation standard, alors que 1.23456e+2 est la notation scienti-
fique.
Les constantes virgules flottantes sont du type double sur 64 bits (type par défaut) ou du
type float sur 32 bits. Pour stocker une constante virgule flottante dans le type float, on
ajoute la lettre f ou F à la fin du nombre.
Une constante booléenne représente deux états possibles : vrai ou faux. Les constantes
booléennes sont stockées dans le type de données booléen. Java représente les états d’une
valeur booléenne par les mots clés true (vrai) et false (faux).
Les constantes caractères sont toujours mises entre guillemets simples ; par exemple,
’A’ et ’9’ sont des constantes caractère. Java utilise le type char pour stocker des caractères
uniques.
Une constante chaîne représente une suite de caractères. Dans Java, les chaînes sont
toujours entre guillemets doubles. Java utilise les classes String et StringBuffer pour repré-
senter les chaînes. Ainsi, de tous les types de constante dont on a parlé, seuls les constantes
chaîne sont stockées par défaut sous forme d’objets.
Opérateur Définition
! Complément logique unaire
(NOT)
&& Court-circuit AND
|| Court-circuit OR
Les programmeurs ont besoin de comparer des valeurs. A la différence des opérateurs
logiques, les opérateurs de comparaison n’évaluent qu’une seule expression.
Opérateur Définition Préséance Associativité
< (resp >) Inférieur à (resp Supérieur à ) 7 Gauche
<= (resp >=) Inférieur (resp Supérieur) ou égal 7 Gauche
à
== Egal à 8 Gauche
!= Différent de 8 Gauche
Comme tous les langages, Java permet d’attribuer des valeurs aux variables.
Opérateur Définition Préséance Associativité
= Affectation 15 Droite
+= Ajout et affectation 15 Droite
-= Soustraction et affectation 15 Droite
*= Multiplication et affectation 15 Droite
/= Division et affectation 15 Droite
&= AND avec affectation 15 Droite
|= OR avec affectation 15 Droite
∧= XOR avec affectation 15 Droite
Si le programmeur n’initialise pas une variable numérique, la machine virtuelle Java lui
donne automatiquement la valeur 0. La plupart des compilateurs Java détectent les va-
riables non initialisées.
Un type de données booléen a deux valeurs : true et false. Java utilise le type de
données intégré boolean. Les variables de type boolean non initialisées ont automa-
tiquement la valeur false. Le code suivant illustre l’utilisation d’une variable de type
boolean :
int a = 1, b = 0 ;
boolean bool = a < b ; //bool a la valeur false
Java utilise le type de données char pour stocker un caractère Unicode unique. Le
type char de Java a une largeur de 16 bits.
Un tableau est une structure de données qui peut contenir plusieurs éléments de même
type. Les éléments d’un tableau peuvent être de n’importe quel type : type primitif, type
composite ou classe définie par l’utilisateur. Voici quelques exemples de déclarations de
tableaux :
int idEtudiant[] ;
char[] grades ;
float coordonnées[][] ;
Ces déclarations amènent deux remarques :
1. La taille du tableau n’est pas précisée ; dans la plupart des langages, la taille d’un
tableau doit faire partie de sa déclaration.
2. Les crochets peuvent suivre l’identificateur, comme dans le premier exemple, ou
suivre le type de données, comme dans le deuxième exemple.
avec une taille de 2 sur 2. La première ligne du tableau est initialisée avec les valeurs 0.0
et 0.1 et la deuxième ligne avec les valeurs 0.2 et 0.3. De par sa conception, coordonnées
est un tableau contenant deux éléments tableau.
Accès aux éléments des tableaux Pour accéder aux éléments d’un tableau, il faut
indexer la variable tableau. Cette indexation implique que le nom de la variable tableau
soit suivi du numéro de l’élément (index) entre crochets. L’index des tableaux commence
toujours à 0. Dans le cas d’un tableau à plusieurs dimensions, il faut utiliser un index par
dimension. Voici quelques exemples :
La petite partie de code suivante illustre l’utilisation des tableaux. Elle crée un tableau
de 5 éléments de type int appelé tableauInt, puis utilise une boucle for pour mettre les
nombres entiers 0 à 4 dans les éléments du tableau :
int[] tableauInt = new int [5] ;
int index ;
for (index = 0 ; index < 5 ; index++)
tableauInt [index] = index ;
Les boucles for sont présentées dans une section ultérieure. Ce code utilise la boucle pour
incrémenter la variable index de 0 à 4 et, à chaque passage, la valeur de index est mise
dans l’élément de tableauInt indexé par index.
2.2.3.2 Chaînes
Une chaîne est une suite de caractères. Pour stocker des chaînes, Java utilise le type
String. Ce type de données fait partie du paquet java.lang. Cela signifie qu’il ne s’agit
pas d’un type intégré ; pour déclarer une variable du type String, il faut utiliser le paquet
java.lang.
Une fois initialisée, une variable de type String ne peut pas être modifiée. Comment
peut-elle être une variable et ne pas pouvoir être modifiée ? On rappelle qu’une variable
n’est qu’une référence à un emplacement en mémoire ; on l’utilise pour accéder à la mé-
moire sur laquelle elle pointe et modifier cette mémoire. Dans le cas d’une variable du
type String, la mémoire sur laquelle pointe la variable String ne peut pas être modifiée ;
toutefois, la variable elle-même peut pointer ailleurs, comme l’illustre le code suivant :
2.2.4 Transtypage
Dans certains cas, il est nécessaire de convertir un type de variable en un autre type.
Par exemple, on peut avoir besoin de transmettre une variable de type int à une méthode
qui n’accepte que des variables de type float. Convertir le type d’une variable s’appelle
transtypage. Pour transtyper une variable, on précède l’identificateur de la variable du
type voulu entre parenthèses. L’exemple suivant montre comment la variable renvoyée
par une méthode, de type int, peut être transtypée en type float :
Il faut être prudent lors des transtypages, car il y a des risques de perte d’informations.
Par exemple, lors d’un transtypage d’une variable de type long de 64 bits en une variable
de type int de 32 bits, le compilateur ignore les 32 bits de niveau supérieur de la variable
de type long. Si la valeur de la variable de type long dépasse 32 bits au moment du
transtypage, le transtypage attribue une valeur inexacte à la variable de type int.
En règle générale, le type cible du transtypage doit avoir une taille au moins égale au
type initial. Le tableau suivant montre les transtypages qui ne risquent pas de provoquer
des pertes d’informations :
Dans certains cas, un transtypage peut être réalisé implicitement par le compilateur.
Voici un exemple :
if (3 > ’a’) {}
Dans ce cas, la valeur ’a’ est convertie en un nombre entier (valeur ASCII de la lettre a)
avant d’être comparée au nombre 3.
class démoPortée
{
int x = 0 ;
void méthode1()
{
int y ;
y = x ; //correct
}
void méthode2()
{
int z = 1 ;
z = y ; //incorrect !
}
}
Ce code déclare une classe appelée démoPortée qui contient deux méthodes : méthode1 ()
et méthode2(). La classe elle-même est considérée comme étant le bloc de code principal
et les deux méthodes sont ses blocs imbriqués.
La variable x est déclarée dans le bloc principal et elle est donc visible (reconnue par le
compilateur) dans la méthode méthode1() et la méthode2(). D’autre part, les variables y
et z sont déclarées dans deux blocs imbriqués mais indépendants ; ainsi, essayer d’utiliser
y dans méthode2 () provoque une erreur, puisque y n’est visible que dans son bloc.
La boucle while est utilisée pour créer un bloc de code qui sera exécuté tant qu’une
condition particulière est satisfaite.
while (condition) {
code à exécuter dans une boucle
}
2.3.1.2 La boucle do
La boucle do ressemble à la boucle while, sauf qu’elle évalue la condition après les
instructions et non avant. Voici la syntaxe générale de la boucle do :
do {
code à exécuter dans une boucle
}while(condition) ;
La boucle for est la plus puissante des constructions de boucles. Voici la syntaxe
générale d’une boucle for :
L’initialisation d’une boucle for se compose de trois parties : une expression d’initialisa-
tion, une expression conditionnelle et une expression d’opération. La troisième expression
met généralement à jour la variable de la boucle initialisée par la première expression.
L’instruction break permet de sortir d’une structure de boucle avant que la condition
du test soit respectée. Quand la boucle rencontre une instruction break, elle se termine
immédiatement en ignorant le code restant. En voici un exemple :
int x = 0 ;
while (x < 10)
{
System.out.println("Bouclage") ;
x++ ;
if (x == 5)
break ;
else
//faire quelque chose d’autre
}
if (condition1)
{//blocCode 1}
else if (condition2)
{//blocCode 2}
else
{//blocCode 3}
L’instruction if-else est généralement constituée de plusieurs blocs. Quand l’instruction
if-else s’exécute, un seul des blocs est exécuté (celui dont la condition a la valeur true,
évidemment). Les blocs if-else et le bloc else sont facultatifs. L’instruction if-else n’est
pas limitée à trois blocs, elle peut contenir autant de bloc if-else que nécessaire.
D’une certaine façon, l’instruction switch est une version spécialisée de l’instruction
if-else. Voici la syntaxe générale de l’instruction switch :
switch (expression)
{
case valeur1 : blocCode1 ;
break ;
case valeur2 : blocCode2 ;
break ;
default : blocCode3 ;
}
Il faut éclaircir un certain nombre de points concernant l’instruction switch :
– Les blocs de code n’ont pas besoin d’être mis entre accolades.
– Le bloc de code default correspond au bloc else d’une instruction if-else.
– Les blocs de code sont exécutés selon la valeur d’une variable ou d’une expression,
pas d’une condition.
– La valeur de l’expression doit être de type nombre entier (ou d’un type qui peut
être transtypé en int sans risque, comme char).
– Les valeurs case doivent être des expressions constantes du même type que l’expres-
sion.
– Le mot clé break est facultatif. Il est nécessaire pour terminer l’exécution de l’ins-
truction switch une fois qu’un code de bloc s’exécute. Si, par exemple, break n’est
pas mis après blocCode1 et si blocCode1 est exécuté, alors blocCode2 s’exécute im-
médiatement après blocCode1 (un effet secondaire parfois utile mais, dans la plupart
des cas, indésirable).
– Si un bloc de code doit être exécuté quand l’expression a une valeur parmi plusieurs,
il faut énumérer les valeurs-chacune étant précédée d’un mot clé case et suivie par
un point-virgule.
Objets, classes
Java est un langage orienté Objet. L’approche Objet est, en programmation, une
approche fondamentalement différente de l’approche procédurale abordée avec un langage
tel que le C :
– Dans une approche procédurale, il y a séparation complète des notions de Données
et d’Actions (de code) : le développeur part des données (de quoi dispose-t’on, que
veux t’on obtenir), pour ensuite définir la série des actions qui permettent de trans-
former les données initiales en résultats souhaités. Ces actions sont décomposées de
manière hiérarchique (une action d’un certain niveau est la composition d’actions
du niveau inférieur). Ce qui permet d’obtenir un certain niveau d’organisation et de
réutilisation (les procédures ou fonctions).
– Un garagiste, connaît les différentes pièces qui composent un véhicule, ainsi que son
fonctionnement interne : pour lui un véhicule est lui-même un ensemble d’objets
interagissant entre eux.
– Le concepteur du véhicule, intervient à un niveau de décomposition encore plus bas :
Il peut par exemple se pencher sur la structure atomique de tel matériau utilisé dans
la construction de son véhicule pour des raisons de solidité de celui-ci.
On voit à travers cet exemple, que parler de l’objet véhicule représente un certain
niveau d’abstraction : on fait abstraction de la réalité pour s’intéresser à un niveau de
représentation de cet objet suffisant pour permettre de résoudre le problème : il peut être
intéressant de descendre dans le détail (la voiture, les pièces composant une voiture,. . .),
mais on ne le fera que si cela est nécessaire (si le problème change et par conséquent on
doit changer de niveau d’abstraction). Le policier et le conducteur de l’exemple précédent
n’ont pas du tout la même vision du même objet.
Un objet est défini par la manière dont on peut interagir avec lui et cela en fonction
du problème que l’on a à résoudre. Il représente un tout logique du point de vue du
problème qu’en s’intéresse : peu importe ses composants et la manière dont il fonctionne
exactement, peu importe s’il peut faire autre chose si cela est utile. Il correspond à un
certain niveau d’abstraction de la réalité et il est représenté essentiellement par les services
qu’il peut rendre.
est réellement constitué, ce qui est important c’est les services (les méthodes) qu’il peut
fournir.
L’encapsulation d’un objet par son interface permet de masquer son contenu et donc offre
la possibilité de modifier celui-ci sans aucun impact pour le reste du monde. Respecter ce
principe d’encapsulation, permet aussi de pouvoir utiliser immédiatement des objets de
même nature, même si leur fonctionnement interne est différent.
Cela constitue un des points centraux de l’approche objet : Pour respecter ce principe
d’encapsulation on ne doit interagir avec un objet que par l’invocation d’une des méthodes
de son interface. En faisant cela on dit à l’objet ce qu’il doit faire, jamais comment il doit
le faire.
Par exemples :
– Lorsque l’on freine avec sa voiture on se contente d’appuyer sur la pédale de frein
(on invoque la méthode de freinage), on ne s’occupe pas de savoir s’il s’agit d’un
frein à disque ou de la manière dont cette commande est transmise
– Un policier qui règle la circulation, invoque la méthode " arrêter " à un véhicule en
levant la main d’une certaine manière. Il ne s’occupe pas de la manière dont chaque
véhicule s’arrête.
pour le repas. En fait, on range alors implicitement cette table dans l’ensemble général des
tables de cuisine. Cet ensemble regroupe tous les objets ayant les mêmes fonctionnalités
que la table. En langage objet on parle de la Classe des tables de cuisines et on dit que la
table de cuisine particulière est une instance de cette classe (C’est à dire un représentant
de cette classe).
Une classe est définie en décrivant l’interface commune à tous les objets qui la com-
posent (qui sont des instances de celle-ci).
Si on cherche un support pour écrire une lettre, on cherchera de manière plus générale
une table permettant de fournir le service " support ". On pourra choisir la table de la
cuisine, du salon ou de la table à manger. La classe des tables de cuisine est donc comprise
dans celle plus générale des tables (qui inclut aussi la classe des tables de salon,. . .). La
classe des tables est un niveau d’abstraction supérieur à celle des tables de cuisine :
elle représente un ensemble d’objets moins spécialisés que la classe des tables de cuisine.
Une table de cuisine est une table qui possède des caractéristiques spécifiques. On dit
que la classe des tables de cuisine est une classe fille de la classe des tables et qu’elle
hérite des caractéristiques de cette classe, tout en la spécialisant par l’ajout de nouvelles
fonctionnalités. Si la classe des tables est déjà définie, et si on veut définir celle des tables
de cuisine, il suffira de dire que cette nouvelle classe hérite de la précédente, avec en plus,
telle ou telle nouvelle caractéristique.
Une classe fille est définie à partir de sa mère par héritage (réutilisation de ce qui est
déjà défini) et spécialisation (ajout de nouvelles fonctionnalités).
En fonction des besoins et du moment, on pourra considérer la table, soit comme une
table de cuisine pour ranger des couverts, soit comme une table de salon pour ranger des
trucs de décoration, soit comme une table quelconque pour écrire une lettre.
3.1.3 Le polymorphisme
Le polymorphisme (du grec " plusieurs formes "), signifie que la même méthode peut
être définie dans différentes classes et que chaque classe peut implémenter (réaliser) cette
méthode à sa manière. Il est ainsi parfaitement possible d’invoquer une méthode sur un
objet sans savoir à quelle classe celui-ci appartient exactement et donc sans savoir quelles
seront exactement les actions réalisées.
Par exemples :
– Lorsque l’on freine dans une voiture on sait que la fonction attendue est de stopper le
véhicule. En fonction du type réel de véhicule (de sa classe plus spécifique) les actions
réelles seront très différentes (ABS ou non, freins à tambour, électromagnétiques,. . .).
Le conducteur n’a pas besoin de connaître ce niveau de détail pour savoir s’arrêter.
– Un policier qui règle la circulation invoque toujours la même méthode pour arrêter
des objets de la classe des véhicules. Chaque véhicule, en fonction de sa classe
spécifique utilisera cependant une méthode particulière pour s’arrêter : un cycliste
ne s’arrêtera pas comme une voiture ou un camion. . .
De manière plus générale, l’approche objet permet de construire des programmes mieux
conçus, plus évolutifs et souvent plus sûrs.
class MaClasse { }
Évidemment, cette classe n’est pas encore utile, mais elle est correcte dans Java. Une classe
un peu plus utile contiendra quelques données membres et des méthodes. On commence
par étudier la syntaxe de l’instanciation d’une classe. Pour créer une occurrence de cette
classe, il faut utiliser l’opérateur new avec le nom de la classe. Il faut déclarer une variable
d’occurrence pour l’objet.
MaClasse monObjet ;
Mais cette instruction n’affecte ni mémoire ni autres ressources à l’objet. Elle crée une
référence appelée monObjet, mais n’instance pas l’objet. C’est le rôle de l’opérateur new.
On remarque que le nom de la classe est utilisé comme s’il s’agissait d’une méthode. Il
ne s’agit pas d’une coïncidence. Après l’exécution de cette ligne de code, il est possible
d’accéder aux variables et aux méthodes membres de la classe avec l’opérateur ".". Une
fois l’objet créé, on n’a pas à se préoccuper de sa destruction. Dans Java, les objets sont
automatiquement éliminés par le Garbage collector. Dès que la référence à un objet (c’est-
à-dire la variable) sort de la portée, la machine virtuelle libère automatiquement toutes
les ressources affectées par l’opérateur new.
d’une classe peut avoir n’importe quel type (primitif, composite et types d’objets). Pour
accéder à une donnée membre, il faut d’abord créer une occurrence de la classe puis
accéder aux données avec l’opérateur ".".
class MathTool {
final static double PI = 3.14 ;
static double getPI() { return PI ; }
static double diametre( double rayon ) { return 2*PI*rayon ;
}
static double power(double x) { return x * x ; }
}
class Test {
void methode1() {
double i = MathTool.power(6) ;
}
}
Cet exemple présente une utilisation classique des méthodes static, à savoir une classe
rassemblant un ensemble d’outils sur un certain sujet.
nom de surcharge des méthodes. Java décide la méthode à appeler en regardant la valeur
de retour et les paramètres.
class Test {
void print(int i) {. . .}
void print(float f) {. . .}
void print(int i, int j) {. . .}
}
...
Test t = new Test() ;
int i ;
t.print( i ) ; // => La première méthode est appelée
A l’appel, la machine virtuelle Java détermine quelle est la méthode dont la lise de pa-
ramètres est la plus proche des paramètres effectivement envoyés. Dans le cas d’un appel
de méthode avec des types ne correspondants pas exactement, des transtypages implicites
peuvent être effectuées.
short s ;
t.print( s ) ;// => La première méthode est appelée avec une conversion de s en int
Le Garbage Collector est un outil très puissant pour gérer les problèmes de mémoires en
Java. Il est en particulier impossible d’utiliser de la mémoire non libérée et donc d’écraser
la mémoire.
class Date {
int jour =1 ;
int mois =1 ;
int an =1990 ;
Date( ) { an = 2000 ; } /* peut aussi s’écrire : this.an = 2000 */
Date( int an ) {this.an = an ;} /* Le paramètre an cache l’attribut an */
Date( int jour, int mois, int an )
{
this.jour = jour ;
this.mois = mois ;
this(an) ; /* appel du deuxième constructeur */
}
}
Class Test {
Voiture v1 ; /* Initialisée à null par défaut */
void methode()
{
...
if ( v1 == null ) v1 = new Voiture("Volvo") ;
...
}
}
Si une méthode est invoquée sur une référence égale à null, cela déclenche une erreur du
type NullPointerException.
==
: permet de tester si deux références désignent le même
objet.
!=
: permet de tester si deux références ne désignent pas le
même objet.
instanceof
: permet de tester si l’objet référencé est une instance d’une
classe donnée ou d’une de ses sous-classes
L’héritage
En Java, il est possible de dériver une classe à partir d’une autre classe : la classe fille
ainsi créée (ou sous-classe, ou classe dérivée) hérite alors des caractéristiques de sa classe
mère (ou super-classe), tout en la spécialisant avec de nouvelles fonctionnalités.
class Felin {
boolean afaim = true ;
void parler() { }
void appeler() {
System.out.println("Le Félin est appelé") ;
if (afaim) parler() ;
}
}
final class Chat extends Felin {
String race ;
void parler() { System.out.println("miaou !") ; }
}
final class Tigre extends Felin {
void parler() { System.out.println("Groar !") ; }
void chasser() { . . .}
}
...
Tigre tigre = new Tigre() ;
tigre.chasser() ; // OK
tigre.appeler() ; // OK : méthode héritée de la classe Felin
Les classes Chat et Tigre héritent de la classe Felin, ce qui signifie que les instances
de Chat et Tigre seront aussi des Félins. En particulier, les deux classes filles héritent
de l’attribut afaim, et des méthodes parler() et appeler(). De plus, la class Felin n’a
explicitement aucune classe mère. Elle hérite donc implicitement de la classe Object.
Les classes Chat et Tigre ont été déclarées final : on ne pourra pas définir de nouvelle
classe héritant d’une de ces classes.
On remarque dans chaque classe fille que la méthode parler() est redéfinie et que de
nouveaux attributs et méthodes sont définis (race, chasser()). Les attributs d’une classe
dérivée comprennent les attributs hérités et les attributs propres (idem pour les méthodes).
Une variable qui référence un objet d’une certaine classe peut référencer un objet de
n’importe laquelle de ses sous-classes (un objet membre d’une sous-classe est aussi membre
de la super-classe) : c’est le cas ici pour la variable qui référence un objet de type Felin
qui peut donc contenir la référence sur un Tigre
Felin felin ;
Tigre tigre = new Tigre() ;
felin = tigre ;
felin.parler() ;//c’est la méthode Tigre.parler() qui est réellement appelée
felin.chasser() ; // Erreur à la compilation : La méthode n’existe pas dans la classe
Felin
tigre = felin ; // Erreur à la compilation : Conversion explicite nécessaire
tigre = (tigre)felin ; // OK
tigre.parler() ; // OK
tigre.chasser() ; // OK
Chat chat = new Chat() ;
felin = chat ;
lion = (lion)felin ; // Erreur détectée lors de l’exécution : ClassException
Le fait de déclencher la méthode parler() sur la référence de type Felin, déclenche en fait
la méthode de la classe Tigre. C’est un des grands avantages des langages orientés objets :
la méthode est exécutée par l’objet réel se trouvant au bout de la référence et lorsqu’on
demande à un Tigre de parler, il le fait comme on le lui a appris, même si celui qui fait
la demande ne sait pas exactement qu’il est un Felin. Cette capacité de Java s’appelle le
polymorphisme.
4.2 Le Polymorphisme
Une méthode polymorphe est une méthode déclarée dans une super-classe et redéfinie
par une sous-classe. Dans Java, toute méthode est par défaut polymorphe. Les méthodes
final ne peuvent pas être redéfinies et ne sont pas polymorphes (définir une méthode final
est utile pour optimiser le Byte code et pour des raisons de sécurité).
L’intérêt des méthodes polymorphes est que lorsqu’on les appelle, la version qui est
exécutée est toujours celle correspondant à l’objet réel se trouvant au bout de la référence
utilisée, même si la nature de cet objet réel n’est pas connue au moment de l’appel.
class Zoo {
int MAX ;
Felin[] liste ;
Int compteur = 0 ;
Zoo( int taille ) { MAX = taille ; liste = new Felin[MAX] }
void addFelin( Felin newfelin) {
if (compteur < MAX) liste[compteur++] = newfelin ;
else /* traitement d’erreur */
}
final void appeler() { // cette méthode ne peut être redéfinie dans une sous-classe
for ( int i=0 ; i<compteur ; i++ ) liste[i].parler() ; // appel polymorphe
}
}
...
Zoo zoo = new Zoo(10) ;
zoo.addFelin(new Tigre()) ;
zoo.addFelin(new Chat()) ;
zoo.appeler() ;. . .
Cet exemple montre une mise en oeuvre simple du polymorphisme. La méthode parler()
a été redéfinie dans toutes les sous-classes de la classe Felin. Lorsqu’on demande à chacun
des félins de l’application d’exécuter la méthode parler(), c’est à chaque fois une méthode
différente qui est déclenchée.
class Mere {
int attribut ;
Mere() { attribut = 1 ; }
Mere(int attribut) { this.attribut = attribut ;}
void print() { System.out.println("base" + attribut) ;}
}
class Fille extends Mere {
boolean flag ;
Fille( int a ) {
super(a) ;
flag = true ;
}
void print() {
super.print() ;
System.out.println("dérivée") ;
}
} ...
Fille f = new Fille(2) ;
Mere m = f ;
m.print() ;
// Affiche :
base 2
dérivée
déclarée abstraite, mais la réciproque n’est pas vraie : une classe peut être abstraite sans
pour autant posséder de méthode abstraite.
Les classes et les méthodes abstraites permettent de définir des fonctionnalités, sans
spécifier la façon dont ces fonctionnalités sont implémentées. Une classe héritant d’une
classe abstraite doit donner une implémentation à toutes les méthodes abstraites de sa
super-classe. Si ce n’est pas le cas, elle doit aussi être déclarée abstraite.
abstract class Felin {
boolean afaim = true ;
abstract void parler( ) ;
void appeler() {
System.out.println("Le Félin est appelé") ;
if (afaim) parler() ;
}
}
class Tigre extends Felin {
void parler() { System.out.println("Groar !") ; }
}
Les interfaces
Fonctionnellement, une interface ressemble à une classe abstraite mais avec une dif-
férence importante : une interface ne peut pas inclure de code. Dans Java, le mécanisme
d’interface est un moyen destiné à remplacer l’héritage multiple.
interface Printable
{void print() ;}
class Point extends Object implements Printable
{private double x,y ;
...
void print()
{System.out.println( "(" + x + "," + ")" ) ;}
}
interface Interval
{ int MIN = 0 ;
int MAX = 1000 ;
}
...
for (int i = Interval.MIN ; i < Interval.MAX ; i++ ) {. . .}
...
interface Printable
{ /* exprime le fait de pouvoir être imprimé */
void print() ;
}
interface InputStream
{ /* exprime le fait de pouvoir être une source de caractères */
public int read() ;
}
interface OutputStream
{ /* exprime le fait de pouvoir accepter des caractères */
public void write(int) ;}
qui est implémenté par un certain nombre de classes de l’application (la classe Client
dans l’exemple). On stocke une collection d’objet implémentant le service Persistent dans
un tableau de type Persistent. Chacun de ces objets dispose donc de la méthode save(),
avec sa propre implémentation. La classe PersistentManager peut alors manipuler tous
ces objets avec l’interface Persistent, sans rien savoir de leur nature réelle.
interface Persistent
{void save() ; }
class Client extends Personne implements Persistent
{ private int numero ;
...
public void save()
{ /* Enregistrement sur fichier ou Base de donnée */ }
}
class PersistentManager
{ private final static int MAX=100 ;
private static Persistent[] liste_des_objets = new Persistent[MAX] ;
static int nb = 0 ;
...
public static void addPersistent(Persistent objet)
{if (nb<MAX) liste_des_objets[nb++] = objet ;}
public static void saveAll ()
{
for (int i=0 ; i<liste_des_objets.length ; i++)
if ( liste_des_objets[i] != null ) liste_des_objets[i].save() ;
}
}
...
Client client = new Client("Toto",4529) ;
PersistentManager.addPersistent(client) ;
...
PersistentManager.saveAll() ;
L’opérateur instanceof peut être utilisé pour savoir si un objet implémente une interface
donnée :
– En utilisant le mot clé import pour importer (inclure) le package auquel appartient
la classe
On peut généralement utiliser l’une ou l’autre de ces deux méthodes, mais il existe un
cas ou l’on doit obligatoirement utiliser la première : si deux classes portant le même nom
sont définies dans deux packages différents.
Java importe automatiquement le package java.lang qui permet d’utiliser
des classes comme Thread ou System.
Remarque : System est une classe de java.lang, out est une variable statique de cette
classe (de type PrintStream) et println est une méthode de la classe PrintStream.
Seules les classes public sont accessibles d’un autre package, ou d’un autre fichier (les
autres classes ne sont pas connues en dehors de leur fichier). Il ne peut y avoir qu’une et
une seule classe public dans un fichier et cette classe doit porter le même nom que
le fichier (en respectant les minuscules / majuscules).
Les fichiers des classes qui font partie d’un package doivent être placés dans une hiérar-
chie de répertoires correspondant à la hiérarchie des packages. Ils doivent obligatoirement
commencer par une déclaration précisant le nom de leur package, précédé du mot clé
package.
Pour respecter les concepts de l’approche objet, il est nécessaire de respecter les règles
de conception suivantes :
1. Les attributs doivent toujours être privés
2. Les attributs constants (final) peuvent être publics ou privés
3. Les méthodes à usage strictement interne doivent être privées
4. Si un attribut doit pouvoir être accessible de l’extérieur : définir des méthodes
publiques permettant d’y accéder.
Ces règles permettent de garantir le respect de l’encapsulation des objets.
class Personne
{ private String nom ;
public Personne(String nom) { this.nom = nom ; }
public String getNom() { return nom ; }
}. . .
Personne moi = new Personne("Toto") ;
System.out.println( moi.getNom() ) ;
class Commande
{ public void go() { System.out.println("Pas de commande") ; }
}
class Shell
{ private Vector commandes = new Vector() ; // Objet permettant de contenir N objets
public void addCommande( Commande commande)
{commandes.addElement(commande) ;}
class Principale
{ Shell shell = new Shell() ;
public static void main(String[] args)
{
shell.addCommande( new Commande()
{
public void go() { System.out.println("commande") ;}
}
);
...
shell.executer() ;
}
}
Dans ce chapitre et dans les suivants, on passe en revue quelques classes indispensables
à connaître, sans trop entrer dans les détails : il est impossible et inutile de tout vouloir
apprendre sur les classes Java, il y en a trop et il sera toujours indispensable de savoir
utiliser le système d’aide et sa propre expérience pour trouver la réponse à ses questions.
Pour chaque classe on n’aborde que quelques exemples d’utilisation montrant les mé-
thodes les plus importantes à connaître.
class Personne
{ // dérive de Object par défaut
private String nom ;
public Personne(String nom) { this.nom = nom ; }
public String toString()
{return "Classe : " + getClass().getName() + " Objet : " + nom ;}
boolean equals(Personne p) { return p.nom.equals(nom) ; }
}
Personne p1 = new Personne("Foulen Ben Foulen") ;
– Les conversions entre types de base et chaînes de caractères sont possibles via des
objets Wrapper.
– Les objets Wrapper peuvent être contenus dans des conteneurs (au contraire des
types de base).
Attention : les objets Wrapper ne supportent pas les opérateurs classiques (+, *,. . .)
// Exemple d’accès aux valeurs min/max d’un type
double f ;
int i ;
...
if ( f > Integer.MIN_VALUE && f < Integer.MAX_VALUE) i = (int) f ;
...
Les objets de la classe Vector étant à taille variable, il n’y a pas de limite au nombre
d’objets qu’il peut contenir (la seule limite est la taille de la mémoire).
Exemple d’utilisation :
Enumeration est l’interface unique pour le parcours de tous les types de conteneurs
définis dans les classes de base de Java. Un objet implémentant cette interface permet de
réaliser une et une seule itération sur le conteneur qui lui est associé.
Pour les conteneurs de type Vector, la méthode elements() permet de récupérer un
objet implémentant l’interface Enumeration. Il est alors possible d’utiliser deux méthodes
sur cet objet :
– boolean hasMoreElements() : Teste s’il reste des éléments à parcourir
– Object nextElement() : Retourne l’élément courant et passe au suivant
// Soit le conteneur :
Vector vec = new Vector() ;
for (int i=0 ; i<10 ; i++)
{
Integer element = new Integer(i) ;
vec.addElement(element) ;
}
//⇒ 0 1 2 3 4 5 6 7 8 9
// Exemple de parcours sans itérateur :
for (int i=0 ; i<vec.size() ; i++ )
System.out.println( vec.elementAt(i) ) ;
// Exemple de parcours avec itérateur :
for (Enumeration e = vec.elements() ; e.hasMoreElements() ; )
System.out.println(e.nextElement()) ;
Les clés peuvent être n’importe quel objet implémentant la méthode hashCode()
de la classe Object. C’est par exemple le cas des chaînes de caractères (String) ou des
Wrapper. Une certaine clé ne peut identifier qu’une seule valeur.
Les conteneurs Hashtable sont très performants pour les opérations d’accès
directs aux objets contenus.
Voici un échantillon des méthodes disponibles :
– Object put(Object cle, Object elt) : Insère l’élément elt avec la clé cle
– Object remove(Object cle) : Supprime et retourne l’objet correspondant à la clé
cle
– Object get(Object cle) : Retourne l’objet correspondant à la clé cle
– boolean containsKey(Object cle) : Teste si un objet correspondant à la clé
existe
– keys() : Retourne une Enumeration sur les clés
– elements() : Retourne une Enumeration sur les éléments
class Adresse
{
private String nom, adresse ;
public Adresse(String nom, String adresse) { this.nom = nom ; this.adresse =
adresse ; }
public String toString() { return nom + " : " + adresse ; }
}
Hashtable carnet = new Hashtable() ;
carnet.put("Foulen Ben Foulen", new Adresse("Foulen Ben Foulen", "9, Rue Sans
Nom 5000 Gabes") ) ;
carnet.put("Foulena Ben Foulen", new Adresse("Foulena Ben Foulen", "18, avenue
Grande 3000 Sfax")) ;
...
Adresse rech = (Adresse)carnet.get("Foulen Ben Foulen") ;
Remarque : La méthode get() retourne une référence du type Object⇒ On est donc
obligés de convertir explicitement la référence retournée en objet de la classe Adresse.
Dans l’exemple précédent, on peut considérer que le conteneur carnet va permettre de
contenir l’ensemble des objets de la classe Adresse. Il serait donc logique de considérer
qu’il s’agit d’un attribut de cette classe et d’encapsuler ainsi la gestion des membres de
cette classe (avec des méthodes static). On reprend l’exemple :
class Adresse
{
private static Hashtable carnet = new Hashtable() ;
private String nom, adresse ;
public Adresse(String nom, String adresse)
{
this.nom = nom ; this.adresse = adresse ;
carnet.put(nom, this) ;
}
public String toString() { return nom + " : " + adresse ; }
public static Adresse get( String nom ) { return (Adresse)carnet.get(nom) ; }
public static Enumeration adresses() { return carnet.elements() ; }
}
new Adresse("Foulen Ben Foulen", "9, Rue Sans Nom 5000 Gabes") ;
new Adresse("Foulena Ben Foulen", "18, avenue Grande 3000 Sfax") ;
...
Adresse rech = Adresse.get("Foulen Ben Foulen") ;
for (Enumeration e = Adresse.adresses() ; e.hasMoreElements() ; )
System.out.println( e.nextElement() ) ;// inutile de convertir l’objet, car toString()
est une méthode de la classe Object