• Aucun résultat trouvé

}

C’est-`a-dire que, au cas denull pr`es, la m´ethodeprint(Object obj)appelleprint(String s) avec comme argument obj.toString(). Et l`a, il y a une petite surprise : c’est la m´ethode toString() de la classe d’origine (la (( vraie )) classe de obj) qui est appel´ee, et non pas la m´ethode toString()des Object. C’est ce que l’on appelle parfois la liaison tardive.

3 Constructions de base

Nous ´evoquons des concepts qui regardent l’ensemble des langages de programmation, et pas seulement les langages objet.

3.1 Valeurs, variables

Par valeur on entend en g´en´eral le r´esultat de l’´evaluation d’une expression du langage de programmation. Si on prend un point de vue math´ematique, une valeur peut ˆetre `a peu pr`es n’importe quoi, un entier (deZ), un ensemble d’entiers etc. Si on prend un point de vue technique, une valeur est ce que l’ordinateur manipule facilement, ce qui entraˆıne des contraintes : par exemple, un entier n’a pas plus de 32 chiffres binaires (pas plus de 32bits). Dans les descriptions qui suivent nous entendons valeur plutˆot dans ce sens technique. Par variable on entend en g´en´eral (selon le point de vue technique) une casequi poss`ede un nom o`u peut ˆetre rang´ee une valeur. Une variable est donc une portion nomm´ee de la m´emoire de la machine.

3.1.1 Scalaires et objets

Il y a en Java deux grandes cat´egories de valeurs les scalaires et les objets. La distinction est en fait technique, elle tient `a la fa¸con dont ces valeurs sont trait´ees par la machine, ou plus exactement sont rang´ees dans la m´emoire. Les valeurs scalaires se suffisent `a elle-mˆemes. Les valeurs des objets sont des r´ef´erences. Une r´ef´erence ((pointe))vers quelque chose (une zone de la m´emoire) — r´ef´erence est un nom civilis´e pourpointeurouadresse en m´emoire. ´Ecrivons par exemple

int x = 1 ;

int [] t = {1, 2, 3} ;

Les variables x et t sont deux cases, qui contiennent chacune une valeur, la premi`ere valeur

´etant scalaire et la seconde une r´ef´erence. Un sch´ema r´esume la situation.

x 1 t

1 2 3

Le tableau {1, 2, 3}correspond `a une zone de m´emoire qui contient des trois entiers, mais la valeur qui est rang´ee dans la variable test une r´ef´erence pointant vers cette zone. Le sch´ema est une simplification de l’´etat de la m´emoire, les zones m´emoires apparaissent comme des cases (les variables portent un nom) et les r´ef´erences apparaissent comme des fl`eches qui pointent vers les cases.

Si x et y sont deux variables, la construction y = x se traduit par une copie de la valeur contenue dans la variablex dans la variabley, que cette valeur soit une r´ef´erence ou non. Ainsi, le code

int y = x ; int [] u = t ;

produit l’´etat m´emoire simplifi´e suivant.

x 1 y 1 t u

1 2 3

Le sch´ema permet par exemple de comprendre pourquoi (ou plutˆot comment) le programme suivant affiche 4.

int [] t = {1, 2, 3} ; int [] u = t ;

u[1] = 4 ;

System.out.println(t[1]) ;

Il existe une r´ef´erence qui ne pointe nulle partnull, nous pouvons l’employer partout o`u une r´ef´erence est attendue.

int [] t = null ;

Dans les sch´emas nous repr´esentonsnull ainsi : t

Puisque nullne pointe nulle part il n’est pas possible de le ((d´er´ef´erencer ))c’est `a dire d’aller voir o`u il pointe. Un essai par exemple det[0] d´eclenche une erreur `a l’ex´ecution.

Egalit´´ e des valeurs

L’op´erateur d’´egalit´e==de Java s’applique aux valeurs — ainsi que l’op´erateur diff´erence!=.

