J A V A

 


Le langage Java. 6

Présentation. 6

Historique. 8

Les environnements de développement 8

Votre première application Java. 9

Le code de l'application. 9

Compilons ce programme. 9

Exécutons ce programme. 10

Quelques explications. 10

Commentez vos programmes. 10

Les types de base du langage. 12

La déclaration des variables. 12

Les types numériques. 13

Les types numériques entiers. 13

Les types numériques décimaux. 13

Le type booléen. 13

Le type caractère. 14

Le type chaîne de caractères. 14

Les wrapped classes. 14

Les tableaux. 15

Les expressions du langage. 17

Les expressions numériques. 17

Les expressions booléennes. 19

Les expressions retournant des chaînes de caractères. 19

Les affectations et les opérateurs associés. 20

Les autres opérateurs du langage. 21

Précédence des opérateurs. 22

Les instructions du langage. 23

Les blocs d'instructions. 23

L'instruction conditionnelle if 24

Les instructions liées aux boucles. 24

La boucle for. 24

La boucle while. 25

La boucle do while. 26

L'instruction continue. 26

L'instruction break. 28

L'instruction switch. 29

Le modèle objet de Java. 31

Un peu de vocabulaire. 31

Définition d'une classe. 32

Les attributs de classe. 32

Les méthodes. 32

La surcharge de méthodes. 33

Instanciation d'objet 34

L'allocation de mémoire. 35

Les constructeurs. 35

Les destructeurs. 36

Le ramasse miettes. 37

Méthodes et attributs statiques. 37

main : point d'entrée du programme. 37

Partage d'informations. 38

L'héritage. 39

Ce qu'il ne faut surtout pas faire. 39

Ce qu'il faut faire. 40

L'intérêt 42

Le polymorphisme. 42

Définition. 42

La liaison dynamique (dynamic binding) 43

Faire usage de l'abstraction. 45

Définir une classe abstraite. 45

Définir une interface. 46

Attributs et méthodes constants. 47

Attributs constants. 48

Méthodes finales. 49

Paramètres constants. 49

Les classes intérieures. 50

Les packages Java. 51

Création d'un package. 51

Le mot clé package. 51

Placer les fichier d'un package. 51

Importer les classes d'un package. 52

Correctement positionner la variable CLASSPATH. 52

Le mot clé import. 52

Accéder directement à une classe d'un package. 53

Compresser un package. 53

L'utilitaire jar 54

La variable d'environnement CLASSPATH. 54

Une petite subtilité. 54

Les droits d'accès. 56

L'encapsulation. 56

L'accès public. 56

L'accès private. 56

L'accès protected. 57

L'accès friendly. 58

Quelques conventions. 58

En conclusion. 58

La gestion de la mémoire. 59

Java et la mémoire inaccessible. 59

Le ramasse miettes. 60

La JVM du JDK.. 60

Mode de fonctionnement du ramasse miettes. 60

Les finaliseurs. 61

Conclusion. 62

Les exceptions Java. 63

On traite l'exception. 63

Le mot clé try. 63

Le mot clé catch. 63

Le mot clé finally. 63

Quelques exceptions. 63

Quelques méthodes utilisables sur les exceptions. 64

On relaye l'exception. 64

Le mot clé throws. 64

Définir ces propres exceptions. 64

Lever une exception. 65

Le mot clé throw. 65

Les threads en Java. 66

Introduction. 66

Qu'est ce qu'un thread ?. 66

Les threads en Java. 66

Création et démarrage d'un thread. 67

Créer un thread en dérivant de la classe java.lang.Thread. 67

Créer un thread en implémentant l'interface java.lang.Runnable. 69

Quelle technique choisir ?. 72

Gestion des threads. 72

Cycle de vie d'un thread. 72

Démarrage, suspension, reprise et arrêt d'un thread. 73

Gestion de la priorité d'un thread. 74

Gestion d'un groupe de threads. 75

Synchronisation de threads et accès aux ressources partagées. 75

Notions de verrous. 76

Attendre l'accès à une ressource. 77

Exemple de producteurs/consommateurs. 78

Conclusion. 81

Génération automatique de la documentation. 82

L'outil javadoc. 82

Réalisation d'une documentation simple. 82

Utilisation des commentaires javadoc. 83

Profiter du format HTML.. 84

Quelques instructions javadoc. 84

Documentation de la classe. 85

Documentation des méthodes. 85

Conclusion. 86

Les entrées/sorties en Java. 87

Java : le package java.net 88

Quelques définitions. 88

Ouverture d'une connexion en mode TCP/IP. 88

Ouverture d'une connexion en mode UDP. 89

Interfaces graphiques. 91

L'Abstract Window Toolkit (l'AWT) 91

L'API Swing. 91

Les sujets abordés par ce cours. 92

La gestions des évènements. 93

La gestion des évènements en Java. 93

Traitement d'évènements simples. 93

Définition d'un écouteur 93

Enregistrement d'un écouteur 94

Définition de classes privées. 95

Définition de classes anonymes. 96

Les classes d'adaptation. 97

Types d'évènements supportés par l'AWT et Swing. 98

Les stratégies de placement 99

Affecter une stratégie de placement à un conteneur 99

La stratégie FlowLayout 99

La stratégie BorderLayout 101

La stratégie GridLayout 103

Un premier exemple simple. 104

Embellissons un peu les choses. 105

La stratégie GridBagLayout 106

La stratégie CardLayout 106

La stratégie BoxLayout 106

Abstract Window Toolkit 107

Les diverses natures d'objets graphiques. 107

Les conteneurs. 107

Les frames. 107

Les boîtes de dialogue. 108

Les composants. 108

Les boutons. 108

Les cases à cocher 109

Les boîtes de sélection. 109

Création d'une barre de menu. 110

Anatomie d'une barre de menu simple. 110

Mise en place de la barre de menu. 110

Création d'un menu déroulant 110

Ajouter des éléments de menu. 112

Quelques autres aspects. 113

Ajouter un séparateur à un menu. 113

Dessiner en Java. 116

 


Le langage Java

Présentation

Java est portable : un programme, une fois compilé fonctionnera aussi bien sous des stations Unix, que sous Windows ou autre. Au point de vue du langage, il intègre tous ce que l'on sait faire de mieux en matière de langage de programmation, tout en évacuant les erreurs faîtes sur les langages dont il est issu (C et C++) : Son API (Application Programming Interface) est très riche : différents packages permettent d'accéder au réseau, aux entrées/sorties, aux différents composants graphiques

