1. Interprétation et compilation de java
2. Les éléments de base en Java
Les deux voies utilisées pour exécuter un programme évolué
sont la compilation ou l'interprétation :
Un compilateur du langage X pour un processeur P, est un logiciel qui traduit un programme source écrit en X en un programme cible écrit en instructions machines exécutables par le processeur P. |
Un interpréteur du langage X pour le processeur P, est un logiciel qui ne produit pas de programme cible mais qui effectue lui-même immédiatement les opérations spécifiées par le programme source. |
Un compromis assurant la portabilité d'un langage : une
pseudo-machine
Lorsque le processeur P n'est pas une machine qui existe physiquement mais un logiciel simulant (ou interprétant) une machine on appelle cette machine pseudo-machine ou p-machine. Le programme source est alors traduit par le compilateur en instructions de la pseudo-machine et se dénomme pseudo-code. La p-machine standard peut ainsi être implantée dans n'importe quel ordinateur physique à travers un logiciel qui simule son comportement; un tel logiciel est appelé interpréteur de la p-machine. |
La première p-machine d'un langage évolué a été
construite pour le langage pascal assurant ainsi une large diffusion
de ce langage et de sa version UCSD dans la mesure où le seul effort
d'implementation pour un ordinateur donné était d'écrire
l'interpréteur de p-machine pascal, le reste de l'environnement
de développement (éditeurs, compilateurs,...) étant
écrit en pascal était fourni et fonctionnait dès que
la p-machine était opérationnelle sur la plate-forme cible.
Donc dans le cas d'une p-machine le programme source est compilé, mais le programme cible est exécuté par l'interpréteur de la p-machine. |
Beaucoup de langages possèdent pour une plate-forme fixée
des interpréteurs ou des compilateurs, moins possèdent une
p-machine, Java est l'un de ces langages. Nous décrivons
ci-dessous le mode opératoire en Java.
1.1 Bytecode et Compilation native
Compilation native
La compilation native consiste en la traduction du source java (éventuellement préalablement traduit instantanément en code intermédiare) en langage binaire exécutable sur la plate-forme concernée. Ce genre de compilation est équivalent à n'importe quelle compilation d'un langage dépendant de la plate-forme, l'avantage est la rapidité d'exécution des instructions machines par le processeur central.
Programe source java : xxx.java (portable)
Programe exécutable sous windows : xxx.exe (non portable)
Bytecode
La compilation en bytecode (ou pseudo-code ou p-code ou code intermédiaire)
est semblable à l'idée du p-code de N.Wirth pour obtenir
un portage multi plate-formes du pascal. Le compilateur Javac traduit
le programme source xxx.java en un code intermédiaire indépendant
de toute machine physique et non exécutable directement, le fichier
obtenu se dénomme xxx.class. Seule une p-machine (dénommée
machine virtuelle java) est capable
d'exécuter ce bytecode. Le bytecode est aussi dénommé
instructions virtuelles java.
Figure : un programme source Exemple.java est traduit par le compilateur (dénommé Javac ) en un programme cible écrit en bytecode nommé Exemple.class
Exemple.java ------------> compilateur Javac ------> Exemple.class
Une fois le programme source java traduit en bytecode, la machine virtuelle java se charge de l'exécuter sur la machine physique à travers son système d'exploitation (Windows, Unix, MacOs,...)
Inutile d'acheter une machine virtuelle java, tous les navigateurs internet
modernes (en tout cas Internet explorer et Netscape) intègrent
dans leur environnement une machine virtuelle java qui est donc installée
sur votre machine physique et adaptée à votre système
d'exploitation, dès que votre navigateur internet est opérationnel.
Fonctionnement élémentaire de la machine virtuelle Java
Une machine virtuelle Java contient 6 parties principales
Comme toute machine la machine virtuelle Java est fondée sur
l'architecture de Von Neumann et elle exécute les instructions séquentiellement
un à une.
Figure : un synoptique de la machine virtuelle Java
Les registres sont des mémoires 32 bits :
L'interprétation et l'exécution du bytecode ligne par
ligne peut sembler prendre beaucoup de temps et faire paraître le langage
Java comme "plus lent" par rapport à d'autres langages. Aussi dans
un but d'optimisation de la vitesse d'exécution, des techniques palliatives
sont employées dans les version récentes des machines virtuelles
Java : la technique Just-in-time et la technique Hotspot
sont les principales améliorations en terme de vitesse d'exécution.
JIT (Just-in-time) est une technique de traduction dynamique durant l'interprétation que Sun utilise sous le vocable de compilation en temps réel. Il s'agit de rajouter à la machine virtuelle Java un compilateur optimiseur qui recompile localement le bytecode lors de son chargement et ensuite la machine virtuelle Java n'a plus qu'à faire exécuter des instructions machines de base. Cette technologie est disponible en interne sur les navigateurs de dernière génération. |
On peut mentalement considérer qu'avec cette technique vous obtenez
un programme java cible compilé en deux passages :
Hotspot est une amélioration de JIT.
Un défaut dans la vitesse totale d'exécution d'un
programme java sur une machine virtuelle Java équipée d'un
compilateur JIT se retrouve dans le fait qu'une méthode
qui n'est utilisée qu'une seule fois
se voit compilée puis ensuite exécutée,
les mesures de temps par rapport à sa seule interprétation
montre que dans cette éventualité l'interprétation
est plus rapide. La société Sun
a donc mis au point une technologie palliative dénommée Hotspot
qui a pour but de déterminer dynamiquement quel est le meilleur
choix entre l'interprétation ou la compilation d'une méthode.
Hotspot lancera la compilation des méthodes utilisées
plusieurs fois et l'interprétation de celles qui ne le sont qu'une
fois. |
2. Les éléments de base
en Java
Tout n'est pas objet dans Java, par souci de simplicité et d'efficacité,
Java est un langage fortement typé. Comme en Delphi, en Java vous
devez déclarer un objet ou une variable avec son type avant de l'utiliser.
Java dispose de même de type prédéfinis ou type élémentaires.
2.1 Tous les type élémentaires de Java et le transtypage
les types servent à déterminetr la nature du contenu d'une
variable, du résultat d'une opération, d'un retour de résultat
de fonction.
type élémentaire | intervalle de variation | nombre de bits |
boolean | false , true | 1 bit |
byte | -128 ... +127 | 8 bits |
char | caractères unicode (valeurs de 0 à 65536) | 16 bits |
double | Virgule flottante double précision ~5.10^308 | 64 bits |
float | Virgule flottante simple précision ~9.10^18 | 32 bits |
int | entier signé -2^31-1... +2^31 | 32 bits |
long | entier signé long -2^63-1... +2^63 | 64 bits |
short | entier signé court -2^15-1... +2^15 | 16 bits |
Rappellons qu'en java toute variable qui sert de conteneur à une valeur d'un type élémentaire précis doit préalablement avoir été déclarée sous ce type.
Il est possible d'indiquer au compilateur
le type d'une valeur numérique en utilisant un suffixe :
|
Exemples :
45l ou 45L représente la valeur 45 en entier signé sur
64 bits. 45f ou 45F représente la valeur 45 en virgule flottante simple précision sur 32 bits. 45d ou 45D représente la valeur 45 en virgule flottante double précision sur 64 bits. 5.27e-2f ou 5.27e-2F représente la valeur 0.0527 en virgule flottante simple précision sur 32 bits. |
Transtypage opérateur ( )
Les conversions de type en Java sont identiques pour les types numériques
aux conversions utilisées dans un langage fortement typé
comme Delphi par exemple (pas de conversion implicite). Si vous voulez
malgré tout, convertir une valeur immédiate ou une valeur
contenue dans une variable il faut explicitement transtyper cette valeur
à l'aide de l'opérateur de transtypage noté: ( ).
2.2 Variables, valeurs, constantes
en Java
Identificateurs de variables Déclarations et affectation de variables Les constantes en Java Base de représentation des entiers |
Comme en Delphi, une variable Java peut contenir soit une valeur d'un type élémentaire, soit une référence à un objet. Les variables jouent le même rôle que dans les langages de programmation classiques impératifs, leur visibilité est étudié dans le prochain chapitre.
Les identificateurs de variables en Java se décrivent comme ceux de tous les langages de programmation :
Attention Java fait une différence entre majuscules et minuscules, c'est à dire que la variable BonJour n'est pas la même que la variable bonjour ou encore la variable Bonjour. En plus des lettres, les caractères suivants sont autorisés pour construire une identificateur Java : "$" , "_" , "µ" et les lettres accentuées.
Exemples de déclaration de variables :
int Bonjour ; int µEnumération_fin$;
float Valeur ;
char UnCar ;
boolean Test ;
etc ...
Exemples d'affectation de valeurs à ces variables :
Affectation | Déclaration avec initialisation |
Bonjour = 2587 ; Valeur = -123.5687 UnCar = 'K' ; Test = false ; |
int Bonjour = 2587 ; float Valeur = -123.5687 char UnCar = 'K' ; boolean Test = false ; |
Exemple avec transtypage :
int Valeur ;
char car = '8' ;
Valeur = (int)car - (int)'0';
fonctionnement de l'exemple : |
Les constantes en Java ressemblent à celles du pascal
Ce sont des variables dont le contenu ne peut pas être modifié, elles sont précédées du mot clef final :
final int x=10 ; x est déclarée
comme constante entière initialisée à 10.
x = 32 ; <------ provoquera
une erreur de compilation interdisant la modification de la valeur de x.
Base de représentation des entiers
Java peut représenter les entiers dans 3 bases de numération
différentes : décimale (base 10), octale (base 8),
hexadécimale (base 16). La détermination de la base de représentation
d'une valeur est d'ordre syntaxique grâce à un préfixe
:
|
2.3 Priorité d'opérateurs en Java
Les 39 opérateurs de Java sont détaillés par famille, plus loin . Ils sont utilisés comme dans tous les langages impératifs pour manipuler, séparer, comparer ou stocker des valeurs. Les opérateurs ont soit une seul opérande, soit deux opérandes, il n'existe en Java qu'un seul opérateur à trois opérandes (comme en C) l'opérateur conditionnel " ? : ".
Dans le tableau ci-dessous les opérateurs de Java sont classés
par ordre de priorité croissante (0 est le plus haut niveau, 14
le plus bas niveau). Ceci sert lorsqu'une expression contient plusieurs
opérateurs à indiquer l'ordre dans lequel s'effectueront
les opérations.
priorité | opérateurs |
0 | ( ) [ ] . |
1 | ! ~ ++ -- |
2 | * / % |
3 | + - |
4 | << >> >>> |
5 | < <= > >= |
6 | = = != |
7 | & |
8 | ^ |
9 | | |
10 | && |
11 | || |
12 | ? : |
13 | = *= /= %= += -= ^= &= <<= >>= >>>= |= |
opérateurs travaillant avec des opérandes à valeur immédiate ou variable
Opérateur | priorité | action | exemples |
+ | 1 | signe positif | +a; +(a-b); +7 (unaire) |
- | 1 | signe négatif | -a; -(a-b); -7 (unaire) |
* | 2 | multiplication | 5*4; 12.7*(-8.31); 5*2.6 |
/ | 2 | division | 5 / 2; 5.0 / 2; 5.0 / 2.0 |
% | 2 | reste | 5 % 2; 5.0 %2; 5.0 % 2.0 |
+ | 3 | addition | a+b; -8.53 + 10; 2+3 |
- | 3 | soustraction | a-b; -8.53 - 10; 2-3 |
Ces opérateurs sont binaires (à deux opérandes) exceptés les opérateurs de signe positif ou négatif. Ils travaillent tous avec des opérandes de types entiers ou réels. Le résultat de l'opération est converti automatiquement en valeur du type des opérandes.
L'opérateur " % " de reste n'est intéressant que pour des calculs sur les entiers longs, courts, signés ou non signés : il renvoie le reste de la division euclidienne de 2 entiers.
Exemples d'utilisation de l'opérateur de division selon les
types des opérandes et du résultat :
programme Java | résultat obtenu | commentaire |
int x = 5 , y ; | x = 5 , y =??? | déclaration |
float a , b = 5 ; | b = 5 , a =??? | déclaration |
y = x / 2 ; | y = 2 // type int | int x et int 2 résultat : int |
y = b / 2 ; | erreur de conversion | conversion automatique
impossible (float b --> int y) |
y = b / 2.0 ; | erreur de conversion | conversion automatique
impossible (float b --> int y) |
a = b / 2 ; | a = 2.5 // type float | float b et int 2 résultat : float |
a = x / 2 ; | a = 2.0 // type float | int x et int 2 résultat : int conversion automatique int 2 --> float 2.0 |
a = x / 2f ; | a = 2.5 // type float | int x et float 2f résultat : float |
Pour l'instruction précédente " y = b / 2 " engendrant une erreur de conversion voici deux corrections possibles utilisant le transtypage :
y = (int)b / 2 ; //
b est converti en int avant la division qui s'effectue sur deux int.
y = (int)(b / 2) ; // c'est le résultat
de la division qui est converti en int.
opérateurs travaillant avec une unique variable comme opérande
Opérateur | priorité | action | exemples |
++ | 1 | post ou pré incrémentation : incrémente de 1 son opérande numérique : short, int, long, char, float, double. |
++a; a++; (unaire) |
-- | 1 | post ou pré décrémentation : décrémente de 1 son opérande numérique : short, int, long, char, float, double. |
--a; a--; (unaire) |
L'objectif de ces opérateurs est l'optimisation de la vitesse d'exécution du bytecode dans la machine virtuelle Java.
post-incrémentation : k++
la valeur de k est d'abord utilisée telle quelle dans l'instruction,
puis elle est augmentée de un à la fin. Etudiez bien les
exemples ci-après qui vont vous permettre de bien comprendre le fonctionnement
de cet opérateur.
Nous avons mis à côté de l'instruction Java les résultats des contenus des variables après exécution de l'instruction de déclaration et de la post incrémentation.
Exemple 1 :
int k = 5 , n ; n = k++ ; |
n = 5 | k = 6 |
Exemple 2 :
int k = 5 , n ; n = k++ - k ; |
n = -1 | k = 6 |
Exemple 3 :
int k = 5 , n ; n = k - k++ ; |
n = 0 | k = 6 |
Exemple 4 :Utilisation de l'opérateur de post-incrémentation
en combinaison avec un autre opérateur unaire.
int nbr1, z , t , u , v ;
nbr1 = 10 ; v = nbr1++ |
v = 10 | nbr1 = 11 |
nbr1 = 10 ; z = ~ nbr1 ; |
z = -11 | nbr1 = 10 |
nbr1 = 10 ; t = ~ nbr1 ++ ; |
t = -11 | nbr1 = 11 |
nbr1 = 10 ; u = ~ (nbr1 ++) ; |
u = -11 | nbr1 = 11 |
La notation " (~ nbr1) ++ " est refusée
par Java.
remarquons que les expressions "~nbr1 ++ " et "~
(nbr1 ++)" produisent les mêmes effets, ce qui est logique puisque
lorsque deux opérateurs (ici ~ et ++ )ont la même priorité,
l'évaluation a lieu de gauche à droite.
pré-incrémentation : ++k
la valeur de k est d'abord augmentée de un ensuite utilisée
dans l'instruction.
Exemple1 :
int k = 5 , n ;
n = ++k ; | n = 6 | k = 6 |
Exemple 2 :
int k = 5 , n ; n = ++k - k ; |
n = 0 | k = 6 |
Exemple 3 :
int k = 5 , n ; n = k - ++k ; |
n = -1 | k = 6 |
post-décrémentation : k--
la valeur de k est d'abord utilisée telle quelle dans l'instruction,
puis elle est diminuée de un à la fin.
Exemple1 :
int k = 5 , n ;
n = k-- ; | n = 5 | k = 4 |
pré-décrémentation : --k
la valeur de k est d'abord diminuée de un puis utilisée dans
l'instruction.
Exemple1 :
int k = 5 , n ;
n = --k ; | n = 4 | k = 4 |
Reprenez avec l'opérateur - - des exemples semblables à
ceux fournis pour l'opérateur ++ afin d'étudier le fonctionnement
de cet opérateur (étudiez (- -k - k) et (k - - -k)).
Ces opérateurs employés dans une expression renvoient un
résultat de type booléen (false ou true). Nous en donnons
la liste sans autre commentaire car ils sont strictement identiques à
tous les opérateurs classiques de comparaison de n'importe quel
langage algorithmique (C, pascal, etc...). Ce sont des opérateurs
à deux opérandes.
Opérateur | priorité | action | exemples |
< | 5 | strictement inférieur | 5 < 2 ; x+1 < 3 ; y-2 < x*4 |
<= | 5 | inférieur ou égal | -5 <= 2 ; x+1 <= 3 ; etc... |
> | 5 | strictement supérieur | 5 > 2 ; x+1 > 3 ; etc... |
>= | 5 | supérieur ou égal | 5 >= 2 ; etc... |
= = | 6 | égal | 5 = = 2 ; x+1 = = 3 ; etc... |
!= | 6 | différent | 5 != 2 ; x+1 != 3 ; etc... |
Ce sont les opérateurs classiques de l'algèbre de boole
{ { V, F }, ! , & , | } où { V,
F } représente l'ensemble {Vrai,Faux}.
Les connecteurs logiques ont pour syntaxe en Java : ! , & , | , ^
& : { V, F } x { V,
F } ®{ V,
F } (opérateur binaire qui
se lit " et ")
| : { V, F } x { V,
F } ®{ V,
F } (opérateur binaire qui
se lit " ou ")
! : { V, F } ¾®{ V, F } (opérateur unaire qui se lit " non ")
^
: { V, F } x { V, F } ®{ V, F }
(opérateur binaire qui se lit " ou exclusif
")
Table de vérité des opérateurs ( p et q étant des expressions booléennes)
p | q | ! p | p & q | p | q | p ^ q |
V | V | F | V | V | F |
V | F | F | F | V | V |
F | V | V | F | V | V |
F | F | V | F | F | F |
Remarque :
"p
Î { V, F } , "q Î { V,
F } , p &q est toujours évalué en entier (
p et q sont
toujours évalués). "p Î { V, F } , "q Î { V, F } , p |q est toujours évalué en entier ( p et q sont toujours évalués). |
Opérateur et optimisé : &&
Théorème "q Î { V, F } , F &q = F |
Donc si p est faux (p = F) , il est inutile d'évaluer q car l'expression p &q est fausse (p &q = F), comme l'opérateur & évalue toujours l'expression q, Java à des fins d'optimisation de la vitesse d'exécution du bytecode dans la machine virtuelle Java, propose un opérateur ou noté && qui a la même table de vérité que l'opérateur & mais qui applique ce théorème.
"p
Î { V, F } , "qÎ { V, F
} , p &&q = p &q Mais dans p&&q , q n'est évalué que si p = V. |
Opérateur ou optimisé : | |
Théorème "q Î { V, F } , V |q = V |
Donc si p est vrai (p = V) , il est inutile d'évaluer q car l'expression p |q est vraie (p |q = V), comme l'opérateur | évalue toujours l'expression q, Java à des fins d'optimisation de la vitesse d'exécution du bytecode dans la machine virtuelle Java, propose un opérateur ou noté || qui applique ce théorème et qui a la même table de vérité que l'opérateur | .
"p
Î { V, F } , "qÎ { V, F
} , p ||q = p |q Mais dans p||q , q n'est évalué que si p = F. |
En résumé:
Opérateur | priorité | action | exemples |
! | 1 | non booléen | ! (5 < 2) ; !(x+1 < 3) ; etc... |
& | 7 | et booléen complet | (5 = = 2) & (x+1 < 3) ; etc... |
| | 9 | ou booléen complet | (5 != 2) | (x+1 >= 3) ; etc... |
&& | 10 | et booléen optimisé | (5 = = 2) && (x+1 < 3) ; etc... |
|| | 11 | ou booléen optimisé | (5 != 2) || (x+1 >= 3) ; etc... |
Nous allons voir ci-après une autre utilisation des opérateurs
&et | sur des variables ou
des valeurs immédiates en tant qu'opérateur bit-level.
Ce sont des opérateurs de bas niveau en Java dont les opérandes
sont exclusivement l'un des types entiers ou caractère de Java
(short, int, long, char, byte). Ils permettent de manipuler directement
les bits du mot mémoire associé à la donnée.
Opérateur | priorité | action | exemples |
~ | 1 | complémente les bits | ~a; ~(a-b); ~7 (unaire) |
<< | 4 | décalage gauche | x << 3 ; (a+2) << k ; -5 << 2 ; |
>> | 4 | décalage droite avec signe | x >> 3 ; (a+2) >> k ; -5 >> 2 ; |
>>> | 4 | décalage droite sans signe | x >>>3 ; (a+2) >>> k ;-5 >>> 2 ; |
& | 7 | et booléen bit à bit | x & 3 ; (a+2) & k ; -5 & 2 ; |
^ | 8 | ou exclusif xor bit à bit | x ^ 3 ; (a+2) ^ k ; -5 ^ 2 ; |
| | 9 | ou booléen bit à bit | x | 3 ; (a+2) | k ; -5 | 2 ; |
Les tables de vérités des opérateurs "&", " | " et celle du ou exclusif " ^ " au niveau du bit sont identiques aux tables de verité booléennes ( seule la valeur des constantes V et F change, V est remplacé par le bit 1 et F par le bit 0)
Table de vérité des opérateurs bit level
p | q | ~ p | p & q | p | q | p ^ q |
1 | 1 | 0 | 1 | 1 | 0 |
1 | 0 | 0 | 0 | 1 | 1 |
0 | 1 | 1 | 0 | 1 | 1 |
0 | 0 | 1 | 0 | 0 | 0 |
Afin que le lecteur se familiarise bien avec ces opérateurs de
bas niveau nous détaillons un exemple pour chacun d'entre eux.
Afin de bien comprendre ces opérateurs, le lecteur doit bien connaître
les différents codages des entiers en machine (binaire pur, binaire
signé, complément à deux) car les entiers Java sont
codés en complément à deux et la manipulation bit
à bit necessite une bonne compréhension de ce codage.
Les exemples en 4 instructions Java : |
int i = -14 , j ;
j = ~ i ; // complémentation des bits
de i
--- ~ i --->
Tous les bits 1 sont transformés en 0 et les bits 0 en 1, puis
le résultat est stocké dans j qui contient la valeur 13 (car
000...01101 représente +13 en complément
à deux).
j = i >> 2 ; // décalage avec signe
de 2 bits vers la droite
--- i >> 2 --->
Tous les bits sont décalés de 2 positions vers la droite
(vers le bit de poids faible), le bit de signe (ici 1) est recopié
à partir de la gauche (à partir du bit de poids fort) dans
les emplacements libérés (ici le bit 31 et le bit 30), puis
le résultat est stocké dans j qui contient la valeur -4(car
1111...11100 représente -4 en complément
à deux).
j = i << 2 ; // décalage de 2
bits vers la gauche
--- i << 2 --->
Tous les bits sont décalés de 2 positions vers la gauche
(vers le bit de poids fort), des 0 sont introduits à partir de la
droite (à partir du bit de poids faible) dans les emplacements libérés
(ici le bit 0 et le bit 1), puis le résultat est stocké
dans j contient la valeur -56(car 11...1001000
représente -56 en complément à deux).
j = i >>> 2 ; // décalage sans
le signe de 2 bits vers la droite
--- i >>> 2 --->
Instruction semblable au décalage >> mais au lieu de recopier
le bit de signe dans les emplacements libérés à partir
de la gauche, il y a introduction de 0 à partir de la gauche dans
les 2 emplacements libérés (ici le bit 31 et le bit 30),
puis le résultat est stocké dans j qui contient la valeur
1073741820(car 0011...11100 représente
1073741820 en complément à deux, en effet :
j = 229+ 228+227+ ... +23+22
= 22 . ( 227+ 226+225+ ...
+21+1) = 22 . ( 228- 1) = 230
- 22 = 1073741820)