Si l’´egalit´e de deux scalaires ne pose aucun probl`eme, il faut comprendre que == entre deux objets traduit l’´egalit´e des r´ef´erences et que deux r´ef´erences sont ´egales, si et seulement si elles pointent vers la mˆeme zone de m´emoire. Autrement dit, le programme

int [] t = {1, 2, 3} ; int [] u = t ;

int [] v = {1, 2, 3} ;

System.out.println("t==u : " + (t == u) + ", t==v : " + (t == v)) ;

affiche t==u : true, t==v : false. Les r´ef´erences t et u sont ´egales parce qu’elles pointent vers le mˆeme objet. Les r´ef´erences t etv qui pointent vers des objets distincts sont distinctes.

Cela peut se comprendre si on revient aux ´etats m´emoire simplifi´es.

t u

1 2 3

v

1 2 3

On dit parfois que == est l’´egalit´e physique. L’´egalit´e physique donne parfois des r´esultats sur-prenants. Soit le programme Test simple suivant

class Test {

public static void main (String [] arg) { String t = "coucou" ;

String u = "coucou" ; String v = "cou" ; String w = v + v ;

System.out.println("t==u : " + (t == u) + ", t==w : " + (t == w)) ; }

}

Une fois compil´e et lanc´e, ce programme affiche t==u : true, t==w : false. Ce qui r´ev`ele que les chaˆınes (objets) r´ef´erenc´es par tet usont exactement les mˆemes, tandis que w est une autre chaˆıne.

t u

"coucou"

w

"coucou"

La plupart du temps, un programme a besoin de savoir si deux chaˆınes ont exactement les mˆemes caract`eres et non pas si elles occupent la mˆeme zone de m´emoire. Il en r´esulte principa-lement qu’il ne faut pas tester l’´egalit´e des chaˆınes (et `a vrai dire des objets en g´en´eral) par ==.

Dans le cas des chaˆınes, il existe une m´ethode equals sp´ecialis´ee (voir 6.1.3) qui compare les chaˆınes caract`ere par caract`ere. La m´ethodeequalsest l’´egalit´e structurelle des chaˆınes.

3.2 Types

G´en´eralement un type est un ensemble de valeurs. Ce qui ne nous avance pas beaucoup ! Disons plutˆot qu’un type regroupe des valeurs qui vont naturellement ensemble, parce qu’elles ont des repr´esentations en machine identiques (byteoccupe 8 bits en machine,int en occupe 32), ou surtout parce qu’elles vont naturellement ensemble (un objetPoint est un point du plan, un objetObjectest un objet).

3.2.1 Typage statique

Java est un langage typ´e statiquement, c’est-`a-dire que si l’on ´ecrit un programme incorrect du point de vue des types, alors le compilateur refuse de le compiler. Il s’agit non pas d’une contrainte irraisonn´ee impos´ee par des informaticiens fous, mais d’une aide `a la mise au point des programmes : la majorit´e des erreurs stupides ne passe pas la compilation. Par exemple, le programme suivant contient deux erreurs de type :