Une des caractéristiques principales de ce langage est que le code Java sera compilé pour une machine dite virtuelle (c'est-à-dire qui n'a pas forcément d'existence physique, mais son concept peut être reproduit sur une machine cette fois-ci réelle) : le code machine résultant est nommé ByteCode. Lors de l'exécution le code machine produit sera transformé en un code machine compréhensible par le microprocesseur que vous utilisez. La raison de ce choix est que Java se veut portable : le programmeur ne souhaite plus faire des adaptions pour que son programme puisse fonctionner sur telle ou telle machine. Si ce choix à des points avantageux, sachez qu'il en a aussi un qui l'est beaucoup moins : convertir une instruction de la JVM (Java Virtual Machine) en une instruction compréhensible par la machine où s'exécute le programme, prend du temps. Pour palier ce problème, deux solutions ont été proposées. La première, c'est d'inclure dans le compilateur, la possibilité de compiler du code natif (pour une machine spécifique). La seconde est plus complexe : l'interprète de la JVM, inclue un JIT (Just In Time compiler). Le JIT est chargé de traduire le code JVM d'une classe) directement en code natif, et ce dès la première utilisation de la classe. Cette phase de traduction prend certes un peu de temps, mais une fois celle-ci effectuée, et ce jusqu'à la fin de l'exécution du programme, plus aucune traduction de code (sur la classe considéré) ne sera refaite. Cette technique améliore considérablement la rapidité d'exécution des programmes, bien qu'une phase de traduction persiste. Une troisième solution fera peut-être son apparition dans un proche avenir. Elle consisterait à intégrer une puce Java (comprenant le code de la JVM) dans les processeurs des machines. Mais cette solution, n'est pas encore d'actualité.

Ce langage est un vrai langage de programmation : il intègre tous ce que l'on sait faire de mieux en matière de langage de programmation, tout en y enlevant ce qui s'est avéré être des erreurs du passé. Java est un langage orienté objet : la brique de base du programme est donc l'objet, instance d'une classe La gestion de la mémoire n'est plus à la charge du programmeur, enfin : en effet, des techniques de désallocation automatisée de la mémoire existent depuis plus de 20 ans, elles sont très efficaces, et seuls quelques langages les. Il n'y a pas, dans Java, de mécanisme de macro-génération de code, comme c'est le cas en C ou en C++, ainsi que pour d'autres langages. La syntaxe, ainsi que quelques points de sémantique, sont inspirés de C++ et donc, par conséquent, de C. Ce dernier point, est à mon sens une bonne chose : ces langages sont désormais rodés, ils ont fait leur preuve, et compte désormais de nombreux adeptes de leurs très célèbres opérateurs (variable++; par exemple). Mais cela ne plaît pas à tout le monde. Java est aussi multithreadé, distribué, robuste et sûr (sur ce point beaucoup de choses ont été faites), ...

Java possède une API (Application Programming Interface) extrêmement riche.. En effet, sans celle-ci, Java n'est rien. C'est par elle que le programmeur va pouvoir accéder à toutes les ressources de la machine, et celles du réseau Internet. Cette vaste librairie est divisée en packages : des ensembles de classes. Voici en quelques points, les grands packages (certains c'entre eux ont des noms de code) qui sont fournis en standard avec Java :

·         Pour la mise en oeuvre d'interfaces graphiques, deux packages principaux vous sont offerts : java.awt et javax.SWING.

o    L'AWT (Abstract Window Toolkit) fournie des méthodes d'accès à toutes les ressources graphiques de la machine. Les éléments graphiques utilisés sont ceux du système d'exploitation hôte. Cependant, les concepteurs de l'AWT se sont assurés que tous les comportements proposés au sein de cette librairie (ce package) soient supportés par tous les environnements pouvant supporter Java.

o    La librairie Swing, quand à elle, propose des composants graphiques totalement pris en charge et dessinés par la JVM. Cela permet de garantir que l'applicatif aura le même look visuel, ce quelque soit le système d'exploitation hôte. En conséquence, SWING et plus riche que l'AWT, mais aussi moins efficace et terme de temps d'exécution.

·         Le package java.applet : vous trouverez dans ce package, tous ce dont vous aurez besoins pour réaliser des applets (application pouvant s'exécuter dans un navigateur Internet. Il doit être utilisé conjointement avec l'AWT : en effet, dans un document HTML, l'applet occupera une zone graphique.

·         Le package java.beans : tout pour la programmation orientée composants. Les Java Beans sont des composants logiciels réutilisables et visuellement manipulables dans un atelier de programmation (Comme NetBeans ou JBuilder notamment).

·         Le package java.lang : fournit un maximum de classes liées au langage : manipulation de chaînes de caractères, manipulations des types de bases, récupération de métaclasses, prise en charge des flots standards de l'applications. On y trouve aussi la classe Object : classe mère de tout objet Java. Ce package est par défaut directement accessible, contrairement aux autres packages, qu'il vous faudra explicitement importer.

·         Le package java.io : un ensemble incroyable de classes pour gérer toutes vos opérations d'entrées/sorties. Il y a vraiment de quoi s'y perdre.

·         Le package java.sql (JDBC : Java DataBase Connectivity) : ce package regroupe toutes les fonctionnalités permettant d'accéder à des bases de données diverses. L'utilisation en est très simple, mais quelques bases en SQL (Structured Query Language) sont requises.

·         Le package java.rmi (Remote Method Invocation) : ce package permet (relativement facilement) de mettre en place des applications réparties. Dans de telles applications, le code n'est pas localisé sur une unique machine, mais au contraire, déployé sur plusieurs.

Et encore  d'autres packages : java.net, JNDI, JAXP, les servlets et pages JSP, ... Dans tous les cas, nous verrons en détails tous ces packages, et d'autres, dans les chapitres qui leur sont consacrés.

De plus Java, est simple (relativement aux autres langages orientés objets : C++ notamment) à appréhender, car il offre, comme nous l'avons dit, tous les mécanismes nécessaires à un langage orienté objets, tout en étant débarrassé de mécanismes souvent sources d'erreurs. Il apparaît donc au terme de cette petite présentation, que Java est une solution aux problèmes informatiques du moment. De plus, comme il est encore jeune, il devrait facilement pouvoir s'adapter aux évolutions futures de l'informatique.

Historique

Le langage Java trouve ses origines dans les années 1990. A cette époque, quelques ingénieurs (innovateurs) de SUN Microsystems ont commencés à parler d'un projet d'environnement indépendant du hardware pouvant facilement permettre la programmation d'appareils aussi variés que les téléviseurs, les magnétoscopes,... James Grosling (un ingénieur de SUN Microsystems) développa un premier langage permettant de programmer dans cet environnement : Oak. En 1992, tout était près pour envahir le marché avec cette nouvelle technologie : ce fut en échec.

Un homme sauva malgré tout le projet : Bill Joy (co-fondateur de SUN Microsystems). En effet, devant la montée en puissance d'Internet, il lui a semblé intéressant de proposer un tel langage (et un tel environnement). Effectivement, Les principaux problèmes rencontrés sur Internet sont liés à l'hétérogénéité des machines et des logiciels utilisés.

Dès lors tous s'accélère. Oak est renommé (en 1995) en Java et il est soumit à la communauté Internet grandissante. Une machine virtuelle, un compilateur ainsi que de nombreuses spécifications sont données gratuitement et Java attaque une conquête fulgurante. Aujourd'hui, après de nombreuses améliorations (parfois modifications) Java n'est plus uniquement une solution liée à Internet : de plus en plus de sociétés (ou de particuliers) utilise ce langage pour leurs développements (de toutes sortes).

Les environnements de développement

Il existe un grand nombre d'environnements de développement pour Java (surtout sous PC). Ils présentent un grand nombre de points communs : éditeur intégré, compilateur, débugger sophistiqué, nombreux générateurs de code (surtout pour ce qui touche aux interfaces graphiques). Le plus souvent, ils sont payants.

Par contre, la société SUN propose un environnement minimal mais suffisant (un compilateur et une machine virtuelle) : le JDK. Ce dernier est gratuit et se télécharge directement à partir de l'adresse http://java.sun.com. Pour débuter, il est très bien. Mais si vous désirez poursuivre avec Java, il faudra alors acquérir un environnement plus adapté.

Pour la suite de ce cours, on considérera que vous utilisez le JDK de SUN. En conséquence, les ordres de compilations, et autres seront donnés pour pour cet environnement. Si vous en utilisez un autre, il sera alors a votre charge de déterminer les actions correspondantes. Il est bien entendu, que pour tout ce qui relève du langage, il n'y aura bien sûr pas de modification.

 


Votre première application Java

Nous allons étudier, comme première application java, un programme qui affichera "Bonjour tout le monde".

Le code de l'application

Veuillez saisir le programme suivant dans votre éditeur de texte. Il me semble que le plus approprié pour cette opération serait un bon vieux Copié/Collé (Copy/Paste en anglais).

Fichier "BonjourMonde.java"

/*****************************************************
*** Ce programme se contente d'afficher le méssage ***
*** "Bonjour tout le monde !"                      ***
*****************************************************/
public class BonjourMonde {
 
  // Définition de la méthode statique main
  public static void main(String params[]){
    System.out.println("Bonjour tout le monde !");
  }
}

Une fois le code saisi, sauvegardez le sous nom suivant "BonjourMonde.java". Il est très important de respecter les majuscules et les minuscules : plus exactement, nom du fichier doit être le même que celui qui suit le mot clé class dans le code de l'application.

Compilons ce programme

Une fois la sauvegarde effectuée, il vous faut compiler le programme. Cette phase de compilation est obligatoire car le code Java n'est pas directement compréhensible par les machines actuelles. La raison essentielle étant que les ordres du langage sont trop complexes : il est donc nécessaire de pouvoir diviser ces ordres en une multitude de micro-ordres (dans le sens ou sémantiquement ils sont réellement plus petit). On obtient ainsi un programme décrit en langage machine, et donc directement exécutable pas le microprocesseur. Un détail très important à ce sujet, et qu'il ne faut pas oublier est que le langage machine produit ne serra compréhensible que par une JVM (Java Virtual Machine) et donc par définition, cette machine n'existe pas (bien que des puce Java commencent à apparaître) : on verra dans quelques instant comment contourner le problème. Pour réaliser cette tache, plusieurs pré requis sont demandés. Nous allons donc les voir chacun dans le détail.

·         Le répertoire de travail : il est clair qu'il est conseillé de mettre votre code Java dans un répertoire propre à chaque exercice (pour ce cours) ou pour chacun de vos projets. Sinon, vous risquez vite de ne plus savoir à quoi correspond chaque fichier.

·         Le shell : pour pouvoir manipuler des programmes Java, il faut (si on utilise le JDK) ouvrir un shell de commande (COMMAND.COM sous Dos ou zsh (par exemple) sous Unix). En effet c'est grâce au shell qu'on va pouvoir donner les ordres de compilation et d'exécution. Petite remarque : chargez le répertoire de travail de façon à pouvoir accéder au fichier que nous devons compiler.

·         La variable d'environnement PATH : celle-ci doit bien entendu contenir le chemin d'accès aux exécutable du JDK.

·         La variable d'environnement CLASSPATH : cette variable doit contenir les différents répertoires ou se trouvent les fichiers de byte-code (code machine de la JVM) nécessaires à l'exécution de votre application. Assurez vous d'hors et déjà, quelle contient bien le répertoire courant, nommé par un point, et un répertoire propre au JDK (souvent directement un fichier .ZIP). Depuis le JDK 1.2, cette variable n'est plus à fixer.

Maintenant que tout cela est au point, il ne nous reste plus qu'à effectuer la tache. Pour cela, rentrez la ligne suivante directement sous le shell : "javac BonjourMode.java". Si tout ce passe bien, il ne devrait pas y avoir de messages retournés. L'invite de commande (le prompt) doit attendre une nouvelle commande.

Exécutons ce programme

Il ne nous reste donc plus qu'à exécuter notre programme. Mais rappeler vous ce que je vous ai dit dans l'introduction de ce cours : la machine pour laquelle le code est généré, est virtuelle (c'est à dire qu'elle n'existe pas). Cette particularité est un gage de portabilité. Il faut donc pour exécuter votre application, lancer l'interprète pour la machine virtuelle. On lui passe en paramètres le nom de la classe principale (celle qui sert au démarrage de l'application). Ici c'est très simple car on n'en a qu'une seule : c'est la classe BonjourMonde. Tapez donc sous le shell la commande suivante :

>java BonjourMonde
Bonjour tout le monde !
> 

Tout devrait se passer normalement, et vous devriez pouvoir lire le message "Bonjour tout le monde !" s'afficher sur votre console.

Quelques explications

Bon je crois que quelques explications sont nécessaire quant au contenu du programme Java. On arrive facilement à discerner trois niveau d'imbrication : au plus haut niveau, on a la définition de la classe nommée BonjourMonde. Ensuite, on y trouve la définition d'une fonction nommée main : comme son nom l'indique, il s'agit du point d'entré pour l'exécution du programme. Le dernier niveau contient le code de la fonction (la méthode est un nom plus adaptée si l'on respecte la terminologie des langages orientés objets). Celui ci se contente d'afficher le message. Mais nous reverrons tout cela plus en détails par la suite.

Commentez vos programmes

Si vous avez bien regardé le code précédent, vous avez du remarquer que certaines parties du code étaient en italique. Ces parties correspondaient à des commentaires. A titre indicatif, un commentaires permet de placer de l'informatique, au sein d'un programme, qui est non significative au niveau de l'exécution du programme, mais qui prend tout son sens lors de la compréhension de ce dernier. Donc, si vous avez été attentif, vous avez du remarquer qu'il y avait deux méthodes pour placer des commentaires. Dans les deux cas, la syntaxe est reprise sur le langages C++ (lui même inspiré par le langage C). Une description plus complète suit.

La première méthode permet de placer un commentaire sur plusieurs lignes. Il est donc nécessaire de placer un marqueur de début de commentaire ainsi qu'un marqueur de fin. La syntaxe étant reprise sur le langage C, on a donc, pour le marqueur de début, les deux signes /* et pour le marqueur de fin les caractères */. Voici donc quelques exemples à titre indicatif.

int a=10;   /* une variable numérique */
int /* Voici maintenant deux lignes 
de commentaires noyées dans du code*/ a=10;

La seconde permet de fixer la fin de la ligne en commentaire, et uniquement la fin de la ligne courante. L'exemple qui suit sera plus parlant.

int a=10;   // je decris ici l'instruction précédente

 

 

 


Les types de base du langage

Comme beaucoup d'autres langages, Java possède un certain nombre de types de bases. Mais contrairement au autres, Java impose que la représentation interne de ces types de bases soit uniforme (même espace mémoire) et ce, quelque soit la machine sur laquelle tourne le programme. En effet, cette uniformisation est un gage de portabilité pour les applications écrites en Java.

La déclaration des variables

Pour un peu situer (ou resituer) les choses, une variable est en quelques sortes la carte d'identité d'un petit élément (une donnée) du programme. Ce petit élément est totalement définit par la connaissance des informations suivantes :

·         le type de la donnée : nous allons très rapidement revenir sur la notion de type. En simplifiant les choses, on pourra dire qu'un type correspond à un ensemble de valeurs que pourra prendre une données qui aura était définit à partir de ce type. Par exemple, le type entier, définit un ensemble de valeurs numériques, non décimales, compris entre une borne minimale et une borne maximale (nous en reparlerons).

·         le nom de la variable : ce nom permet d'identifier ce petit élément de programme. En l'utilisant, on pourra alors affecter, à la donnée mentionnée, une valeur (bien entendu, contenue dans l'ensemble de valeurs définit par le type).

·         une éventuelle valeur initiale : pour pouvoir correctement utiliser cette donnée, il pourra être judicieux de lui donner une valeur initiale. Par la suite, la valeur de la variable pourra malgré tout changer (sauf dans quelques cas précis).

Bien entendu, il faut, pour que le compilateur comprenne ce qu'on veut lui dire, que la définition des variables respecte une certaine syntaxe. Toute déclaration débute en mentionnant le type utilisé pour la (ou les) variable(s) considéré(s). Ensuite, on faire suivre le nom de chaque variable à définir (séparées par des virgules). Un point-virgule final permet de terminer la déclaration. Pour chaque variable, on peut à la volée, mentionner la valeur initiale en utilisant le signe d'égalité suivit de la valeur : le compilateur fera alors attention à ce que chaque valeur soit compatible avec le type utilisé. Attention à une chose : le langage fait la distinction entre minuscules et majuscules. La variable nommée i est donc distincte de celle nommée I. Saisissez (ou mieux copier à partir de votre navigateur) la section de code suivante, compilez et testez là afin de bien comprendre comment on définit une variable. Tester d'autres valeurs. Essayer de forcer une valeur 3.2 pour j : qu'en concluez-vous ?

Fichier "Variables.java"

public class Variables {
 
  public static void main(String params[]){
     // Définition de deux variables entières i et j
     // i sera de plus initialisée avec la valeur 15
     int i=15 , j;
 
     // Définition d'une chaîne de caractères avec
     // comme valeur initiale "Cours Java"
     String chaine = "Cours Java";
 
     // On donne une valeur à j
     j = 10;
 
     // On utilise ces variables en les affichant
     System.out.println("i = " + i + " et j = " + j);
     System.out.println("chaine = " + chaine);
  }
}

Les types numériques

Le langage propose un grand nombre de types numériques. On peut classer ces types en deux catégories : les types entiers et les types décimaux. Ce qui différencie les types d'une même catégorie, c'est la taille de mémoire utilisée pour contenir une valeur. Plus cette taille est grande, plus les valeurs utilisables sont nombreuses. Vous choisissez donc un type en fonction de l'étendu de valeur souhaitée pour vos variables.

Les types numériques entiers

Quatre types d'entiers vous sont proposés. La différence réside dans la taille nécessaire pour stocker la valeur. Le tableau qui suit précise un petit peu mieux les choses. Le bit y est utilisé pour décrire les tailles. Notez qu'un bit ne peut prendre que deux valeurs (0 ou 1). En conséquence, n bits ne peuvent définir que 2n valeurs. On regroupe traditionnellement (c'est lié à des aspects électroniques) les bits par groupe de huit, ce qui forme un octet (byte en anglais). Un octet peut donc contenir 28 (soit 256) valeurs distinctes.

 

byte

short

int

long

Taille (bits)

8

16

32

64

Etendue

0 .. 255

-32768 .. 32767

-231 .. 231-1

-263 .. 263-1

Les types numériques décimaux

De même que pour les type numériques, vous avez deux types pour les nombres décimaux, la seule différence résidant dans la taille utilisée pour stocker une valeur de ce type.

 

float

double

Size (bits)

32

64

Exemple de valeurs

3.25f

3.25

Le type booléen

On introduit une variable de ce type pas le mot clé boolean. Ce type accepte seulement deux états : l'un est nommé true et symbolise un état d'acceptation, l'autre, nommé false, symbolise un état de réfutation. Attention, ce n'est plus comme en C : le type booléen n'est pas en Java, un sous-type numérique (je rappelle qu'en C, la valeur 0 est considérée comme fausse et les autres valeurs entières comme vraies).

Le type caractère

Ce type, introduit par le mot clé char, permet la gestion des caractères (comme son nom le laisse présager). Jusqu'à présent, la plupart des langages utilisaient à cet effet le codage ASCII (ou des dérivés). Le codage ASCII permet de représenter seulement 128 caractères : en effet à l'origine, seul les sept bits de poids faibles d'un octet servaient au codage du caractère, le huitième servait alors de bit de parité assurant le bon transport du caractère à travers le réseau. Ensuite, des couches de protocole de transport de données plus sûres ont rendus ce huitième bit utilisable. Différents systèmes dérivés d'ASCII ont fait leur apparition (la table ISO-Latin-1, ...). Malheureusement, aucun de ces standards n'a su s'imposer de manière universelle, et l'échange de données entre différents systèmes reste très problématique. N'avez vous jamais reçu d'email parasité aux niveaux des caractères accentués (il ne font pas partie de la table ASCII).

Pour palier les problèmes engendrés, un nouveau standard de codage de caractères, universel, fut proposé : ce codage est nommé Unicode. Unicode utilise 16 bits pour représenter un caractère et permet donc, potentiellement, de coder 65536 caractères et donc nos caractères accentués, tout comme d'autres caractères provenant d'autres alphabets (Cyrillique, Hébreux, Arabe, Chinois, Grec, ...). Si vous souhaitez obtenir un aperçu de la table Unicode, il vous suffit d'activer ce lien.

Les concepteurs de Java, dans un souci de portabilité du code produit, ont donc trouvés judicieux d'utiliser ce standard. Les exemples qui suivent vous donne la syntaxe à utiliser pour décrire un caractère en Java.

'a'

'\t' pour un tab

'\u0521' un caractère quelconque

Le type chaîne de caractères

Première remarque, par rapport au langage C, le type chaîne de caractères et totalement différent du type tableau de caractères (la description des tableaux Java se trouve quelques paragraphes plus bas). Pour ce qui est de la syntaxe d'un chaîne de caractères, rien n'a changé par rapport à C ou C++ : une chaîne de caractères commence par un caractère double guillemets et se termine aussi par ce même signe. Ainsi, "Dominique" est une chaîne de caractères. Là ou ca se complique un petit peu, c'est que le type d'un telle valeur est String, qui est une classe fournie par la machine virtuelle.

Les wrapped classes

Quasiment de même que les chaînes de caractères possède une classe (en fait une chaîne est un objet), tous les types de bases (que l'on vient de passer en revue) possède une classe, dite wrapped classe, qui propose des fonctionnalités sur des élément du type de base considéré. Ces classes seront étudiée dans le chapitre consacré au package java.lang.

 

Les tableaux

De même que beaucoup d'autres langages, Java permet lui aussi de définir des tableaux. Pour ceux qui ne connaissent pas encore ce concept, on dira qu'un tableau est la succession d'un certain nombre d'éléments d'un type particulier : on peut donc, par exemple, obtenir un tableau (une succession) de dix float.

Un remarque importante : on peut créer un tableau d'élément de n'importe quel type. Une seconde remarque tout aussi importante (et lourde de conséquence) : un tableau d'élément d'un type donné possède lui aussi un type : le type tableau d'élément du type initial. On peut donc créer des tableaux de tableaux : c'est ce mécanisme qui permet de simuler de tableaux multidimensionnels.

int monTableau[];
nomTableau = new int[10];

un tableau de dix entiers

int tableau[][] = new tableau[10][];

un tableau de dix tableaux d'entiers

int a, b[];

un entier (variable a) et un tableau de dix entiers (variable b)

int[] monTableau2 = new int[10];

un tableau de dix entiers

int[] a, b[];

un tableau d'entier non alloué (variable a) et un tableau de tableaux d'entiers non alloué (variable b). On voit donc que si l'opérateur [] se trouve avant les variables, il agit alors sur toutes les variables de la déclaration.

int a[], b[][];

cette déclaration est donc équivalente à la précédente.

Quelques exemples de définition de tableaux en Java

Si tout ne vous a pas semblé limpide, ce n'est pas trop grave. Nous allons reprendre très clairement tous les points importants pour créer un tableau. Premièrement, vous devez déclarer une variable comme étant de type tableau de quelque chose : en effet, si vous ne le faîte pas, vous ne pourrez plus, ultérieurement, accéder à celui-ci. Pour ce faire, vous devez placer l'opérateur [] dans une déclaration de variable. Deux possibilités sont possibles : soit vous placez l'opérateur immédiatement après le type (et donc devant les variables), soit vous le placez derrière chaque variable. La subtilité tient dans le fait que si vous choisissez la première possibilité, l'opérateur agira sur toutes les variables de la déclaration. Dans le second cas, il agira uniquement sur la variable qui le précède. Dans les deux cas, vous définissez au moins un tableau. Deuxièmement point important, vous devez allouer de la mémoire pour contenir les données de ce tableau. Cela se fait en utilisant l'opérateur new que nous étudierons plus tard. Cet opérateur doit forcément connaître la taille du tableau pour le créer : on la spécifie, placée entre des crochets, directement après la variable. Il est possible de réunir ces deux étapes en une seule opération, comme le montre les exemples précédents.

Il est possible de connaître la taille d'un tableau, ce à tout moment, grâce à l'expression suivante : variableTableau.length;. Nous reviendrons plus en détail sur la manipulation des tableaux, tout au long de ce cours.

Nous en avons donc finit de la présentation des types de bases du langage. Nous pouvons, par conséquent, passer à l'étude des expressions du langage, qui vont manipuler les types maintenant supposé connus.

 


Les expressions du langage

Comme beaucoup d'autres langages, Java manipule bien sûr des expressions, et donc des opérateurs (de différentes natures). Nous allons donc, au travers de ce chapitre, étudier l'ensemble des opérateurs du langages ainsi que les différentes natures d'expressions du langage.

Les expressions numériques

Les opérateurs que je nomme numériques, calculent tous un résultat numérique. Cependant, il faut bien noter qu'il y a plusieurs opérateurs qui ont le même nom (par exemple les opérateurs nommés +) mais qui s'appliquent sur des types différents : l'exemple qui suit vous fixera mieux les idées.

int i = 4 + 3; 

Dans ce cas, l'opérateur + prend deux entiers (chacun sur 32 bits) et rend un entier, lui aussi représenté sur 32 bits.

double d = 3.2 + 1.84; 

Ici, l'opérateur d'addition, prend deux opérandes de types décimaux (représenté chacun sur 64 bits) et retourne un flottant du même type.

En règle générale, il y a autant d'opérateurs + que de types numériques. Mais la question est alors de savoir ce qui doit se passer si l'on tente d'ajouter un entier avec un décimal. Il est clair que le résultat doit aussi être décimal. En conséquence, c'est le deuxième cas donné en exemple qui s'applique, avec une phase implicite de conversion de la valeur entière en valeur décimale. Cette phase de conversion est plus connue sous le nom de casting. Toujours en règle générale, un cast est implicitement effectué s'il est nécessaire, et s'il n'y a pas de perte de précision sur la valeur numérique. Il est donc clair qu'un cast implicite d'un décimal en un entier ne serra jamais possible. Il faut alors explicitement demander le cast. L'exemple qui suit vous donne la syntaxe à utiliser : il suffit de préfixer la valeur par le type, mit entre parenthèses.

int i = 4 + (int)3.8; 

Dans ce cas, l'opérateur + prend deux entiers (chacun sur 32 bits) et rend un entier, lui aussi représenté sur 32 bits. Le résultat est 7 car 3.8 donne 3 après le cast.

Une fois cela assimilé il faut comprendre encore deux ou trois petites choses. Un opérateur, par définition, prend un certain nombre d'expressions (ou opérandes) pour calculer une valeur qui pourra à son tour être utilisée par un autre opérateur. En d'autres termes, une expression est constituée d'opérateurs et de sous expressions, comme le montre l'exemple suivant.

int i = 3 * 6 + 2 * 9; 

Finalement, la variable i de type entier sur 32 bits, contiendra la valeur 36. En effet, des règles de priorité sur les opérateurs, impliques qu'un opérateur va primer sur un autre selon les cas de figure. Donc en reprenant l'exemple suivant, la valeur est calculée en faisant la somme du produit de 3 par 6 et de 2 par 9 (soit 18 + 18). Se reporter à la table de précédence des opérateurs pour plus d'informations.

Nous trouvons notamment, comme opérateurs numériques, la somme (+), la soustraction (-), la multiplication (*), la division (/). Ils s'appliquent tous sur des opérandes numériques quelconques (entières ou flottantes). On en trouve aussi qui s'applique uniquement à des opérandes entières : le reste de la division entière (%), le et bit à bit (&), le ou inclusif bit à bit (|) ou encore le ou exclusif bit à bit (^), le décalage à gauche, bit à bit, (<< équivalent à un multiplication par 2) et le décalage à droite (>> équivalent à une division entière par 2).

Nous trouvons aussi deux opérateurs permettant d'incrémenter (++) ou de décrémenter (--) d'une unité une variable numérique (entière ou flottante). Il faut noter que ces deux opérateurs sont unaires et surtout que l'on peut les utiliser en préfixe ou en postfixe d'une variable. Dans ce cas l'action sémantique n'est pas la même. Pour comprendre cela, regardons l'exemple qui suit.

Fichier "Essai.java"

public class essai {
    public static void main(String args[]){
        int a = 10;
        System.out.println("a = " + a + ";\t(++a*2) = "
                           + (++a*2) + ";\ta = " + a + ";");
        a = 10;
        System.out.println("a = " + a + ";\t(a++*2) = "
                           + (a++*2) + ";\ta = " + a + ";");
  }
}

Résultats

a = 10; (++a*2) = 22;  a = 11;
a = 10; (a++*2) = 20;  a = 11;

Bon si vous avez toujours pas fuit votre écran devant ces quelques lignes indéchiffrables, je vais pouvoir tenter de vous expliquer ce qu'elles font, et pourquoi elles le font. Donc, comme dans le cas de votre première application Java, le programme commence par la définition d'une classe (ici nommée essai). Ensuite, on y spécifie notre seule méthode, qui doit servir de point de départ à l'application : on la nomme donc main (nous reviendrons sur la nécessité du static ultérieurement, sachez seulement qu'il est obligatoire pour la méthode main, idem pour le public).

La ligne qui suit sert à créer une variable nommée à laquelle on affecte la valeur 10. Ensuite, tout se complique, mais ça reste du domaine du compréhensible. System est un objet prédéfinit en Java qui permet d'avoir accès au système (comme son nom l'indique). Cet objet contient dans ces attributs un autre objet nommé out qui lui donne accès au flot de données de sortie de l'application, et on applique la méthode println qui y place un message (passé en paramètre) ainsi qu'un retour chariot.

Toute la difficulté réside dans le fait de comprendre le message. Ce dernier commence par une chaîne de caractères suivit d'un opérateur +. Il est bien clair, qu'il ne s'agit donc pas de l'opérateur dont on a déjà parlé : celui-ci réalise la concaténation de deux chaînes de caractères (nous en reparlerons). Je vous sens alors choqué : ce qui suit n'est pas une chaîne de caractères. Mais par contre, en Java, tous peut s'exprimer en ce type. Comme il n'y a pas, à priori, de perte de données, le casting d'un nombre en une chaîne de caractères est donc accepté, et le tour est joué. A force de pratiquer, ces petites astuces vous deviendrons familières.

Ce n'est pas finit, on trouve ensuite une autre chaîne de caractères contenant les deux caractères "\t" : ils servent à placer une tabulation dans la chaîne. Ensuite on trouve notre fameux opérateur ++ (et oui, vous l'aviez déjà oublié, mais il est toujours le sujet principal de notre débat) : lors de sa première apparition, il est utilisé en préfixe, tandis qu'il l'est en postfixe dans sa seconde apparition. Si vous regardez les résultats (ceux affichés par les System.out.println, vous vous apercevrez que la valeur calculée n'est pas la même, bien que l'incrémentation de la variable a soit faites. La raison en est très simple : dans le premier cas, on incrémente la variable, et on finit le calcul. Dans le second cas, on effectue le calcul, puis ensuite, on n'oublie surtout pas d'incrémenter la variable. Faites les calculs à la main et vous verrez que vous trouverez alors les mêmes valeurs. Est-ce maintenant plus clair ? Si oui, sachez que l'opérateur -- fonctionne de même (bien qu'il effectue une décrémentation). Si au contraire ce n'est pas le cas, faites une pause, prenez un café, et reprenez ce paragraphe.

Les expressions booléennes

De même qu'il existe des expressions qui calculent des résultats numériques, il en existe qui calculent des résultats booléens (c'est à dire un résultat pouvant être interprété soit comme vrai, soit comme faux). Il existe donc des opérateurs booléens nécessaires à la construction des ces expressions. Nous y trouvons notamment des prédicats (autre façon de nommer ces opérateurs) de comparaison numérique (==, il en existe autant que de types numériques et des conversions peuvent exister), de différenciation (!=), d'infériorité (strictes (<) ou non (<="))," de supériorité (> et >=). Nous trouvons aussi le ou logique (||) et le et logique (&&) qui prennent tous deux des opérandes booléennes. Il existe encore l'opérateur unaire (qui prend uniquement une seule opérande) de négation (!).

boolean egal=(3==4);

int a=3,b=4;
boolean diff=((a<b)||(a>b));

La variable egal vaut donc à cet instant false. la variable diff sera elle vraie soit si a est plus petite que b, soit si elle est plus grande (donc différente). Sinon diff sera faux.

boolean bool1=(3==4);
boolean bool2=(3.0==4.0);

Il est bien clair que dans ces deux cas, les opérateurs de comparaisons ne sont les mêmes. Un prend deux opérandes entières, l'autres deux opérandes décimales.

Les expressions retournant des chaînes de caractères

Comme nous l'avons dit précédemment, un opérateur agissant sur des chaînes de caractères, permet des constituer des expressions de type chaîne de caractères. L'opérateur + permet de concaténer deux chaînes pour en former une nouvelle (nous avons déjà vu un exemple d'utilisation il y a quelques paragraphes).

 

Fichier "Essai.java"

public class Essai {
    public static void main(String args[]){
        String a = "Le début et ...";
        String b = " la fin !!!";
        String c = a + b;
 
        System.out.println(c);
    }
}

Résultat :

Le début et ... la fin !!!

Une chose utile à se rappeler est que toute valeur en Java (pas forcément numérique) peut s'afficher sous forme de chaîne de caractères. L'expression ("a "+1) aura donc pour valeur "a 1".

Les affectations et les opérateurs associés

Une affectation est aussi une expression qui calcule une valeur et l'affecte à une variable. La valeur d'une telle expression est celle calculée dans la partie droite de l'affectation (les opérateurs d'affectation étant binaires infixes). L'opérateur permettant de définir une affectation sera noté =.

Fichier "Essai.java"

public class essai {
    public static void main(String args[]){
        int a;
        System.out.println(
            "Premier = " + (a=10)*2 + ";\t" +
            "Second = " + a + ";"
        );
    }
}

Résultat :

b = 20;        a = 10;

A partir de là, et dans un souci de convivialité, d'autres opérateurs d'affectation sont définis. Le principe reste quasiment le même, à la différence que la valeur qui va être affectée à la variable, est calculé en fonction de l'état de cette variable. Pour mieux se fixer les idées regardons le tableau suivant qui fixe les équivalences entre ces nouveaux opérateurs et l'opérateur d'affectation classique.

a += b;

a = a + b;

a -= b;

a = a - b;

a *= b;

a = a * b;

a /= b;

a = a / b;

a %= b;

a = a % b;

a &= b;

a = a & b;

a |= b;

a = a | b;

a ^= b;

a = a ^ b;

a >>= b;

a = a >> b;

a <<= b;

a = a << b;

Les autres opérateurs du langage

Il existe encore d'autres opérateurs dont une petite description sera donnée dans le tableau suivant. Nous seront, par la suite amenés à en reparler.

new

Opérateur d'allocation mémoire

int tableau[]=new int[10];

Il sera étudier plus en détail dans le chapitre consacré à la programmation orientée objet.

[]

Accès aux éléments d'un tableau

b = tableau[indice];

Attention, les indices d'un tableau commencent toujours à partir de zéro. Si un tableau contient dix éléments, les indices varierons donc entre 0 et 9.

.

Opérateur de traversé, utilisé pour accéder aux champs (attributs et méthodes) des objets.

int value = objet.champQuelconque;

 

?:

L'opérateur conditionnel : c'est le seul opérateur ternaire (qui prend trois opérandes) du langage. Il permet de calculer une expression plutôt qu'une autre selon la valeur booléenne d'une autre expression. L'expression qui sert à choisir quelle autre expression calculer se place devant le point d'interrogation. Celle qui sera calculée si le test est vrai doit être mise entre le point d'interrogation et les deux points, la dernière, après les deux points.

boolean b = true;
// plein de code Java
int value = b ? 3*a+z : 3*a-k;

,

Cet opérateur permet de constituer une expression à partir de deux sous-expressions. Ceci est utile dans les constructions du langage qui n'accepte qu'une unique expression à un endroit particulier (nous en verrons). L'exemple qui suit sera expliqué en détail dans le chapitre suivant.

while((step=f(x),a-=step)>0) g(a);

Précédence des opérateurs

Comme vous l'avez certainement déjà remarqué, il est possible de se servir de parenthèses pour fixer l'ordre d'évaluation des sous-expressions formant une expression Java. Ainsi, les expression 3*2+4 et 3*(2+4) n'auront pas les mêmes valeurs (10 et 18). Mais attention tout de même : l'utilisation abusive des parenthèses rend très rapidement un code illisible. Dans le but de ne pas arriver dans de telles situations, sachez que des règles de priorités ont étaient définies sur les opérateurs. Le tableau suivant fixe cet ordre : du plus prioritaire à l'opérateur de séquencement (,) qui est le moins prioritaire. Dorénavant, utilisez donc bien les parenthèses.

[ ] .

! ++ --

* / %

+ -

<<>>

== != <<=> >=

&

^

|

&&

||

?:

= += -= *= /= %= <<=>>= &= ^= |=

,

 

Nous venons donc de survoler l'ensemble des opérateurs, ainsi que quelques expressions du langage. Ils nous seront fort utile pour calculer des valeurs (numériques ou autres) dans nos programmes. Mais cela ne fait pas tout, il nous faut aussi pouvoir définir le comportement général du programme. Dans ce but, nous allons nous attacher, dans le chapitre suivant, à étudier les instructions du langage.


 

Les instructions du langage

Dans le chapitre précédent, nous avons vu ce qu'étaient les expressions : celles-ci permettent de calculer des valeurs. Parallèlement, nous avons les instructions qui permettent de lancer des actions. En composant les instructions, on définit ainsi le comportement que le programme devra adopter. Pour mieux comprendre les choses, nous allons, tout au long de ce chapitre, étudier une à une, toutes les instructions utilisable en Java. Ceux d'entre vous qui connaissent C, C++ ou bien encore Javascript, si retrouverons assez facilement (les syntaxes restantes très proche les unes des autres). Pour les autres, il est temps d'apprendre à utiliser ces instructions.

Syntaxiquement parlant, toute instruction se finie par un point-virgule (histoire de bien séparer les choses), et ce même si l'instruction est en dernière position d'une construction. Notons tout de suite qu'une expression peut être une instruction (si elle est suivie d'un point-virgule). Il est donc tout a fait possible d'avoir comme instruction "a+b;" et heureusement, car n'oublions pas que l'affectation se fait grâce à des opérateurs. Il aurait été dommage de ne pas pouvoir écrire "a=3;" dans un coin d'un programme.

Les blocs d'instructions

il peut-être utile, selon les situations, de placer plusieurs instructions à un endroit précis. Or certaines constructions (instructions), n'accepte qu'une seule sous-instruction (comme, par exemple, pour le if). Pour palier à ce problème, une solution agréable à était utilisée (en fait, héritée de C) : on définit un bloc d'instruction comme étant une instruction.

Au point de vue de la syntaxe, un bloc d'instructions commence par une accolade ouvrante et se termine par une accolade fermante. Entre ces deux caractères, nous y trouvons les sous-instructions, qui sont séparées les unes des autres par des points-virgules. Attention : la dernière instruction d'un bloc doit se terminer, elle aussi, par un point-virgule. Le bloc peut contenir zéro (bloc vide), une (équivalent à une instruction unique) ou plusieurs sous-instructions.

Fichier "Essai.java"

public class Essai {
    public static void main(String args[]) {
        {   // Un nouveau bloc, lui même dans un bloc
 
            System.out.println("Un");
            System.out.println("Deux");
            System.out.println("Trois");
        }
 
        System.out.println("Exit");
    }
}

L'instruction conditionnelle if

Syntaxiquement parlant, l'instruction conditionnelle s'introduit avec le mot clé if (attention, majuscules et minuscules sont différentes en Java : IF ou If ou bien encore iF sont des identificateurs différents). Il faut faire suivre cet identificateur d'un test (une expression booléenne) parenthésée, puis d'une instruction. Cette dernière sera exécutée uniquement si la valeur du test est vraie (true). Si ce n'est pas le cas, il est possible de rajouter le mot clé else suivi d'une instruction qui sera alors lancée. Le tableau suivant vous donne la syntaxe complète : notez que ce qui est facultatif est noté entre crochets.

Fichier "Essai.java"

public class Essai {
    public static void main(String args[]) {
        // On récupère le nombres de paramètres
        // saisis sur la ligne de commande
        int l = args.length;
 
        if (l != 1) {
            System.out.println("Entrez un paramètre");
            System.exit(-1);
        }
        else { 
            System.out.println(args[0]);
        }
    }
}

Notez bien que la partie associée à un résultat faux, est facultative. Autre détail important, même si votre test se réduit en la valeur d'une variable déclarée comme booléenne, il est obligatoire de la parenthéser. Rien de supplémentaire est à ajouter sur cette instruction.

Les instructions liées aux boucles

Comme nous allons maintenant le voir, il existe quelques instructions qui nous permettent de mettre en place des boucles (ou des itérations, c'est la même chose) et d'en contrôler leurs exécutions. Trois styles de boucles sont utilisables, et deux instructions permettent de débrancher l'exécution à l'intérieur d'une boucle. Etudions cela de plus près

La boucle for

Cette deuxième instruction permet de définir ce que l'on appelle des boucles. Pour un peu mieux comprendre les choses, je vais tenter de vous expliquer ce qu'est une boucle en informatique. Certaines actions nécessite un traitement dit itératif (c'est à dire répétitif) : une même action doit être appliquée jusqu'à obtention d'une condition d'arrêt (on ne voudrais pas que le traitement soit de durée infinie). Pour imager le concept, imaginons une personne sur une chaîne de fabrication qui attends désespérément l'heure de rentrer chez elle. De manière analogue, un programme peut avoir besoin d'effectuer une itération sur un traitement, et comme le programme doit se terminer, il nous faut matérialiser un cas d'arrêt. L'instruction for permet de réaliser cela très simplement. D'autres instructions permettent aussi de créer des boucles, nous les verrons après.

Pour en revenir au for, son utilisation est vraiment très simple. Vous introduisez l'instruction par le mot clé for, puis vous devez fournir trois expressions (elles seront toutes les trois séparées les unes des autres par des points-virgules, et mises entre parenthèses) suivies d'une instruction qui constituera le traitement à répéter. Pour ce qui est des trois expressions, pas de panique : c'est vraiment simple. Elles servent à manipuler des variables qui vont être utiles pour la boucle. La première sert à initialiser ces variables. Pour ceux qui se demandent comment initialiser trois variables avec une expression, rappelez vous que l'on a la possibilité de construire une expression à partir de sous expressions grâce au séparateur d'expressions. La deuxième permet de déterminer la condition de rebouclage à partir de l'état des variables utilisées : si cette condition n'est pas réalisée, on stop l'itération. Enfin, la dernière sert à modifier l'état des variables après chaque itérations.

Fichier "Essai.java"

public class Essai {
    public static void main(String args[]) {
        // On veut réaliser 10 affichages
 
        for(int i=0;i<10;i++) System.out.println("i vaut : " + i);
    }
}

Remarquez que le dernier exemple sert à définir une boucle infinie : cela peut être utile dans certains cas très particuliers. Autre remarque devant être signalée, la deuxième expression est forcément de type booléenne, la valeur false assurant la sortie de la boucle. A titre d'exemple, voici un petit programme qui calcul la somme des dix premiers entiers positifs et l'affiche. Certes on aurait pu mettre que somme valait 1+2+3+4+5+6+7+8+8+9+10, mais imaginez vous l'expression que vous auriez du donner si j'avais demandé la somme des dix mille premiers entiers. Une boucle est alors la seule alternative sérieuse.

Fichier "TenCounter.java"

public class TenCounter {
    public static void main(String args[]){
        int somme=0;
 
        for(int indice=1;indice<=10;indice++) somme += indice;
        
        String str = "La somme des dix premiers entiers positifs est : ";
        System.out.println(str + somme);
    }
}

La boucle while

Cette instruction permet elle aussi de définir des boucles, mais la syntaxe diffère. Pour ceux qui se demandent pourquoi avoir plusieurs constructions possibles pour les boucles, je répondrais que selon les cas il y en a qui sont plus adaptées que les autres. La syntaxe du while est la suivante : l'instruction commence par le mot clé while suivie d'une expression booléenne parenthésée et d'une instruction. L'expression sert à déterminer s'il faut encore effectuer une itération, ou bien passer à l'instruction suivant la boucle. L'instruction de la boucle, elle est exécutée à chaque étape de la boucle. Pour mieux voir les choses, reprenons l'exemple précédent (la somme des dix premiers entiers positifs), et réécrivons le avec le while. En voici le code.

Fichier "TenCounter.java"

public class TenCounter {
    public static void main(String args[]){
        int somme=0, indice=1;
 
        while(indice<=10) somme += indice++;
 
        String str = "La somme des dix premiers entiers positifs est : ";
        System.out.println(str + somme);
    }
}

Dans tout les cas, on retrouve l'initialisation de la variable indice, mais elle est située en dehors de la boucle, on retrouve aussi l'incrémentation, mais celle-ci est maintenant placée dans la partie des choses à faire à chaque tour de boucle. Le résultat lui reste strictement le même.

La boucle do while

Cette instruction a un fonctionnement quasi identique à la précédente. La différence essentielle réside dans le fait que l'on commence par calculer l'expression correspondante au test ou bien que l'on commence à exécuter l'instruction. Au point de vue de la syntaxe, on introduit l'instruction par le mot clé do que l'on fait suivre d'une instruction (le corps de la boucle, qui sera la première chose qui sera lancée), puis du mot clé while, lui même suivit d'une expression parenthésée. Cette dernière constitue la condition de rebouclage (la valeur false assurant la fin de la boucle). A titre d'exemple voici encore un programme calculant la somme des dix premiers entiers positifs.

Fichier "TenCounter.java"

public class TenCounter {
    public static void main(String args[]){
        int somme=0, indice=1;
 
        do 
            somme+=indice++;
        while(indice<=10);
 
        String str = "La somme des dix premiers entiers positifs est : ";
        System.out.println(str + somme);
    }
}

Nous en avons maintenant finit des instructions de boucles. Cependant, deux autres instructions sont tout de même très pratiques pour contrôler ce qui se passe durant l'exécution d'une boucle. Il s'agit de continue et de break dont une petite description suit.

L'instruction continue

L'instruction continue permet d'arrêter le traitement pour l'itération en cours, et d'en recommencer immédiatement une nouvelle. Passons tout de suite à un exemple, histoire de mieux comprendre les choses. Supposons que l'on ait besoin de calculer la somme des dix premiers entiers positifs, excepté pour l'entier 5. Un code Java possible est alors ...

Fichier "TenCounterMinusFive.java"

public class TenCounterMinusFive {
    public static void main(String args[]){
        int somme=0, indice=1;
 
        while(indice<11) {
             if (indice==5) { indice++;   continue; }
             else somme += indice++;
        }
 
        String str = "La somme des dix premiers entiers positifs (privé de 5) est : ";
        System.out.println(str + somme);
    }
}

Si vous exécutez ce code, le résultat affiché vaudra 50 (soit 55 moins la valeur du cinquième entier, soit 5). Il faut bien comprendre deux choses. Premièrement, quoi que l'on puisse mettre après le continue, ce code ne sera pas exécuté. Deuxièmement, un continue doit forcément être placé dans une branche d'un test conditionnel, sans quoi ça n'a pas de sens. Pour mieux comprendre ce dernier point, méditez sur le bout de programme suivant.

while(a<1000) {
    a++;
    continue;
   
    // Tout le code qui suit ne sera JAMAIS exécuté.
    // Alors pourquoi l'avoir mit ?? Il est donc clair que ce
    // programme ne compilera pas.
    b = a+2;
}

On peut encore faire plus compliqué avec cette instruction. Elle peut permettre de reprendre l'exécution en un point précis du programme. De tels points d'un programme sont nommés des labels (ou étiquettes). Au point de vue de la syntaxe, on définit un label par un identificateur immédiatement suivit d'un deux-points (par exemple, labelName:). Le programme suivant à pour but de mieux vous faire comprendre ce qui se passe (un exemple d'exécution y est joint).

Fichier "Continue.java"

public class Continue {
    public static void main(String args[]){
        int a, b=0, s=0;
 
        while(b < 5) {
            a=0;   b++;
            while(a < 5) {
                a++;
                if (a == 3) continue;
                else s++;
            }
        }
        System.out.println("Somme calculée avec \"continue;\" : " + s);
 
        b = s = 0;
label1: while(b < 5) {
            a=0;   b++;
            while(a < 5) {
                a++;
               if (a==3) continue label1;
               else s++;
            }
        }
        System.out.println("Somme calculée avec \"continue label1:\" : " +s);
    } 
}

Résultats

> javac Continue.java
> java Continue
Somme caculée avec "continue;" : 20
Somme caculée avec "continue label1;" : 10
> 

Le résultat obtenu était tout à fait prévisible. En effet, dans le premier cas, on esquive seulement cinq incrémentations de la variable s. Dans le second bloc de boucles, les choses se compliquent, car dés lors que a vaut 3, on esquive toutes les incrémentations suivantes pour l'itération sur b en cours. Du coup, le résultat en est sévèrement altéré. Il existe une autre instruction qui permet d'effectuer un débranchement au sein des boucles en voici une brève présentation.

L'instruction break

Cette instruction permet donc aussi de faire un débranchement au sein d'une boucle, mais contrairement à l'instruction précédente, l'exécution se poursuit non pas par une nouvelle itération, mais par l'instruction suivant la boucle. Elle sert donc purement et simplement à stopper la boucle de façon définitive. Le programme suivant illustre mes propos.

Fichier "Break.java"

public class Break {
    public static void main(String args[]){
        int somme=0, indice=1;
 
        while(indice<=10) {
            if (indice==5) break;
            somme += indice++;
        }
 
        System.out.println("Le résultat vaut : "+somme);
    }
}

Le résultat vaut bien évidement 10. Dernière remarque au sujet de cette instruction : on peut aussi interrompre une boucle et reprendre au label spécifié (de même que l'instruction continue).

 

L'instruction switch

Pour conclure ce chapitre, nous allons voir comment utiliser l'instruction switch. Celle-ci permet de choisir une séries d'instructions à exécuter parmi différentes séries. La sélection s'effectue grâce à la valeur d'une variable. Cette variable doit au préalable avoir été déclaré d'un type énumérable (byte, short, int, long). Notez, toute fois, qu'une valeur de type boolean, bien que répondant au critère demandé, n'est pas autorisée. Le choix de la sélection à exécuter se fait en associant des valeurs aux différentes sections de code potentiellement exécutables. Dès lors que l'instruction sera exécutée, la variable considérée aura alors une certaine valeur, et la sélection pourra s'opérer.

Au niveau de la syntaxe, on introduit une telle instruction de la manière suivante : tout d'abord, le mot clé switch doit figurer, immédiatement suivit de la variable (d'un type entier) placée entre parenthèses. Ensuite, on doit définir les sélections qui doivent être placées entre accolades. Chacune des sélections se définie ainsi : on place le mot clé case suivit d'une valeur (comprise dans les bornes définies par le type utilisé), du caractère : puis d'une série d'instructions à exécuter. On remarquera que la sélection accepte un nombre quelconque d'instructions, par opposition aux autres instructions déjà étudiées.

Une remarque importante est à signaler : il est conseillé que l'instruction break apparaisse en fin de définition de la sélection. Ce n'est en aucun cas une obligation, mais si ce n'est pas le cas, et que la sélection considérée est traitée, le code de la suivante le sera lui aussi, et ce jusqu'à qu'un break soit rencontré. Cette propriété peut être utilisée, pourvu que vous en soyez bien conscient. Si une sélection du switch est lancée son exécution ne s'arrêtera qu'au prochain break, ou, si l'on en rencontre pas, à la fin de l'instruction switch.

L'exemple suivante vous ferra, sans aucun doute, mieux comprendre les choses. Je vous invite à le tester et à le modifier de façon à ce que vous compreniez bien toute la sémantique de cette instruction.

Fichier "Switch.java"

public class Switch {
    public static void main(String args[]){
        for(int i=0;i<10;i++)
            switch(i){
               case 9: System.out.println("Neuf"); break;
               case 8: System.out.print("Huit"); System.out.println(""); break;
               case 7: { System.out.print("Sept"); System.out.println(""); } break;
               case 6: System.out.print("Six ");
               case 5: System.out.println("Cinq"); break;
               case 4:
               case 3: System.out.print("Quatre Trois ");
               case 2: System.out.println("Deux"); break;
               case 1: System.out.println("Un"); break;
               default: System.out.println("Zero");
            }
    }
}

Résultats

> javac Switch.java
> java Switch
Zero
Un
Deux
Quatre Trois Deux
Quatre Trois Deux
Cinq
Six Cinq
Sept
Huit
Neuf
> 

Nous en avons donc fini avec ce chapitre et avec les structures de contrôles (les instructions) de base du langage. Nous allons, dans le chapitre suivant, étudier l'ensemble de notions liées à la programmation orientée objet en Java.

 

 


Le modèle objet de Java

Nous avons, dans le chapitre précédent, étudié l'ensemble des instructions de base du langage. Si vous avez bien regardé tous les exemples, vous avez pu remarquer qu'à chaque fois, le code était placé dans une structure introduite par le mot clé class. Il s'agissait d'une définition de classe. Celle-ci se place dans le cadre de la programmation orientée objet. En quelques mots, on peut dire qu'il s'agit d'une méthodologie permettant de décrire un problème de façon la plus proche possible de la spécification de ce dernier. En d'autres termes, l'approche programmation orientée objet confère au langage une expressivité accrue. Cette approche est aussi très payante en terme de génie logiciel.

Un peu de vocabulaire

Nous allons, dans les paragraphes qui suivent, tenter de définir quelques notions de vocabulaire liées à la programmation orientée objet. Ceux d'entre vous déjà expert en la matière peuvent, s'ils le désirent, passer directement à la section suivante.

L'unité de base d'un programme écrit dans un langage dit orienté objet, est l'objet. Cette unité de code contient notamment des données (souvent appelées attributs), et une série d'actions (traditionnellement nommée méthodes) effectuables sur ces données. On regroupe souvent les termes d'attributs et de méthodes sous celui de champs de l'objet.

Il faut bien comprendre que chaque attribut, ce à un moment donné, a une valeur (d'un certain type). L'ensemble des valeurs des attributs de l'objet détermine l'état courant de l'objet. Cet état ne doit changer que si une méthode de l'objet est invoquée. Ce n'est pas une obligation, mais cela est fortement conseillé (nous y reviendrons lorsque nous parlerons de la programmation orientée composant).

En Java, on ne définit pas directement l'objet, on en définit la nature. Le concept permettant cela est celui de classe. Pour mieux comprendre les choses, revenons un peu en arrière et reprenons la notion de type. Un type définit donc la nature d'une variable qui pourra se voir affectée de n'importe quelle valeur de ce type. Parallèlement, une classe définie une ensemble de valeurs (d'objets) utilisables. On pourra par conséquent définir une variable qui aura comme type une classe. Un objet se créer donc à partir d'une définition de classe. On dit que l'objet est instance de sa classe. L'exemple suivant vous montre comment instancier un objet à partir d'une classe (ici nommé Circle), nous y reviendrons.

Circle monCercle=new Circle();

Il est clair que pour deux objets instances d'une même classe, la définition des méthodes reste la même (elles sont "en quelque sorte" des constantes de la classe). Par contre, les attributs, eux peuvent changer. La valeur des attributs définit donc, à un instant donné, l'état courant de l'objet.

 

Définition d'une classe

Comme nous l'avons donc déjà dit, on introduit une définition de classe par le mot clé class. Celui ci doit être suivit du nom de la classe puis d'une description de ses champs (attributs et méthodes) mise entre parenthèses. De l'information supplémentaire peut être fournie, nous y reviendrons dans les sections suivantes.

Fichier "Circle.java"

class Circle {
    // définition des champs
    // ...
}

Les attributs de classe

Dans les chapitres précédents, nous avons déjà vu comment définir des variables à l'intérieur d'une méthode (rappelez vous les exemples avec les méthodes main). Pour définir un attribut de classe, on réalise la même démarche. En effet, un attribut de classe peut quand même être vu comme une variable, au même titre que les variables. La différence essentielle réside dans le fait que le domaine de visibilité est plus grand dans le cas des attributs de classe. Par domaine de visibilité, j'entends l'étendue des parties du code du programme sur lesquelles, l'attribut peut être accédé (ne serait-ce déjà que toutes les méthodes de la classe). Une variable, elle, ne peut être utilisée uniquement qu'au niveau du bloc d'instructions dans lequel elle est définie (éventuellement une méthode). La classe suivant donne la description d'une classe Circle associée à la notion de cercle : elle devra donc contenir un point pour le centre (de type Point2D) et un rayon (valeur flottante, double précision). La classe Point2D, elle, contiendra deux valeurs (flottantes, double précision) permettant de définir les coordonnées d'un point dans le plan.

Fichier "Circle.java"

class Circle {
    Point2D centre;
    double rayon;
}

Fichier "Point2D.java"

class Point2D {
    double x,y;
}

Les méthodes

Maintenant, nous désirons définir des comportement liés à cette classe graphique qu'est le cercle. Nous devons donc définir des méthodes à cet effet. Supposons que dans le cadre du programme que nous sommes en train d'écrire nous ayons besoin de déplacer un cercle selon un vecteur donné : il nous faut donc écrire une méthode qui prend un vecteur en paramètre. Nous devons donc, au passage, définir une classe Vector2D (cette classe possèdera deux attributs de type flottant double précision). Une petite remarque, au stade actuel de l'écriture de ce petit programme, on pourrait penser qu'il soit judicieux de ne pas créer la classe Vector2D et d'utiliser à la place la classe Point2D contenant la même information. Ceci est une mauvaise idée, car il est clair que la sémantique d'un point du plan n'est pas la même que celle d'un vecteur. Mathématiquement, ils représentent deux concepts différents avec lesquels ont fait des choses différentes. Il est donc très fortement conseillé des séparer les choses quand ces dernière ne représentent pas le même concept. Si vous faite ainsi, il sera très facile de rajouter un comportement (une méthode) si vous l'avez oublié (autrement, cela ne serait peut-être pas si simple). Nous obtenons donc les classes suivantes :

Fichier "Circle.java"

class Circle {
    Point2D centre;
    double rayon;
 
    void Move(Vector2D vecteur) {
        centre.x += vecteur.x;
        centre.y += vecteur.y;
    }
}

Fichier "Point2D.java"

class Point2D {
    double x,y;
}

Fichier "Vector2D.java"

class Vector2D {
    double x,y;
}

Vous aurez sans doute remarqué l'utilisation de mot clé void. Celui-ci sert à définir un type de retour nul (sans valeur) pour la méthode. Si une méthode doit retourner une valeur (non nulle) alors il faut spécifier son type à cet endroit précis de la définition de la méthode.

La surcharge de méthodes

Si maintenant, vous voulez définir un déplacement de cercle, non pas en spécifiant un vecteur, mais bien une valeur en x et une seconde en y, il vous faut écrire une autre méthode. L'exemple suivant vous montre comment faire.

Fichier "Circle.java"

class Circle {
    Point2D centre;
    double rayon;
 
    void Move(Vector2D vecteur) {
        centre.x += vecteur.x;
        centre.y += vecteur.y;
    }
  
    void Move(double x,double y) {
        centre.x += x;
        centre.y += y;
    }
}

Vous aurez certainement remarqué que l'on a utilisé le même nom pour les deux méthodes : cette possibilité s'appelle la surcharge. Cela fonctionne car lorsque vous utiliserez une des deux méthodes, vous spécifierez soit un argument de type Point2D, soit deux arguments de type double. Plus formellement deux méthodes ne doivent pas avoir le même prototype (nous entendons par là le type de paramètres et celui de retour de la méthode). Donc, si nous voulons définir deux méthodes de déplacement, une horizontalement et une verticalement, nous ne pouvons pas les nommer de la même manière, sans quoi on ne pourrait savoir laquelle choisir lors d'un appel de méthode. Voici donc un programme possible, agrémenté d'une méthode d'affichage texte.

Fichier "Circle.java"

class Circle {
    Point2D centre;
    double rayon;
 
    void Move(Vector2D vecteur) {
        centre.x += vecteur.x;
        centre.y += vecteur.y;
    }
 
    void Move(double x,double y) {
        centre.x += x;
        centre.y += y;
    }
  
    void MoveH(double x) { centre.x += x; }
    void MoveV(double y) { centre.y += y; }
 
    void println(){
        System.out.print("Objet Circle :\n\tcentre : ");
        centre.print();
        System.out.println("\n\trayon : " + rayon);
    }
}

Fichier "Point2D.java"

class Point2D {
    double x,y;
 
    void print(){
        System.out.print("[" + x + ", " + y + "]");
    }
}

N.B. : les méthodes System.out.print et System.out.println affichent toutes les deux un message passé en paramètre, la différence résidant dans le fait que la seconde rajoute en plus un retour à la ligne suivante. Second détail, a priori, il aurait été plus judicieux de fournir à la place des fonctions d'affichages, des fonctions retournant la description textuelle de l'objet (si possible nommée toString). Mais cela aurait soulevé des problèmes qui seront traités un peu plus tard.

Instanciation d'objet

Nous avons, jusqu'à maintenant, vu comment définir une classe d'objets. Nous allons, à présent, voir comment définir des objets à partir d'une classe. Cette création d'objet, à partir d'une classe, est appelé instanciation. On instancie donc un objet en appliquant l'opérateur new sur un constructeur de classe. Précisons un peu les choses.

L'allocation de mémoire

Pour qu'un objet puisse réellement exister au sein de la machine, il faut qu'il puisse stocker son état dans une zone de la mémoire. Or deux objets définis à partir de deux classes différentes n'ont, à priori, pas forcément besoin de la même taille d'espace mémoire (car n'ayant pas les même définitions d'attributs). L'opérateur new est donc là dans le but de nous simplifier la vie. Par l'intermédiaire d'une méthode un peu particulière (un constructeur, que nous allons étudier dans la section suivante) d'une classe données, cet opérateur déterminera sans problème la taille de l'espace mémoire requit.

Les constructeurs

De manière basique, on peut dire qu'un constructeur est une méthode, d'une classe donnée, servant à créer des objets. Remarque importante à retenir : un constructeur n'a pas de type de retour, contrairement aux méthodes, et se nomme toujours de la même manière que sa classe. De même que les méthodes acceptent la surcharge, les constructeurs l'admettent aussi. L'exemple suivant propose donc quelques constructeurs pour nos classes déjà étudiées.

fichier "Circle.java"

 
class Circle {
    Point2D centre;
    double rayon;
 
    Circle(){ centre=new Point2D();   rayon=1; }
    Circle(Point2D c,double r){ centre=c; rayon=r; }
 
    void Move(Vector2D vecteur) {
        centre.x += vecteur.x;   centre.y += vecteur.y;
    }
 
    void Move(double x,double y) {
        centre.x += x;   centre.y += y;
    }
 
    void MoveH(double x) { centre.x += x; }
 
    void MoveV(double y) { centre.y += y; }
 
    void println(){
        System.out.print("Objet Circle :\n\tcentre : ");
        centre.print();
        System.out.println("\n\trayon : " + rayon);
    }
}

fichier "Point2D.java"

 
class Point2D {
    double x,y;
 
    Point2D(){ x=y=0; }
    Point2D(double i, double j){ x=i;   y=j; }
 
    void print(){ System.out.print("[" + x + ", " + y + "]"); }
}

fichier "Vector2D.java"

 
class Vector2D {
    double x,y;
 
    Vector2D(){ x=y=0; }
    Vector2D(double i, double j){ x=i;   y=j; }
}

On remarque deux choses en regardant de programme. Un constructeur peu créer des objets qu'il utilise. Second point, on s'aperçoit que les constructeurs servent principalement à définir l'état initial des objets instanciés. Sans un tel mécanisme il serait difficile de connaître l'état initial des objets et il serait donc quasi impossible de déterminer l'évolution du programme.

Petit exercice : déterminez l'affichage résultant du programme suivant (solution).

fichier "Start.java"

 
class Start {
    public static void main(String args[]){
        Circle c1=new Circle();
        Circle c2=new Circle(new Point2D(5,4),3);
 
        c1.println();   c2.println();
    } 
}

Quelques règles sur les constructeurs :

Si aucun constructeur n'est spécifié, dans la définition de la classe, un constructeur par défaut vous est obligatoirement fourni, celui-ci n'admettant aucun paramètre.

Si vous en définissez au moins un, le constructeur par défaut (qui n'admet pas de paramètres) n'est plus fourni. Si vous en avez utilité il vous faudra alors le définir explicitement.

Les destructeurs

Nous venons donc de voir que des constructeurs pouvaient être fournis pour permettre la création d'objets. Parallèlement, un destructeur (et un seul) peut être défini, pour être utilisé lors de la destruction de l'objet. Celui-ci doit forcément se nommer finalize, il ne prend aucun paramètre et ne renvoie aucun type (void). Cette méthode doit de plus être qualifiée de public, sans quoi le compilateur vous rappellera à l'ordre. Voici un petit exemple :

 
class Point2D {
    //...
 
    public void finalize() { 
        System.out.println("Objet Point2D détruit");
    }
    //...
}

Je sens arriver la question fatidique ... Mais comment un objet est-il détruit ? Le ramasse miettes (garbage collector pour les anglophones) est là pour ça. Etudions donc son principe de plus près.

Le ramasse miettes

Un programme Java a besoin de mémoire pour pouvoir s'exécuter (en règle général, plus il en a, mieux c'est). Comme on l'a déjà vu, l'opérateur new se charge d'allouer (de réquisitionner) de la mémoire à la demande. Une conséquence évidente est que si l'on ne libère pas la mémoire des objets devenus inutiles, on peut rapidement en manquer. Le ramasse miettes (ou GC [Garbage Collector]) se charge de repérer ces objets inutiles, et de libérer la mémoire qu'ils utilisent désormais inutilement. Il opère de façon totalement automatisé, et dans la quasi totalité des cas, vous n'avez pas à vous en soucier. N'oubliez pas que vous avez la possibilité de définir, par l'intermédiaire des destructeurs, des actions à effectuer en cas de destructions d'objet.

Pour les plus curieux, sachez qu'en fait le GC fonctionne en permanence dans un thread de faible priorité.

Méthodes et attributs statiques

En quelques mots, on peut résumer ce qui vient d'être vu ainsi : on définit des classes, à partir desquelles on peut instancier des objets qui eux sont manipulables. Or si l'on revient au premier exemple de ce cours, on s'apercevra que tout fonctionne sans qu'il y ait eut d'instanciation. La seule différence avec ce que l'on a déjà vu, réside dans la présence du mot clé static. Nous allons donc étudier quelques utilisations possibles de ce mot clé.

main : point d'entrée du programme

Comme nous l'avons déjà dit, on lance l'exécution d'un programme Java en démarrant une machine virtuelle Java (la JVM) avec, en paramètre, le nom de la classe de démarrage, laquelle doit forcément; contenir une méthode main. Une fois que la JVM c'est mise en place, elle lance le programme proprement dit, par la première instruction de la méthode main, et ce, sans instancier d'objet à partir de la classe de démarrage. Cela fonctionne, car la méthode est déclarée static : c'est à dire qu'elle existe sans qu'il y ait eut instanciation. La tâche principale de cette méthode est alors d'instancier des objets sur différentes classes afin que le programme puisse travailler.

Une chose importante doit bien être comprise : comme la méthode main existe indépendamment de toute classe, si elle doit utiliser des attributs ou des méthode de la classe, il faut alors que ces champs soient eux aussi déclarés static, sans quoi, ils n'existent pas. Plus formellement, les méthodes déclarées statiques, sur une classe, ne peuvent en manipuler que des champs statiques.

Notons au passage que la méthode main admet en paramètre un tableau de chaînes de caractères ("String args[]"). Celui-ci contient les éventuelles options spécifiées sur la ligne de commande, lors du lancement de la JVM. Pour connaître la taille du tableau, il suffit bien entendu de faire "args.length". A titre d'information, le nom du paramètre peut-être n'importe quel nom, mais il est de bon ton d'utiliser args.

fichier "Start.java"

 
class Start {
    static int a = 3;
 
    static public void main(String args[]){
        a += 5;
        System.out.println("a^2 = " + Square(a));
    }
 
    static int Square(int value){
        return value*value;
    }
}

Partage d'informations

Une conséquence logique d'une définition statique est la suivante : les champs statique d'une classe sont partagés par toutes les instances de cette classe: En effet, comme tout champs statique existe indépendamment de toute instanciation d'objet, il existe aussi après une quelconque instanciation. L'exemple suivant reprend ce qui vient d'être dit : attention tout de même, car il peut dérouter quelques novices en Java. Des explications suivront.

fichier "Start.java"

 
class Start {
    static int a = 3;
 
    static public void main(String args[]){
        Start s1=new Start(), s2=new Start();
        s1.a++;   s2.a++;
        System.out.println("a = " + Start.a);
    }
}
 
>java Start
5
> 
      

Pour ceux qui n'auraient pas suivit, reprenons les choses calmement. Le programme se lance en exécutant la méthode main statique. Celle-ci instancie deux objet à partir de la classe Start elle-même (ça marche parfaitement). On pourrait, dans le main, si on le voulait, faire "s1.main()" qui instancirait encore deux objets, mais on aurait très rapidement un souci de mémoire (qu'en pensez vous ?). Ensuite, sur chacun des deux objets, on incrémente la variable a. Mais comme elle est partagée, on incrémente finalement la variable de deux unités. Finalement, on affiche le résultat : 5. Notez qu'on accède ici à la variable en utilisant le nom de la classe. Ceci est permit car cela permet d'accéder des champs statiques d'un classe à partir d'une autre.

N'ayant rien de plus à ajouter sur les champs statiques, je vous propose donc, dans la section suivante, d'étudier un des concepts clés de la programmation orientée objet : l'héritage.

L'héritage

Afin de vous amener simplement, mais clairement, à l'assimilation de ce concept, nous allons tout d'abord considérer une extension du programme manipulant nos fameux cercles sur un espace à deux dimension. Il en résultera une simplification phénoménale du problème.

Ce qu'il ne faut surtout pas faire

L'extension va consister en l'introduction de classes liées aux autres notions de figures dans le plan. En effet, nous aimerions, maintenant, pouvoir manipuler, outre des cercles, des carrés, des rectangles, des triangles, ou toutes autres figures géométriques de votre choix. L'idée la plus basique se résume en la définition d'une classe par type de figures. Voici donc un autre exemple de classe.

fichier "Square.java"

 
class Square {
    Point2D centre;
    double longueur;
 
    Square(){ centre=new Point2D();   longueur=1; }
    Square(Point2D c,double l){ centre=c; longueur=l; }
 
    void Move(Vector2D vecteur) {
      centre.x += vecteur.x;   centre.y += vecteur.y;
    }
 
    void Move(double x,double y) {
        centre.x += x;   centre.y += y;
    }
 
    void MoveH(double x) { centre.x += x; }
    void MoveV(double y) { centre.y += y; }
 
    void println(){
        System.out.print("Objet Square :\n\tcentre : ");
        centre.print();
        System.out.println("\n\tlongueur : " + longueur);
    }
}

Jusque là, rien de bien nouveau, et l'ensemble de classes fonctionne correctement. Malgré cela, ce n'est pas franchement ce que l'on peut faire de mieux. En effet, si l'on regarde le code de la classe Circle et celui de la classe Square, c'est la même chose, aux noms des variables près. Si l'on avait un moyen de regrouper les parties de code identiques (on dit factoriser le code), cela serait bien mieux. C'est, en outre, ce que propose le concept d'héritage.

Ce qu'il faut faire

L'idée principale consiste à définir une classe à partir d'une autre. La classe définie à partie d'une autre sera nommée classe fille. Celle qui sert à définir un classe fille sera nommée classe mère. On dit alors que la classe fille hérite (ou dérive) de la classe mère. Une remarque importante peut déjà être faites : une classe fille dérive d'une unique classe mère, l'héritage multiple n'étant pas supporté par le langage Java (nous verrons par la suite un moyen de simuler l'héritage multiple, ce avec le concept d'interface). Une fois que l'héritage est spécifié, la classe fille possède aussi l'ensemble des attributs et des méthodes de sa classe mère.

La principale difficulté, avec l'héritage, est de définir ce qui est propre à la classe mère et ce qui l'est pour sa classe héritière. Dans tous les cas, cela est fortement lié au problème considéré. Revenons donc à nos classes Circle et Square. On remarque alors que dans les deux cas, on a besoin de connaître la position du centre de la figure. De même, on définit dans les deux cas, les mêmes méthodes, liées au déplacement de la figure. En réfléchissant encore un peu, on peut alors pressentir qu'il en sera de même pour toutes les classes associées à la notion de figures géométriques. Il pourrait donc être judicieux de définir une classe Shape de laquelle toutes les classes, associées à une figure géométrique, pourraient hériter.

Au niveau de la syntaxe à utiliser pour définir un lien d'héritage, c'est très simple. Il suffit d'ajouter le mot clé extends suivit du nom de la classe mère, de suite après le nom de la classe fille, ce dans la définition de cette dernière.

class Circle extends Shape { ... }

Un point important et souvent source d'erreur est à éclaircir. On n'hérite en aucun cas des constructeurs. Si vous ne spécifiez pas explicitement un constructeur particulier, vous ne pourrez l'utiliser, ce même s'il en existe un défini dans la classe mère. Par contre, des règles existent sur l'utilisation des constructeurs de la classe mère dans les constructeurs d'une classe fille quelconque.

Avant de voir ces règles en détail, quelques précisions sont à apporter sur deux mots clés du langage : super et this. Le premier sert à accéder les définitions de classe au niveau de la classe parente de la classe considérée (ces définitions pouvant être soit des méthodes ou des constructeurs). Le second sert à accéder à la classe courante. L'exemple suivante devrait mieux vous faire comprendre les choses. Les règles suivent directement l'exemple.

fichier "Classe1.java"

fichier "Classe2.java"

 
class Classe1 {
    Classe1(){
        System.out.println("Classe1");
    }
 
    Classe1(int val){
        this();
        System.out.println(val);
    }
}
 
class Classe2 extends Classe1 {
    Classe2(){
        super(5);
        System.out.println("Classe2");
    }
 
    Classe2(int val){
        System.out.println(val);
    }
}

exemple de création d'objet

résultats

new Classe1();

Classe1

new Classe1(3);

Classe1
3

new Classe2();

Classe1
5
Classe2

new Classe2(2);

Classe1
2

Règle 1 : si la première instruction d'un constructeur ne commence pas par le mot clé super le constructeur par défaut de la classe mère est appelé (faites attention à ce qu'il soit définit).

Règle 2 : un appel de constructeur de la classe mère peut uniquement se faire qu'en première instruction d'une définition de constructeur. Une conséquence évidente est qu'on ne peut utiliser qu'un seul appel au constructeur de la classe mère.

Maintenant que ces quelques points vous ont été présenté, nous pouvons revenir à notre exemple initial, celui des figures géométriques. Notez bien que ce petit exemple reprend, en grande partie, tout ce qui vous a déjà été dévoilé dans ce chapitre.

fichier "Shape.java"

 
class Shape {
    Point2D centre;
 
    Shape(){ centre=new Point2D(); }
    Shape(Point2D c){ centre=c; }
 
    void Move(Vector2D vecteur) {
        centre.x += vecteur.x;   centre.y += vecteur.y;
    }
 
    void Move(double x, double y) {
      centre.x += x;   centre.y += y;
    }
 
    void MoveH(double x) { centre.x += x; }
    void MoveV(double y) { centre.y += y; }
 
    void print(String nature){
        System.out.print("Objet "+nature" :\n\tcentre : ");  centre.print();
    }
 
    void println(){ print("Shape");   System.out.println(""); }
}

fichier "Square.java"

 
class Square extends Shape {
    double longueur;
 
    Square(){ longueur=1; }
    Square(Point2D c,double l){ super(c);   longueur=l; }
 
    void println(){
        super.print("Square");
        System.out.println("\n\tlongueur : " + longueur);
    }
}

fichier "Circle.java"

 
class Circle extends Shape {
  double rayon;
 
  Square(){ rayon=1; }
  Square(Point2D c,double r){ super(c);   rayon=r; }
 
  void println(){
    super.print("Circle");
    System.out.println("\n\trayon : "+rayon);
  }
}

J'espère que ce petit exemple vous aura convaincu de l'intérêt de l'héritage. Si ce n'est pas le cas, la section suivante est faîtes pour vous : elle tente de vous présenter, un par un, les atouts majeurs qu'apporte l'héritage.

L'intérêt

Le premier point important qu'il faut absolument bien assimiler, est que l'héritage supprime, en grande partie, les redondances dans le code. En effet, une fois la hiérarchie de classes bien établie, on localise en un point unique les sections de code (celles-ci restantes à tous moments accessibles grâce au mot clé super).

Seconde chose importante, et ce à condition que la hiérarchie de classes est été bien pensée, on peut très facilement rajouter, après coup, une classe, et ce à moindre coup, étant donné que l'on peut réutiliser le code des classes parentes.

Dernier point, si vous n'aviez pas encore modélisé un comportement dans une classe donnée, et que vous vouliez maintenant le rajouter, une fois l'opération terminée, ce comportement sera alors directement utilisable dans l'ensemble des sous-classes de celle considérée.

Le polymorphisme

Définition

Un langage orienté objet est dit polymorphique, s'il offre la possibilité de pouvoir percevoir un objet en tant qu'instance de classes variées, selon les besoins. Le langage Java est polymorphique.

Pour mieux comprendre, reconsidérons notre hiérarchie de classes associée aux figures géométriques. Au niveau des figures, il est bien clair qu'un carré est une figure géométrique. Au point de vue du programme Java, il en va de même : un objet instancié sur une classe donnée peut être utilisé en temps qu'instances de toutes les classes parentes de la classe considéré. On y arrive de deux façons - par le polymorphisme - par utilisation du casting (équivalent à la première manière). Le petit exemple suivant explicite mieux les choses.

fichier "A.java"

fichier "B.java"

 
class A {
  ...
}
 
class B extends A {
  ...
}
B b=new B();
 
A a=b;          // Utilisation du polymorphisme
(A) b;          // On utilise le casting

Une petite remarque qui sera utile pour plus tard : en Java, si vous ne spécifiez pas de lien d'héritage, la classe en cours de définition hérite alors de la class Object. Cette classe, fournie par la JVM (Java Virtual Machine), est la classe de plus haut niveau de laquelle toute les autres dérivent (directement ou indirectement). On peut donc, et ce pour tout objet Java, écrire "Object o=objetJava;". Nous verrons plus tard que la classe Object fournie un certain nombre de fonctionnalités qui sont donc applicables sur n'importe quel objet Java.

Mais attention : tout n'est pas aussi facile qu'il y parait. Pour vous en convaincre, essayez de répondre au petit exercice suivant (solution). Des explications seront fournies dans la section suivante.

fichier "A.java"

fichier "B.java"

 
class A {
  void m(){
    System.out.println("Mother");
  }
}
 
class B extends A {
  void m(){
    System.out.println("Son");
  }
}
public class Polym {
    public static void main(String args[]){
        B b=new B();
        A a=b;
 
        a.m();       // Utilisation du polymorphisme
    }
}

La liaison dynamique (dynamic binding)

Peut-être avez vous été fort étonné en visualisant la solution de l'exercice précédent. Pourtant, il en va ainsi : on appelle cela la liaison dynamique (en anglais "dynamic binding" ou "late binding" ou parfois même "run-time binding"). En fait, il n'y a aucune difficulté. Statiquement (lors de la compilation), l'objet peut être perçu comme étant d'une sur-classe de celle de sa création, mais perçu seulement. Lors de l'exécution, c'est la méthode la plus spécifique que est utilisée : celle de la classe de création si elle existe. L'exemple qui suit vous donne le code des classes de figures géométriques, avec un petit exemple d'utilisation.

fichier "Shape.java"

 
class Shape {
    Point2D centre;
 
    Shape(){ centre=new Point2D(); }
    Shape(Point2D c){ centre=c; }
 
    void Move(Vector2D vecteur) {
        centre.x += vecteur.x;   centre.y += vecteur.y;
    }
 
    void Move(double x,double y) {
        centre.x += x;   centre.y += y;
    }
 
    void MoveH(double x) { centre.x += x; }
    void MoveV(double y) { centre.y += y; }
 
    void print(String nature){
        System.out.print("Objet "+nature" :\n\tcentre : ");
        centre.print();
    }
 
    double area(){ return 0; }
 
    void println(){ print("Shape");   System.out.println(""); }
}

fichier "Square.java"

 
class Square extends Shape {
    double longueur;
 
    Square(){ longueur=1; }
    Square(Point2D c,double l){ super(c);   longueur=l; }
 
    double area(){ return longueur*longueur; }
 
    void println(){
        super.print("Square");
        System.out.println("\n\tlongueur : " + longueur);
    }
}

fichier "Circle.java"

 
class Circle extends Shape {
    double rayon;
 
    Circle(){ rayon=1; }
    Circle(Point2D c,double r){ super(c);   rayon=r; }
 
    double area(){ return 2*Math.PI*rayon*rayon; }
 
    void println(){
        super.print("Circle");
        System.out.println("\n\trayon : " + rayon);
    }
}

fichier "Start.java"

 
class Start {
    static double surfaceur(Shape s){ return s.area(); }
 
    static public void main(String args[]){
        Shape s1=new Shape();
        Square s2=new Square(new Point2D(),2);
 
        System.out.println(surfaceur(s1));
        System.out.println(surfaceur(s2));
    }
}

Résultat

Ce petit programme a encore un petit défaut! A priori, une figure, sans autre précision sur sa nature n'a pas de surface. Or une méthode double area() de la classe Shape est nécessaire pour pouvoir utiliser le polymorphisme. Le concept d'abstraction permet alors de résoudre ce genre de problème. Etudions donc ce nouveau concept.

Faire usage de l'abstraction

Il peut donc, dans certains cas, être utile de définir une méthode sans en donner le code. Les seules informations données sont donc le nom de la méthodes, les types des paramètres et le type de retour de cette dernière : on nomme cela le prototype de la méthode. En Java, on dit qu'on définit une méthode abstraite. C'est la même chose. Pour réaliser une telle définition, il suffit de mettre en tête de la définition du prototype le mot clé abstract. Dans ce cas, on ne définit plus le corps de la méthode et on supprime les accolades que l'on remplace pas un point-virgule. Voici un exemple de définition de méthode abstraite.

abstract double area();

Mais attention : on ne peut pas définir une méthode abstraite n'importe ou. Cela ne se peut que dans une définition de classe abstraite.

Définir une classe abstraite

Une classe abstraite est une classe qui peut contenir des méthodes abstraites. Une chose importante est a signaler d'hors et déjà : comme une classe abstraite, possédant des méthodes dont le code est inconnu, est incomplète, on ne peut en aucun cas instancier d'objet de cette classe. Il faut impérativement définir des classes filles, lesquelles fournirons les définitions manquantes (soit quoi elles seraient aussi abstraites). Ces classes filles peuvent, par contre, servir à instancier des objets. Dans le cadre de nos classes de figures, cela se traduit pas le fait que si la classe Shape est abstraite, on ne peut plus instancier d'objet de cette nature (plus de "new Shape()"). Le polymorphisme peut encore être utilisé ("Shape s=new Circle()").

Du point de vue de la syntaxe, on définit une classe abstraite en rajoutant devant le mot clé abstract. Le code de la classe Shape devient donc le suivant.

fichier "Shape.java"

 
abstract class Shape {
    Point2D centre;
 
    Shape(){ centre=new Point2D(); }
    Shape(Point2D c){ centre=c; }
 
    void Move(Vector2D vecteur) {
        centre.x += vecteur.x;   centre.y += vecteur.y;
    }
 
    void Move(double x,double y) {
        centre.x += x;   centre.y += y;
    }
 
    void MoveH(double x) { centre.x += x; }
    void MoveV(double y) { centre.y += y; }
 
    void print(String nature){
        System.out.print("Objet "+nature+" :\n\tcentre : ");
        centre.print();
    }
 
    abstract double area();
 
    void println(){ print("Shape");   System.out.println(""); }
}

Certains d'entre vous sont peut-être gênés par le fait que l'on ne puisse plus créer d'objet de classe Shape. Mais physiquement, il n'y a pas de figures, mais des cercles et des carrés, et d'autres formes, que l'on regroupe sous le terme générique de figures. Donc, notre modèle est conforme à la spécification (implicite) de notre problème.

Il existe une autre technique pour introduire de l'abstraction dans un programme, par le biais des interfaces. Voyons cela de plus près.

Définir une interface

En fait, ce mécanisme n'est qu'une généralisation du concept de classe abstraite. Plus précisément, une interface est une classe dont toutes les méthodes sont abstraites. On n'a donc plus besoin de spécifier que les méthodes sont abstraites (signalées par le mot clé abstract), car elle doivent forcément l'être.

Au niveau de la syntaxe, on introduit une interface non plus par le mot clé class mais par le mot interface (comme vous vous en doutiez très certainement). Petite subtilité au passage, on n'hérite pas d'une interface, mais on l'implémente. En d'autres termes, on doit forcément fournir le code de toutes les méthodes de l'interface utilisée (sauf dans le cas d'une classe abstraite qui implémente un interface, ou bien d'une i