class MalType {

static int incr (int i) { return i+1 ; } static void mauvais() {

System.out.println(incr(true)) ; // Mauvais type System.out.println(incr()) ; // Oubli d’argument }

}

La compilation de la classe MalType´echoue :

% javac MalType.java

MalType.java:9: Incompatible type for method. Can’t convert boolean to int.

System.out.println(incr(true)) ; // Mauvais type

^

MalType.java:10: No method matching incr() found in class MalType.

System.out.println(incr()) ; // Oubli d’argument

^ 2 errors

Le syst`eme de types de Java assez puissant et les classes permettent certaines audaces. La plus courante se voit tr`es bien dans l’utilisation deSystem.out.println(afficher une ligne sur la console), on peut passer n’importe quoi ou presque en argument, s´epar´e par des ((+)):

System.out.println ("bool´een :" + (10 < 11) + "entier : " + (314*413)) ; Cela peut se comprendre si on sait que + est l’op´erateur de concat´enation sur les chaˆınes, que System.out.println prend une chaˆıne en argument et que le compilateur ins`ere des conver-sions l`a o`u il sent que c’est utile.

Il y a huit types scalaires, `a savoir d’abord quatre types (( entier )), byte, short, int et long. Ces entiers sont en fait des entiers modulo 2p (avec p respectivement ´egal `a 8, 16, 32 et 64) les repr´esentants des classes d’´equivalence modulo 2p sont centr´es autour de z´ero,

c’est-`

a-dire compris entre −2p1 (inclus) et 2p1 (exclu). On dit aussi que les quatre types entiers correspondent aux entiers repr´esentables sur 8, 16, 32 et 64 chiffres binaires, selon la technique dite du compl´ement `a la base (l’oppos´e d’un entiernest 2p−n).

Les autres types scalaires sont les bool´eens boolean (deux valeurs true et false), les caract`ereschar et deux sortes de nombres flottants, simple pr´ecision float et double pr´ecision double. L’´economie de m´emoire r´ealis´ee en utilisant les float (sur 32 bits) `a la place des double(64 bits) n’a d’int´erˆet que dans des cas sp´ecialis´es.

Les tableaux sont un cas `a part (voir 3.6). Les classes sont des types pour les objets. Mais attention, les types sont en fait bien plus une propri´et´e du source des programmes, que des valeurs lors de l’ex´ecution. Nous essayons d’´eviter de parler du((type))d’un objet. En revanche, il n’y a aucune difficult´e `a parler du type d’une variable qui peut contenir un objet, ou du type d’un argument objet.

3.2.2 Conversion de type

La syntaxe de la conversion de type est simple (type)expression

La s´emantique est un peu moins simple. Une conversion de type s’adresse d’abord au compilateur.

Elle lui dit de changer son opinion sur expression. Par exemple, comme nous l’avons d´ej`a vu en page 179

Pair p = . . . ;

System.out.println((Object)p) ;

dit explicitement au compilateur que l’expressionp(normalement de type Pair) est vue comme un Object. Comme Pair est une sous-classe de Object (ce que sait le compilateur), ce chan-gement d’opinion est toujours possible et (Object)p ne correspond `a aucun calcul au cours de l’ex´ecution. En fait, dans ce cas d’une conversion vers un sur-type, on peut mˆeme omettre la conversion explicite, le compilateur saura changer son opinion tout seul si besoin est.

Il n’en va pas de mˆeme dans l’autre sens Object o = . . . ;

Pair p = (Pair)o ;

System.out.println(p.x) ;

Le compilateur accepte ce source, la conversion est n´ecessaire pour le faire changer d’opinion sur la valeur d’abord rang´ee danso, puis dansp: cet objet est finalement une paire, et poss`ede donc une variable d’instancex. Mais ici, rien ne le garantit, et l’expression (Pair)ocorrespond `a une v´erification lors de l’ex´ecution. Si cette v´erification ´echoue, alors le programme ´echoue aussi (en lan¸cant l’exception ClassCastException). Il est malheureusement parfois n´ecessaire d’utiliser de telles conversions (vers un sous-type) voir III.3.2, mˆeme quand on programme proprement en ne m´elangeant pas des objets de classes distinctes.

On peut aussi convertir le type des scalaires, cette fois les conversions entraˆınent des trans-formations des valeurs converties, car les repr´esentations internes des scalaires ne sont pas toutes identiques. Par exemple, si on change un int (32 bits) en long (64 bits), la machine r´ealise un

certain travail. Ce calcul n’est pas ici une v´erification, mais un changement de repr´esentation.

La plupart des conversions de types entre scalaires restent implicites et sont effectu´ees `a l’occa-sion des op´erations. Si par exemple on ´ecrit 1.5 + 2, alors le compilateur arrive `a comprendre 1.5 + (double)2, afin d’effectuer une addition entre double. Il y a un cas o`u on ins`ere soit-mˆeme ce type de conversions.

// a et b sont des int, on veut calculer le pourcentage a/b double p = 100 * (((double)a)/b) ;

(Notez l’abondance de parenth`eses pour bien sp´ecifier les arguments de la conversion et des op´erations.) Si on ne change pas explicitement un des arguments de la division en double, alors((/ ))est la division euclidienne, alors que l’on veut ici la division des flottants. On aurait d’ailleurs pu ´ecrire :

double p = (100.0 * a) / b ;

En effet, le compilateur change alorsaendouble, pour avoir le mˆeme type que l’autre argument de la multiplication. Ensuite, la division est entre un flottant et un int, et ce dernier est converti afin d’effectuer la division des flottants.

Le compilateur n’effectue jamais tout seul une conversion qui risque de faire perdre de l’in-formation. `A la place, quand une telle conversion est n´ecessaire pour typer le programme, il

´echoue. Par exemple, prenons la partie enti`ere d’undouble.

// d est une variable de type double, on veut prendre sa partie enti`ere int e = d ;

Le compilateur, assez bavard, nous dit : T.java:4: possible loss of precision found : double

required: int int e = d ;

^

Dans ce cas, on doit prendre ses responsabilit´es et ´ecrire int e = (int)d ;

Il faut noter que nous avons effectivement pris le risque de faire n’importe quoi. Si d est trop gros pour que sa partie enti`ere tienne dans 32 bits (sup´erieure `a 231−1), alors on a un r´esultat

´etrange. Le programme

double d = 1e100 ; // 10100

System.out.println(d + ", " + (int)d) ; conduit `a afficher 1.0E100, 2147483647.

3.2.3 Compl´ement : caract`eres

Les caract`eres de Java sont d´efinis par deux normes internationales synchronis´ees ISO/-CEI 10646 et Unicode. Le nom g´en´erique le plus appropri´e semblant ˆetre UTF-16. En simpli-fiant, une valeur du type char occupe 16 chiffres binaires et chaque valeur correspond `a un caract`ere. C’est simplifi´e parce qu’en fait Unicode d´efinit plus de 216caract`eres et qu’il faut par-fois plusieurscharpour faire un caract`ere Unicode. Dans la suite nous ne tenons pas compte de cette complexit´e suppl´ementaire introduite notamment pour repr´esenter tous les id´eogrammes chinois.

Un char a une double personnalit´e, est d’une part un caract`ere (comme ’a’, ’´e’ etc.) et d’autre part un entier sur 16 bits (disons le((code))du caract`ere), qui contrairement `a shortest

toujours positif. La premi`ere personnalit´e d’un caract`ere se r´ev`ele quand on l’affiche, la seconde quand on le compare `a un autre caract`ere. Les 128 premiers caract`eres (c’est-`a-dire ceux dont les codes sont compris entre 0 et 127) correspondent exactement `a un autre standard international bien plus ancien, l’ASCII.

L’ASCII regroupe notamment les chiffres de ’0’ `a ’9’ et les lettres (non-accentu´ees) mi-nuscules et majuscules, mais aussi un certain nombre de caract`eres de contrˆole, dont les plus fr´equents expriment le((retour `a la ligne )). Une petite digression va nous montrer que la stan-dardisation du jeu de caract`eres n’est malheureusement pas suffisante pour tout normaliser. En Unix un retour `a la ligne s’exprime par le caract`ere line feed not´e ’\n’, en Windows c’est la s´equence d’uncarriage return not´e’\r’et d’unline feed, et en Mac OS, c’est uncarriage return tout seul !

Le plus souvent ces d´etails restent cach´es, par exemple System.out.println() effectue toujours un retour `a la ligne sur l’affichage de la console, c’est le code de biblioth`eque qui se charge de fournir les bons caract`eres `a la console selon le syst`eme sur lequel le programme est en train de s’ex´ecuter. Toutefois des probl`emes peuvent surgir en cas de transfert de fichiers d’un syst`eme `a l’autre. . .

3.3 D´eclarations

De fa¸con g´en´erale, une d´eclaration ´etablit une correspondance entre unnom et une construc-tion nommable (variable, m´ethode, mais aussi constante camoufl´ee en variable). Les d´eclaraconstruc-tions de variable sont de la forme suivante :

modifiers type name;

Lesmodifierssont des mots-cl´es (static, sp´ecification de visibilit´e private etc., et final pour les constantes), le type est un type (genre int, int[] ou String) et name est un nom de variable. Une bonne pratique est d’initialiser les variables d`es leur d´eclaration, ¸ca ´evite bien des oublis. Pour ce faire :

modifiers type name =expression;

O`uexpressionest une expression du bon type. Par exemple, voici trois d´eclarations de variables, de types respectifs((entier)), chaˆıne et tableau de chaˆıne :

int i = 0;

String message = "coucou" ; String [] jours =

{"dimanche", "lundi", "mardi", "mercredi",

"jeudi", "vendredi", "samedi"} ;

Les d´eclarations de((variable ))sont en fait de trois sortes : (1) Dans une classe,

(a) Une d´eclaration avec le modificateur static d´efinit une variable (un champ) de la classe.

(b) Une d´eclaration sans le modificateur static d´efinit une variable d’instance des objets de la classe.

(2) Dans une m´ethode, une d´eclaration de variable d´efinit une variable locale. Dans ce cas seul le modificateur final est autoris´e.

Ces trois sortes de (( variables )) sont toutes des cases nomm´ees mais leurs comportements sont diff´erents, voire tr`es diff´erents : les variables locales sont autonomes, et leurs noms ob´eissent

`

a la port´ee lexicale (voir “structure de bloc” dans 3.4), les deux autres (( variables )) existent

comme sous-parties respectivement d’une classe et d’un objet ; elles sont normalement d´esign´ees commeC.x et o.x, o`u C eto sont respectivement une classe et un objet.

Sur une note plus mineure, les variables de classe et d’objet non-initialis´ees explicitement contiennent en fait une valeur par d´efaut qui est fonction de leur type — z´ero pour les types d’entiers et de flottants (et pour char), false pour les boolean, et null pour les objets. En revanche, comme le compilateur Java refuse l’acc`es `a une variable locale quand celle-ci n’a pas

´et´e initialis´ee explicitement, ou affect´ee avant lecture, il n’y a pas lieu de parler d’initialisation par d´efaut des variables locales.

Les d´eclarations de m´ethode ne peuvent se situer qu’au niveau des classes et suivent la forme g´en´erale suivante :

modifiers type name (args)

Les modifiers peuvent ˆetre, static (m´ethode de classe), une sp´ecification de visibilit´e, final (red´efinition interdite), synchronized (acc`es en exclusion mutuelle, voir le cours suivant) et native(m´ethode ´ecrite dans un autre langage que Java). La partietype est le type du r´esultat de la m´ethode (void si il n’y en a pas), name est le nom de la m´ethode et args sont les d´eclarations des arguments de la m´ethode qui sont de bˆetes d´eclarations de variables (sans le ((;))final) s´epar´ees par des virgules((,)). L’ensemble de ces informations constitue lasignature de la m´ethode.

Suit ensuite le corps de la m´ethode, entre accolades (( { )) et (( } )). Il est notable que les d´eclarations d’arguments sont des d´eclarations de variables ordinaires. En fait on peut voir les arguments d’une m´ethode comme des variables locales presque normales. `A ceci pr`es qu’il n’y a pas lieu d’initialiser les arguments ce qui d’ailleurs n’aurait aucun sens puisque les arguments sont initialis´es `a chaque appel de m´ethode.

3.4 Principales expressions et instructions

Nous d´ecrivons maintenant ce qui se trouve dans le corps des m´ethodes, c’est-`a-dire le code qui fait le vrai travail. Le corps d’une m´ethode est une s´equence d’instructions). Les instruc-tions sont ex´ecut´ees. Une instruction (par ex. une affectation) peut inclure une expression. Les expressions sont´evalu´ees en un r´esultat.

La distinction entre expressions (dont l’´evaluation produit un r´esultat) et instructions (dont l’ex´ecution ne produit rien) est assez hypocrite, car toute expression suivie de((;))devient une instruction (le r´esultat est jet´e).

Les expressions les plus courantes sont :

Constantes Soit1(entier), true (bool´een),"coucou !"(chaˆıne), etc. Une constante amusante est null, qui est un objet sans champs ni m´ethodes.

Usage de variable Soit((x )), o`ux est le nom d’une variable (locale) d´eclar´ee par ailleurs.

Mot-cl´ethis Dans une m´ethode dynamique , this d´esigne l’objet dont on a appel´e la m´ethode.

Dans un constructeur, this d´esigne l’objet en cours de construction. Il en r´esulte que this n’est jamais null, car si on en est arriv´e `a ex´ecuter un corps de m´ethode, c’est bien que l’objet dont a appel´e la m´ethode existait.

Acc`es `a un champ Si x est un nom de champ et classe un nom de classe C, alors (( C.x )) d´esigne le contenu du champ m de C. De mˆeme ((objet.x )) d´esigne le champ de nom x de l’objet objet. Notez que, contrairement `a x, objet n’est pas forc´ement un nom, c’est une expression dont la valeur est un objet. Heureusement ou malheureusement, il existe des notations abr´eg´ees qui all`egent l’´ecriture (voir 3.5), mais font parfois passer l’acc`es `a un champ pour un usage de variable.

Appel de m´ethode C’est un peu comme pour les champs :

statique Soit,C.m(...),

dynamique ou bien, objet.m(...).

O`u m est un nom de m´ethode et (...) est une s´equence d’expressions s´epar´ees par des virgules. Notez bien que les mˆemes notations abr´eg´ees que pour les champs s’appliquent au nom de la m´ethode. Incidemment, si une m´ethode a un type de retourvoid, son appel n’est pas vraiment une expression, c’est une instruction.

Appel de constructeur G´en´eralement de la forme new C (...). La construction des tableaux est l’occasion de nombreuses variantes, voir 3.6

Usage des op´erateurs Par exemple i+1 (addition) ou i == 1 || i == 2 (op´erateur ´egalit´e et op´erateur ou logique). Quelques op´erateurs inattendus sont donn´es en 7.2. Notons qu’un ((op´erateur ))en informatique est simplement une fonction dont l’application se fait par une syntaxe sp´eciale. L’application des op´erateurs est souventinfixe, c’est-`a-dire que l’op´erateur apparaˆıt entre ses arguments. Mais elle peut ˆetre pr´efixe, c’est-`a-dire que l’op´erateur est avant son argument, comme dans le cas de la n´egation des bool´eens!; ou encore postfixe, comme pour l’op´erateur de post-incr´ement i++. En Java, comme souvent, les op´erateurs eux-mˆemes sont exclusivement compos´es de caract`eres non alphab´etiques (+,-,=, etc.).

Acc`es dans les tableaux Par exemplet[i], o`u t est un tableau d´efini par ailleurs. Il n’est pas

Acc`es dans les tableaux Par exemplet[i], o`u t est un tableau d´efini par ailleurs. Il n’est pas