Mémento de programmation Java
Packaging
Un fichier java est structuré de la manière suivante :
-
[package <declaration>;]
-
[import <blabla>;]
-
définition de classes.
Remarques :
-
un fichier java comporte au plus une classe publique (il peut ne
pas y en avoir du tout), et optionnellement plusieurs classes non publiques
-
s'il y a le mot clé package, toutes les classes du fichier appartiendront
au package défini. Les fichiers .class du package Toto devront se
trouver dans le répertoire Toto.
Pour éxécuter une classe appartenant à un package,
il faudra lancer java <NomPackage>.<Classe>
-
si le fichier comprend une classe publique, le nom du fichier doit être
<nom
classe publique>.java
Visibilité
package Toto;
public class A {
...
}
class B {
...
} |
import Toto;
// je vois la classe A de Toto car elle est publique
// je ne vois pas la classe B (elle n'est pas publique). Pour // voir
la classe B, il faudrait que j'appartienne au package // Toto. |
Exécution
java <nom de la classe qui
contient le point d'entrée main à exécuter>
le point d'entrée est public
static void main(String args[]) car :
-
public pour que la Java Virtual Machine puisse y avoir accès
-
static pour qu'on n'ait pas besoin d'en construire une instance pour l'exécuter.
-
args contient les arguments passés par l'utilisateur (après
le nom de la classe : java MaClasse Arg0 Arg1 ...). On peut donner n'importe
quel nom à args.
Types, identifiants et littéraux
Types
Java définit la taille des types primitifs connus (la taille
ne dépend pas de la plateforme) :
un booléen (boolean)
ou un byte feront toujours 1 octet
(logique !) de taille
un short ou un char
feront toujours 2 octets de taille
un int ou un float
feront toujours 4 octets de taille
un long ou un double
feront toujours 8 octets de taille.
Les types dans Java sont tous signés SAUF boolean et char
(ce ne sont pas des types numériques).
L'étendue des types numériques est donc :
Type numérique |
Minimum |
Maximum |
Byte |
-2^7 (-128) |
2^7 -1 (127) |
Short |
-2^15 (-32768) |
2^15-1 (32767) |
Int |
-2^31 |
2^31-1 |
Long |
-2^63 |
2^63-1 |
Les caractères Java sont au format Unicode (ce
qui est un format codé sur 16 bits ie 2 octets). Si les 9 bits de
poids fort sont à 0, alors les 7 autres correspondent aux 7 bits
du format ASCII.
Les opérations sur des types flottants peuvent aboutir
à des valeurs qui ne sont pas des nombres :
Float.NaN (Not A Number), Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY
idem pour Double.
Littéraux
Un nombre peut être écrit :
-
en décimal (par défaut)
-
en octal : précédé d'un zéro. Exemple : 034
-
en hexadécimal : précédé de 0x ou 0X.
Exemple : 0x1c
Par défaut, un nombre sera considéré comme un int,
donc sur 32 bits ; à moins d'avoir spécifié un L (pour
64 bits, long).
Pour un floattant, par défaut, on considère que c'est
un double.
Les floattants peuvent s'écrire :
-
4.23E(e)+21, ou 1.828f(F) ou 1.2d(D)
Les caractères peuvent s'écrire :
-
'w'
-
on peut utiliser certains caractères de contrôle : \n (newline)
\r (return) \t (tab) ...
-
les caractères non écrivables sont spécifiés
par \u<hexa> Ex. '\u4214'
Rem. On ne peut pas écrire un caractère avec char c = -1,
char c = 500L ...
Variables, identifiants
Un identifiant de variable doit
-
commencer par une lettre, un $ ou un underscore
-
peut se poursuivre par des lettres, $, underscore, 0-9
Quelques mots clés : ils sont case sensitive, goto ...
union n'est pas un mot clé.
Initialisation
-
les données membres d'une classe sont initialisées à
0.
-
les variables statiques sont initialisées au chargement
de la classe
-
les variables non statiques sont initialisées juste avant l'exécution
du constructeur.
-
les variables locales (variables au sein d'une méthode) ne
sont pas initialisées par le système (on aura donc une
erreur de compilation si la variable n'est pas initialisée).
Tableaux
L'utilisation d'un tableau se fait en trois temps :
-
déclaration : les [] peuvent se trouver avant ou après
le nom de la variable. Pour les tableaux multidimentionnels, on a plusieurs
séries de [].
-
construction (new) : il faut déclarer la taille du tableau.
Comme la taille n'est utilisée qu'au run-time, il est parfaitement
légal de mettre une variable en guise de taille. Par ex. tab
= new int[isize];
-
initialisation : par défaut, tous les items sont initialisés
à 0 (qu'ils s'agissent de variable locale, ou statique).
On peut combiner déclaration et construction :
int tab [] = new int [25];
On peut combiner déclaration, construction et initialisation
par :
float diam[] = {1.1f, 2.2f };
On peut obtenir la taille d'un tableau par length
: tab.length (ne pas mettre de
length( ) ce n'est pas une méthode !).
Les tableaux sont numérotés de 0 à length -
1.
Remarque : int tab[5] = new int [5]; est interdit.
Les opérateurs
-
+ peut servir de concaténation de Strings.
-
^ désigne le OU exclusif.
-
~ désigne l'inversion bittesque.
-
! désigne la négation. Elle ne s'effectue que sur
des booléens.
-
&&, || sont des opérateurs de court-circuit : les membres
à droite ne sont pas forcément examinés.
-
la comparaison d'un NaN retourne toujours false, sauf NaN != NaN qui retourne
true. Il vaut donc mieux tester qu'une variable n'est pas un NaN via la
méthode isNaN(...)
-
une division par 0 renvoie une ArithmeticException.
-
un overflow de float ou de double renvoie un POSITIVE/NEGATIVE_INFINITY.
-
un overflow d'int n'est pas indiqué et renvoie n'importe quoi
(suivant la machine).
shifts :
-
les valeurs entrées sont des 0, sauf pour le shift à droite
(>>) sur des nombres négatifs, où on rentre des 1 (afin de
conserver le fait que le nombre est négatif).
-
Pour un "triple-shift" (>>>) on rentre des 0 quoiqu'il arrive.
Remarques :
les opérandes sont promotés au moins à la taille
d'un int avant d'effectuer un shift. Ensuite, le résultat est tronqué.
le shift ne peut se faire au maxi que sur la taille du type (32 pour
les int, 64 pour les long). Au delà, le shift sera effectué
sur %32(64) du shift indiqué.
Exemple :
192 (int) =
00000000 00000000 00000000 11000000
192 >> 1 = (96) 00000000 00000000 00000000
01100000
-192 (int) =
11111111 11111111 11111111 01000000
-192 >> 1 = (-96) 11111111 11111111 11111111 10100000
Passage d'arguments : par copie (p17)
pour les variables, on passe une copie.
pour les objets (idem pour les tableaux), la construction d'un objet
renvoie un identifiant référencant uniquement l'objet (généralement,
c'est un pattern sur 32 bits). Le passage par argument d'un objet passe
donc la copie d'une référence, ie, on ne peut modifier la
référence de l'appelant, en revanche, on peut modifier l'objet
lui-même.
Les boucles
while ( ) et do { ... }, if ...
attention, l'expression doit être booléenne !
Exemple : int i; if (i) est interdit !
for ( ... )
On peut déclarer une variable dans un for(...). Alors cette variable
n'est utilisable qu'au sein de la boucle.
Exemple : for (int i = 0; i<2;i++) { .... // i valable }
// i invalide
On peut mettre plusieurs expressions dans le premier élément
et le dernier du for, séparés par des virgules.
Mais, attention, on ne peut plus déclarer de variable après
une expression.
Exemple : int i=2, j=1 // ok mais j=1, int i=2 // illegal
labels
même utilisation qu'en C du continue, break.
si on ne nomme pas le break, on doit le faire sur un for, while, do
ou switch.
le continue se fait toujours sur une boucle.
break arrête l'itération de la boucle actuelle, tandis
que continue passe directement à l'itération suivante sans
avoir complété l'actuelle.
exemple : mainLoop : for (int i = 0; i<2;i++) { ... }
switch
fonctionne avec des byte, short, char et int.
ne fonctionne pas avec des long, float, double...
Conversions, affectations,
cast
Des conversions de types peuvent apparaître dans chacun des cas suivants
:
-
Affectation : int i = 5;
-
Promotion :
byte b;
int i;
i = b * 5; // b est d'abord
promoté à un int, avant d'effectuer la multiplication.
-
Appel de fonction :
void toto(double) { ... }
float f;
toto(f);
// f est converti en double
-
Cast explicite :
int i;
double d = (double) i;
// cast explicite
Convertir, c'est l'opération qui fait changer de type une valeur.
Une opération de réduction, c'est une opération
où l'on garde seulement les bits de poids faible.
Régle : le cast explicite est nécessaire pour
toute opération de réduction. Il n'est pas nécessaire
pour les autres opérations.
Attention :
-
On ne peut pas caster un booléen en autre chose.
-
Le résultat d'une comparaison est toujours de type booléen.
-
L'utilisation d'opérateurs unaires promeut les bytes short et char
en int.
-
L'utilisation d'opérateurs binaires promeut l'opérande à
la taille du plus grand, et au moins à un int.
Conversion, cast d'objets
float f = 1.2f;
int i = (int) f; // ok, et i est à
1.
Remarques :
byte b = 123; // ok... c'est
un peu une sorte d'exception, et ça ne marche que si le littéral
est bien dans les limites d'un byte.
short s = 12; // ok
short s =35557; // ko à la
compilation.
La conversion d'un objet peut se faire vers un objet de classe mère.
Object est la classe mère de tout objet.
Cloneable est la classe mère de toute interface.
Exemple :
si Thésard dérive de Chercheur, que Chercheur implémente
Individu, alors :
Thesard ludo = new Thesard();
Chercheur c
= ludo; // ok
Individu i =
ludo; // ok
Thesard axelle
= c; // ko
Thesard toto
= i; // ko
Au run-time, la JVM effectue la comparaison des vrais types des
objets (ceci est possible car Java garde une trace du type effectif de
chaque objet). Ainsi, certaines exceptions peuvent être levées
au run-time.
Mettons que Thésard et Génie dérivent de Chercheur,
alors :
Thesard toto = new Thesard();
Chercheur c
= toto; // ok
Thesard t =
(Thesard) c; // ok au run-time parce que c est effectivement
un thésard
Genie g = (Genie)
c; // ko au run-time, parce que
c est un thésard, et rien ne permet de caster en Genie (à
priori).
Classes et objet
Surcharge (termes overloading et overriding)
Réutiliser le nom d'une fonction, avec des arguments différents,
à plusieurs effets, c'est du overloading.
Dans ce cas, la surcharge uniquement par type de retour n'est
pas autorisée.
Réutiliser exactement la même fonction à plusieurs
effets (via dérivation), c'est du overriding.
On ne peut surcharger une fonction qu'en la rendant plus (ou
égal) accessible.
On ne peut lancer des exceptions non déclarées par la
fonction mère.
Late binding : dans le cas de surcharge de méthodes, le compilateur
ne peut pas déterminer quelle méthode il doit appeler, car
celle-ci dépendra de la classe de l'objet qui invoquera la méthode.
Le binding se fait alors au dernier moment : à l'allocation, le
runtime indique de quelle classe il s'agit, et cette information est conservée
durant la vie de l'objet.
Constructeurs
-
Pour utiliser le constructeur de la classe mère, on peut le faire
par appel de super( ... arg....);
-
Pour utiliser un autre constructeur de la classe, on peut le faire par
appel de this( ... arg... ); (si on le fait par un new, hélas,
on crée un autre objet).
Exemple :
public class MyClass {
public MyClass(int i, int j){
...
}
public MyClass(int i){
this(i,0);
// appelle le constructeur MyClass(int i, int j)
}
}
-
Un constructeur par défaut MyClass( ) (ie sans arguement) est défini
pour chaque classe qui n'a pas de constructeur explicite. Le constructeur
par défaut n'est donc pas hérité. Si un constructeur
ne fait appel ni à this(...) ni à super(...), alors, le compilateur
fait appel au constructeur par défaut de la classe mère en
première ligne du code.
-
super et this doivent être placés en première ligne
du code - afin que l'objet soit immédiatement initialisé.
-
un constructeur ne peut pas être abstract, native, static, synchronized,
final. Mais rien n'empêche qu'il soit private, protected ou public
(par ex. faire un constructeur private peut empêcher une classe d'être
instanciée).
Garbage collector
On ne peut jamais forcer son démarrage, même System.gc() ne
fait que suggérer son utilisation.
Interface
-
Une interface ne définit que les prototypes de méthodes.
-
Une interface peut être friendly (par défaut) ou publique
: [public] interface <nom interface>
{ ... }
-
Une interface ne peut définir que des variables statiques finales.
-
Les classes qui implémentent une interface doivent définir
TOUTES les méthodes de l'interface (à moins d'être
abstraites).
-
Les classes implémentées doivent être en accès
public.
Une interface permet de mettre en oeuvre facilement le polymorphisme :
interface Toto {
...
}
class A implements Toto {
...
}
Toto t = new A(); // légal.
A l'exécution, on saura que t est de classe A.
Classes intrinsèques
classe intrinsèque à une autre classe
public class OuterClass {
public class InnerClass
{
...
}
public void makeInner(){
InnerClass a =
new InnerClass(); // c'est comme si on avait this.InnerClass()
...
}
}
Pour construire une instance de la classe intrinsèque, il
faut avoir une référence à la classe extrinsèque.
(d'ailleurs, une instance de la classe extrinsèque sera forcément
créée).
-
soit on est dans du code non statique, et alors, l'utilisation de this
est autorisée (et implicite). (cf makeInner)
-
soit on est dans du code statique, et alors, this n'existe pas.
Il faut alors construire explicitement la classe extrinsèque
:
-
OuterClass.InnerClass i = new OuterClass().
new InnerClass(); // new OuterClass crée un "this" pour new InnerClass.
ou :
-
OuterClass o = new OuterClass();
OuterClass.InnerClass i = o.new InnerClass();
Puisque la classe intrinsèque possède une référence
à la classe extrinsèque, elle peut accéder aux
variables membres de la classe extrinsèque.
Si la classe intrinsèque est déclarée statique,
attention, elle ne peut accéder qu'aux variables membres statiques
de la classe extrinsèque.
classe intrinsèque à une méthode
On peut définir une classe intrinsèque à une méthode.
Cependant, attention, cette classe intrinsèque ne peut accéder
qu'aux données marquées "final" de la méthode.
En effet, les données d'une classe peuvent survivre à
l'exécution de la méthode. Or, conventionnellement, les données
locales de la méthode sont détruites au retour de la méthode.
Donc, on ne peut les garder. En revanche, les données final peuvent
être recopiées dans l'objet.
classe anonyme
public void f(){
bttn.addActionListener(
new
ActionListener() {
// rem. c'est le nom de l'interface
public void actionPerformed(ActionEvent ae){
System.out.println("action");
}
}
);
}
-
Les classes anonymes sont construites là où elles sont déclarées.
-
Une classe anonyme ne peut être instantiée au aucun autre
endroit.
-
Une classe anonyme ne peut pas avoir de constructeur (puisqu'elle n'a pas
de nom)
Polymorphisme
Vérification dynamique
La vérification dynamique de type ne fonctionne que pour les
méthodes d'instances.
class Chercheur {
void travailler(){
System.out.println("CHERCHEUR: travailler.");
}
}
class Thesard extends Checheur {
void travailler(){
System.out.println("THESARD: travailler.");
}
}
Chercheur c = new Chercheur();
Thesard t = new Thesard();
Chercheur ludo = new Thesard();
// ok
c.travailler(); // appelle la méthode
travailler de Chercheur - logique
t.travailler(); // appelle la méthode
travailler de Thesard - logique
ludo.travailler(); // appelle
la méthode travailler de THESARD. Il y a vérification dynamique
du type de ludo, et on appelle la méthode correspondante.
Attention, cette vérification dynamique ne s'effectue pas pour
les méthodes statiques, ni pour les variables d'instances (ni quoique
ce soit d'autres qu'une méthode d'instance).
Quelques classes remarquables ...
Math
c'est une classe finale : on ne peut donc pas en créer d'instance
ses méthodes sont statiques : donc on peut les utiliser sans
créer d'instance (ouf !)
La classe Math est final. On ne peut donc pas en créer d'instance
Wrapper classes
Il s'agit de classes d'encapsulation des types primitifs de Java. Il y
en a pour chaque type.
Ce sont également des classes finales.
On peut les construire :
-
en passant la valeur du type primitif
Exemple :
short s = 12;
Short wrappedShort = new Short(s);
-
ou en passant une String qui représente cette valeur. (attention,
ne fonctionne pas pour Character). Ces constructeurs peuvent retourner
un NumberFormatException.
Exemple :
Byte wrappedByte = new Byte("41");
Remarque :
dans le cas d'un booléen, true est construit si la chaine correspond
à "true" (sans considérer la casse). Sinon, on a un false.
On peut récupérer la valeur d'une instance d'une wrapper
classe par une méthode public xxxx xxxxValue( );
Exemple :
Character wrapperC = new Character('m');
char d = wrapperC.charValue();
Toutes les classes, sauf Character, définissent la méthode
statique valueOf( ) qui parse une string, construit une instance
de la wrapper class appelée et la retourne.
Exemple :
Double d = d.valueOf("12.23");
Remarque : il n'y a pas de différence entre Double d
= d.valueOf("12.23"); et Double d = new Double("12.23"); A la limite, on
peut juste considérer que la première sorte est un peu plus
rapide.
Comparaison
La comparaison entre deux instances d'une wrapper class se fait par
la méthode equals.
if (d1.equals(d2)){
...
}
Attention, d1==d2 opère
une comparaison de références d'objets, pas de contenu !
String
Il s'agit d'une classe de chaînes pseudo non modifiable : ie, on
peut modifier ce à quoi la chaine fait référence,
mais pas le contenu.
L'index des caractères commence à 0.
Exemple :
String s = "5+4=20";
s = s.replace('+','*');
s référence alors une String de valeur "5*4=20". Attention,
le littéral "5+4=20" existe toujours en mémoire, mais n'est
plus référencé par s.
Il existe une pool de chaines littérales construites dans un
programme Java. Ainsi, dès qu'un programme référence
une nouvelle chaine, le littéral est ajouté au pool, et on
ne retourne qu'une référence à cette chaine
String s1 = "toto";
String s2 = "toto"; // référence
au littéral toto
if (s1.equals(s2)) {
// fonctionne car s1 et s2 font référence
à la même chaine.
} |
String s1 = "toto";
String s2 = "toto";
if (s1==s2){
// fonctionne car s2 référence
la même valeur dans le pool
} |
Aussi, lorsqu'on effectue la commande String s = new String("toto");
il y a en fait deux objets en mémoire :
-
dans le pool, on a la chaine littérale "toto", à moins qu'elle
n'existe déjà.
-
on crée ensuite une instance de String, dans laquelle on recopie
le littéral "toto".
-
on renvoie une référence à cette String.
Rem. Les fonctions trim (enlève les espaces), replace, toUpperCase
etc fonctionnent de la manière suivante : si la chaine n'est pas
modifiée, on renvoie une référence à la string
passée. Sinon, on crée un nouvel objet qui contient le résultat
et retourne cet objet là. L'objet en entrée n'est biensûr
jamais affecté.
La classe StringBuffer représente une chaine dynamiquement
modifiable. Une StringBuffer a une capacité, ie la taille maximale
qu'elle peut avoir sans devoir effectuer une réallocation. C'est
le système qui se préoccupe de cette réallocation,
on n'a pas usuellement à s'en soucier.
Remarque : on ne peut pas comparer une String à
une StringBuffer, ou réciproquement.
L'opération +
si on fait un "toto" + "tata", c'est comme si on faisait new StringBuffer().append(toto).append(tata);
si on fait un "toto"+ dimension, où dimension est un objet,
c'est comme si on faisait un appel à dimension.toString();
Les modificateurs
modificateurs d'accès
-
private : utilisable uniquement dans la classe où la déclaration
est faite
-
friendly (par défaut) : accessible à toute classe du package.
S'il s'agit d'une classe dérivée, mais hors du package, ça
ne marche pas.
-
protected : accessible à toute classe du package + toutes les classes
dérivées
-
public : accessible à tout le monde.
Une classe extrinsèque ne peut être que public ou friendly.
On ne peut surcharger une fonction qu'en la rendant plus accessible
Exemple : la classe Applet définit une méthode
init( ) déclarée en public. Lorsqu'on crée une applet,
on surchargera habituellement init par notre propre init. Notre init doit
alors être public (private, friendly ou protected aboutiraient à
une erreur de compilation car on restreint l'accès).
autres modificateurs
final : indique qu'on ne peut pas modifier !
-
final <objet> = interdiction de modifier la référence
à l'objet (mais on peut modifier l'objet)
-
final <méthode> = interdiction de surcharger cette fonction.
-
final <classe> = interdiction d'instancier la classe (cf classe Math).
Exemples :
class MyObject {
int value;
MyObject(int v){
value = v; }
}
final MyObject m = new MyObject(1500);
m = new MyObject(2); // illegal
m.value = 10; // legal
class Genie {
final int Nb();
}
class Mari extends Genie {
int Nb(); // illegal
}
abstract : indique qu'une classe/méthode est incomplète
Remarques :
-
dès qu'une méthode est incomplète, alors la classe
est également incomplète (et donc doit être marquée
abstract)
-
on ne peut pas appeler le constructeur d'une classe abstraite
-
si une classe hérite d'une classe abstraite et qu'elle n'implémente
pas toutes les fonctions manquantes, elle est alors abstraite elle aussi.
(logique).
-
une méthode abstraite ne peut pas être final (logique, c'est
quasi opposé en signification !)
-
une méthode abstraite ne peut être static, car cela voudrait
dire qu'elle est propre à la classe, or la classe ne peut être
instanciée...
static : spécifie une donnée propre à la classe
(et non à l'instance). Cette donnée est allouée au
chargement de la classe.
-
static <variable> = indique que cette variable est propre à la
classe, et non à l'instance. Il peut y avoir plusieurs instances
de la classe, il n'y aura qu'une seule fois la variable. Une variable statique
est forcément déclarée à l'extérieur
de toute méthode (même à l'extérieur d'une méthode
statique).
-
static <methode> = méthodes propres à la classe, et non
pas aux instances de la classe. Par conséquent :
-
on peut appeler une méthode statique d'une classe sans avoir créé
d'instance de cette classe.
-
=> une méthode statique ne peut utiliser que les ressources statiques
de la classe !
-
=> il n'y a pas de "this" dans une méthode statique (puisqu'il n'y
a pas forcément d'instance !).
-
une méthode statique ne peut pas être surchargée par
une méthode non statique
-
static { ... code ... } = initialisateur statique. Délimite une
portion de code de la classe qui n'est exécuté qu'une seule
fois au chargement de la classe.
On peut faire référence à une variable statique par
le nom de sa classe aussi bien que par le nom de la variable.
Chercheur c = new Chercheur( ); //
cette classe définit une variable statique QImin.
Chercheur.QImin = 150;
// valide
c.QImin = 152;
// valide
native : indique que le code de la méthode se trouve en
dehors de la JVM.
Il faut donc faire un System.loadLibrary("MyNativeLib") auparavant.
Cet appel est généralement fait un initialisateur statique
de la classe, afin de ne pas causer de retard d'exécution.
Le code chargé peut être écrit en C, C++...
si on écrit :
class Example {
public static void main(String
args[]){
System.out.println("coucou");
}
native void machin();
}
cela compile. En revanche, pour appeler machin(), il faudra charger
la librairie correspondante.
transient : indique que la donnée ne doit pas être
stockée avec l'objet (par confidentialité, par exemple).
Une variable marquée "transient" ne sera pas écrite si
on effectue un output de l'objet dans un stream (sérialisation).
Une variable transient ne peut pas être final, ni static.
Exemple :
class BankAccount implements Serializable
{
private float amout;
private transient String cardCode;
}
synchronized : définit une section critique. Peut être
marqué sur une méthode, une portion de code, ou un objet
synchronize(objet){...}
volatile : une variable volatile indique qu'elle peut être
modifiée de manière asynchrone (et donc le compilateur fait
attention). Les actions effecutées sur une variable volatile se
font dans l'ordre dans lequel elles sont indiquées par le thread
concerné.
Exceptions
Rattraper des exceptions
Il existe plusieurs sortes d'exceptions :
-
Checked Exceptions : "normales" (java.lang.Exception).
-
Runtime Exception : bugs du programme (java.lang.RuntimeException)
-
Error : le reste...
On n'est censé rattraper que les checked exceptions
(ie tout java.lang.Exception mis à part RuntimeException).
try {
... // code dans
durant lequel des exceptions peuvent être lancées
}
catch (ExceptionClassOne e){
...// code exécuté
uniquement si une exception de classe ExceptionClassOne
// est lancée.
}
[catch (ExceptionClassTwo f){
...// code exécuté
uniquement si une exception de classe ExceptionClassTwo
//
est lancée
}]
[finally {
// code qui est
toujours exécuté qu'il y ait une exception ou non.
// ce code est
même exécuté si on fait un return avant !
}]
// suite du code : n'est éxécuté
que si aucune exception n'a été levée,
// ou si l'exception a été
rattrapée.
Remarques :
-
au plus une branche catch est exécutée. Donc si la classe
A dérive de B, la branche catch( A...) doit être placée
avant la branche catch(B ...)
-
la branche finally est toujours exécutée, qu'une exception
ait lieu ou non, qu'il y ait un return antérieur ou non.
Lancer des exceptions
Toute méthode susceptible de lancer une exception doit le déclarer
par la formule throws ExceptionClassOne[,ExceptionClassTwo....]
Exemple : void doSomething(int
i) throws ArithmeticException { ... }
L'exception est construite tel un objet, et lancée par throw.
Exemple : throw new IOException("File
not found");
Surcharger des méthodes qui lancent des exceptions...
La méthode qui surcharge ne doit lancer des exceptions que du type
déclaré dans la classe mère. Elle peut donc lancer
des exceptions de sous-classes de cette classe, mais pas plus.
Exemple : Soit StreamCorruptedException et MalformedURLException
deux sous-classes de IOException.
class A {
public int func() throws IOException
{
...
}
}
class B extends A {
public int func() throws StreamCorruptedException,
MalformedURLException { // ok
...
}
}
class C extends A {
public int func throws IOException,
ArithmeticException { // illegal
}
}
Threads
Création
Pour faire démarrer un thread (ie le rendre éligible), on
peut procéder de deux manières différentes :
-
créer une sous-classe de Thread, dans laquelle on implémente
la méthode public void run( ) { ... }. Puis construire une instance
de la sous-classe, et la démarrer par appel de start( ).
class MyClass extends Thread {
public void run( ) {
...
}
}
...
MyClass c = new MyClass();
c.start( ) ;
-
ou créer un thread à partir d'un objet qui implémente
Runnable, et faire un start.
class MyClass implements Runnable
{
public void run( ){
...
}
}
MyClass c = new MyClass();
Thread t = new Thread(c); // création
à partir de l'objet qui implémente Runnable
t.start();
Terminaison
Un thread se termine lorsque sa méthode run() retourne.
Un démon (isDaemon, setDaemon) se termine également
si le main() du programme retourne. En effet, le but d'un démon
est d'assurer une tâche de fond non essentielle au programme : il
se termine donc au retour du main().
L'appel à System.exit(...) termine l'exécution du programme
(mais pas nécessairement un return).
Un programme se termine lorsque tous les threads non démons
sont terminés.(ie les démons ne survivent pas à la
mort du programme).
Remarque : les threads créés par des démons
sont également des démons (on peut changer cela par un setDaemon).
la méthode stop() n'est plus conseillée en Java 1.2 :
dans la plupart des cas, il vaut mieux ajouter une variable indiquant quand
arrêter le thread. Le thread doit régulièrement consulter
cette variable, et retourner de son run lorsque la variable indique un
arrêt. Il est conseillé de déclarer cette variable
en tant que volatile (indique qu'elle peut être modifiée de
manière asynchrone)... ou alors synchroniser l'accès à
la variable
Les priorités
Dans Java, les threads ont une priorité de 1 à 10 (plus haute
priorité). La priorité par défaut est 5.
Un thread hérite de la priorité du thread qui le crée.
Pour positionner exactement une priorité, on utilise setPriority.
On peut utiliser les constantes Thread.NORM_PRIORITY, MAX/MIN_PRIORITY.
Attention, la gestion des priorités est dépendante
de la machine virtuelle.
L'implémentation du scheduler est plateforme dépendante.
Certains schedulers seront
-
préemptifs (une tâche est préemptée si une autre
tâche de priorité supérieure est prête à
être exécutée),
-
ou time-sliced (chaque tâche dispose d'un certain temps dans le CPU).
Le contrôle de threads
Yield : permet de passer de l'état Running à l'état
Eligible, et ainsi de laisser une chance à d'autres threads de prendre
un peu de temps CPU.
Suspend / resume : permet de passer un thread de l'état
Running à l'état Suspendu (Suspend) ou de l'état Suspendu
à Eligible (Resume).
Ces primitives peuvent être appelées par un autre thread
(et même, resume doit être appelé depuis un autre thread
si on veut que l'action soit effectuée !!).
L'utilisation de ces méthodes n'est plus conseillée en
Java 1.2. En effet, le suspend peut engendrer des deadlocks. Il suffit
que le thread ait acquis le vérou sur un objet, puis se suspend.
Personne ne peut accéder à l'objet. Si le thread censé
appeler resume() essaie auparavant de prendre contrôle de l'objet
vérouillé, il se bloque, et resume( ) ne sera jamais appelé.
Il vaut mieux demander au thread de regarder une variable indiquant l'état
du thread (suspendu ou actif) (cf stop).
Sleep : permet de passer un thread dans l'état "endormi"
pendant au moins quelque temps (précision ms ou ns).
Attention, une exception InterruptedException peut être levée.
Le bloc catch correspondant sera alors exécuté lorsque le
thread aura à nouvea la main.
C'est une méthode statique, qui s'appelle sur le thread courant.
Synchronize :
Tout objet possède un vérou. A tout moment, le vérou
est contrôlé par au plus un thread. Le vérou donne
accès au code "synchronize" de l'objet. Un thread qui veut
exécuter le code "synchronize" d'un objet doit tout d'abord acquérir
son vérou. Si l'objet est déjà vérouillé
par un autre thread, le thread appelant passe dans l'état "bloqué"
en attente de la libération du vérou requis (il repasse alors
dans l'état "ready")
On peut synchroniser une méthode entière ou simplement
une partie de code (il faut alors spécifier l'objet qu'on synchronise
- on peut mettre this) :
public synchronized void myMethod(String
s){
... // code synchronisé
}
public int aMethod(){
... // code non
synchronisé
synchronized(anObject){
... // code synchronisé
}
// code non synchronisé
}
Moniteur
Un moniteur est un objet susceptible de bloquer
ou de redémarrer des threads. C'est un objet qui comporte du code
synchronisé (synchronized, wait, notify...)
Wait / Notify : ne doit être appelé que dans du
code synchronisé. Wait passe le thread dans l'état "En attente"
et libère le lock sur l'objet. Notify prend un des threads "en attente"
et le passe dans l'état "prêt". La liste d'attente des threads
"en attente" est gérée par l'objet qui a appelé Wait.
Donc, notify doit être appelé sur le même objet (moniteur).
NotifyAll
passe tous les threads en attente dans l'état prêt.
Wait peut envoyer des InterruptedExceptions.
Interfaces graphiques
Les différents composants visuels
L'appel aux méthodes setSize et setBounds ne marchera généralement
pas car la taille des composants est régie par un layout manager.
Seul le canvas pourra être dimensionné facilement par ces
méthodes.
Radio bouton
Il faut grouper plusieurs cases à cocher dans un CheckboxGroup.
CheckboxGroup cbg = new CheckboxGroup();
Checkbox c1 = new Checkbox("ludo",false,cbg);
Checkbox c2 = new Checkbox("axelle",false,cbg);
Checkbox c3 = new Checkbox("toto",true,cbg);
Un seul radio bouton peut être sélectionné à
la fois. Si un autre est sélectionné, cela déselectionne
le premier.
La classe CheckboxGroup n'est pas un composant (ie, pas de restrictions
pour l'organisation interne de ses membres).
Rem. Un checkbox n'est pas déformable (il ne sera pas influencé
par le layout manager quant à sa taille).
FileDialog : une classe qui permet d'ouvrir ou de sauvegarder
un fichier. La fenêtre qui apparait est modale.
new FileDialog(myframe,"Un titre",FileDialog.LOAD);
List( <nombre de lignes visibles>, <multiple selection
booléan>);
ScrollPane : une classe qui permet de faire apparaitre des scrollbar
si nécessaire.
Scrollbar : orientation, initial value, slider size, min value,
max value
TextField, TextArea : deux sous-classes de TextComponent.
Dans un espace cartésien, les coordonnées données
seront toujours y puis x. Exemple : TextArea(y,x)
Les tailles sont en taille de caractères, et peuvent donc être
légèrement différentes d'une font à une autre.
Si on tape du texte hors de la taille du composant, il scrolle automatiquement
vers la droite.
Les conteneurs
Les conteneurs dérivent de Components.
Applet : changer la taille d'une applet n'est pas forcément
autorisé par le browser. La taille de l'applet est généralement
spécifiée dans le tag HTML. Les applets ont une fonction
init().
Les applets sont dérivées des Panels, qui eux mêmes
dérivent de Container.
Frame : il s'agit d'une fenêtre indépendante, décorée
à la manière du système d'exploitation sous-jacent.
Ces fenêtres ont un titre (éventuellement vide). Au départ,
un frame n'a ni taille ni n'est visible : il faut donc faire appel à
setSize()/setBounds() et setVisible(true).
setVisible(false) cache le frame, mais ne le détruit pas. Pour
détruire un frame (et les ressources associées), il faut
appeler dispose().
Les browsers ont des restrictions sur l'affichage de frames au sein
d'applets (titre untrusted) pour des raisons de sécurité
(empêcher de transmettre des informations).
Un frame dérive de Window qui dérive lui même de
Container.
Panel : sert à grouper des composants au sein de frames
ou applets. Un panel dérive de Container.
Dialog : popup-window qui accepte les entrées utilisateur.
Modale ou non modale. (layout manager par défaut= Border Layout).
Dérive de Window, qui dérive elle même de Container.
Les menus
Il faut :
-
créer un menu bar et l'attacher au frame (new MenuBar, setMenuBar)
-
créer et remplir le menu : on peut y mettre des menus, check-box
menu items, séparateurs, et des menus items. (MenuItem(String),
CheckboxMenuItem(String) ...)
-
attacher le menu au menu bar. (add, addSepartor(), ou setHelpMenu() )
Ne pas confondre les CheckboxMenuItem et les Checkbox !
Layout managers
Par défaut
Panel |
FlowLayout |
Applet |
FlowLayout |
Frame |
BorderLayout |
Dialog |
BorderLayout |
Généralités
Il existe 5 layout managers différents : GridBag, Border,
Card, Flow, et Grid. Ils implémentent tous l'interface java.awt.LayoutManager
Les layouts s'appliquent à des containers (un container contient
des composants ou d'autres containers).
setLayout(... layout...);
Tous les composants ont des "tailles préférentielles".
Il s'agit d'une taille, hors restrictions de layouts, qui permet d'afficher
au mieux le composant : on affiche juste ce dont on a besoin. Par exemple,
la taille préférentielle d'un bouton est juste suffisamment
large pour montrer son texte etc.
Lorsqu'on ajoute un layout manager au container, les composants s'affichent
avec une combinaison entre leur taille préférentielle souhaitée
et les restrictions du layout manager. (le layout manager prédominera).
Pour ne pas utiliser de Layout manager, il suffit de faire setLayout(null),
et alors le setBounds fonctionnera correctement. mais ce n'est pas conseillé
!
FlowLayout
affiche les composants en rangées horizontales. S'il n'y a pas assez
de place pour les mettre en une rangée, les composants suivants
passeront dans une rangée en dessous.
On peut spécifier le style d'alignement souhaité : gauche,
droite, centre.
new FlowLayout(FlowLayout.RIGHT);
ce layout respecte toujours la taille préférentielle
des composants.
GridLayout
divise son territoire en une matrice de rangées et de colonnes (new
GridLayout(rows,columns)). Chaque cellule a la même taille, et chaque
composant remplit complètement (au maximum) sa cellule.
ce layout ignore complètement la taille préférentielle
des composants.
s'il n'y a pas assez de place pour tous les composants, de nouvelles
cellules seront créées.
BorderLayout
divise son territoire en cinq régions : nord, sud, est, ouest et
centre. Chaque région ne peut contenir au plus qu'un seul composant.
les composants placés en nord et en sud s'étendent horizontalement
suivant la taille de la fenêtre, mais gardent impérativement
leur hauteur préférentielle.
les composants placés en est et en ouest s'étendent verticalement
suivant la taille de la fenêtre, mais gardent impérativement
leur largeur préférentielle.
le composant placé au centre prend la taille restante. Le centre
est l'emplacement par défaut.
exemple :
setLayout(new BorderLayout())
...
add(<composant, container>, BorderLayout.SOUTH);
GridBagLayout
divise son territoire en un tableau de cellules, mais à la différence
de GridLayout, les cellules peuvent avoir des tailles différentes.
De plus un composant peut occuper une cellule... ou s'étendre sur
plusieurs cellules.
la classe GridBagConstraints permet de noter les informations sur la
position des composants.
l'utilisation de ce layout se fait :
GridBagLayout g = new GridBagLayout();
GridBagConstraints c = new GridBagConstraints();
...
g.setConstraints(theComponent, c);
add(theComponent);
Remarques
on peut utiliser les mêmes contraintes pour plusieurs composants,
mais attention éventuellement à bien remettre les valeurs
par défaut...
gridx, gridy = désigne la colonne et la ligne du composant.
GridBagConstraints.RELATIVE pour gridx passe au composant à
droite du dernier saisi. Pour gridy, on passe au composant en dessous du
dernier saisi.
par défaut, 0,0.
gridwidth, gridheight = désigne le nombre de lignes et
colonnes que le composant peut occuper.
.REMAINDER indique qu'il peut occuper tout ce qu'il reste.
fill = valable uniquement lorsque la taille requise pour le composant
est largement inférieure à la taille dont on dispose.
.NONE rien, par défaut.
.HORIZONTAL on étend le composant horizontalement sur sa cellule.
.VERTICAL
.BOTH
ipadx, ipady = padding interne du composant. Il indique les tailles
entre le "texte" et le bord du composant. Le padding s'applique deux fois
(à gauche et à droite, et en haut et en bas).
insets = padding externe du composant. Il indique les tailles
entre les bords du composant et sa cellule. Il faut construire un objet
Insets(top, left, bottom, right).
anchor = valable uniquement lorsque la taille requise pour le
composant est largement inférieure à la taille dont on dispose.
.CENTER par défaut, on met le composant au milieu de sa cellule.
.NORTH on met le composant en haut de sa cellule
.NORTHEAST, .EAST, .SOUTH....
weightx, weighty : permet de gérer l'espace en trop. Les
valeurs de ces champs sont comprises entre 0.0 et 1.0
Le composant reçoit un certain pourcentage d'espace "en trop"
lorsque la taille affichable est supérieure à la taille nécessaire.
A 0.0, tous les espaces en trop vont sur les bords, si bien que les
composants sont tjrs centrés au centre du container.
A 1.0, tous les espaces en trop sont utilisés sur les composants.
CardLayout
permet de montrer ou de cacher certains composants, suivant le temps.
les composants affichés prennent toute la place qu'ils ont dans
le container.
Swing et AWT
Les ajouts de Swing sont :
-
Les composants swing ont été écrits sans aucun code
natif.
-
On peut changer facilement le look and feel de composants.
UIManager.setLookAndFeel( UIManager.getCrossPlatformLookAndFeelClassName());
-
Diverses fonctionnalités : On peut mettre des images dans des boutons,
ou des labels, on peut faire des composants non rectangulaires.
attention :
-
les composants Swing ne sont pas thread-safe.
-
les composants Swing doivent être ajoutés dans des containeurs
Swing. (on ne peut pas mettre un JButton dans un Frame par ex).
-
les composants ne sont pas directement ajoutés dans un containeur,
mais dans un content pane contenu par le container (ex. on n'ajoute pas
directement un JButton au JFrame, mais on l'ajoute au getContentPane()
du JFrame).
Les événements
Un event listener est un objet à qui on confie la tâche d'écouter
les divers événements qui pourraient se produire.
Le modèle de programmation d'événements du
JDK 1.0 et 1.1 est incompatible, ils ne doivent pas apparaitre dans
le même programme.
On peut avoir plusieurs écouteurs pour le même événement,
mais
rien ne garantit l'ordre de réception des événéments.
Deux manières de mettre en place l'écoute des événements
:
créer sa propre classe qui implémente le listener,
et lier son listener au composant souhaité.
class MyClass implements ActionListener
{
public void actionPerformed(ActionEvent
ae){
}
}
et dans l'application :
Button btn = new Button("Ok");
MyClass listener = new MyClass();
btn.addActionListener(listener);
add(btn);
Suivant l'événement qu'on écoute, il existe différentes
classes de listener : KeyListener, MouseListener, ActionListener...
Le nom de la fonction à implémenter dépend de
l'événement écouté : actionPerformed, mouseClicked
...
Le nom de la fonction à ajouter au composant dépend également
de l'événement écouté : addXXXListener. |
gestion explicite de l'événement : l'événement
est traité dans le code même du composant qui le génère.
Il faut ensuite activer l'écoute de l'événement.
public class MyBtn(){
public void processActionEvent(ActionEvent
ae){
...
super.processActionEvent(ae);
}
public MyBtn(){
...
enableEvents(AWTEvent.ACTION_EVENT_MASK);
...
}
Le nom de la fonction à implémenter dépend de l'événement
écouté : processXXXEvent(XXEvent ae);
Le masque d'écoute d'événement dépend de
l'événement écouté : AWTEvent.XXX_EVENT_MASK. |
Les adaptateurs
Certaines classes (ex. WindowListener) demandent l'implémentation
de plusieurs méthodes. En conséquence, si on veut juste écouter
un événement (ex windowIconified), faire une classe avec
juste cette méthode ne compile pas : il manque le code des autres
méthodes.
Pour éviter de devoir le faire, les adaptateurs reprennent ce
code en faisant "rien du tout" pour chacune de ces fonctions. Il suffit
alors de créer une classe de XXXAdapter (ex. WindowAdapter) et de
modifier seulement la méthode (windowIconified) et cette fois-çi,
ça compilera.
class MyListener extends WindowAdapter
{
public void windowIconified(WindowEvent
we){
...
}
}
Paint
Généralités
Le dessin utilise la classe Graphics, qui désigne un contexte graphique.
Un contexte graphique est spécifique à chaque composant.
Le dessin s'implémente dans la fonction paint : public
void paint(Graphics g){ ... }
La primitive update( ) met tout le composant à sa couleur de
fond puis appelle paint en lui passant le contexte graphique. Lors de dessins
intensifs, il peut être plus intéressant d'appeler plutôt
repaint( ) qui poste une requête de update( ) plutôt que de
faire des tas d'update( ) successifs.
Ne pas mettre de code de dessin dans les gestions d'événements,
mais au contraire tout regrouper dans la méthode paint( ).
Primitives de dessin
Les signatures des fonctions de dessins sont généralement
du style :
coordonnée x, coordonnée y, largeur, hauteur (où
x et y sont pris dans un repère "cartésien", y étant
donc vertical vers le bas).
drawPolygon : dessine un polygone fermé, pour lequel on spécifie
un certain nombre de points (int y[], int x[], nb).
drawPolyline : dessine un polygone ouvert.
drawArc : dessine un arc d'ovale. Les angles sont mesurés en
dégrés, le 0 partant à droite, et 90 degrés
étant en haut.
drawString : dessine une string dont la baseline est spécifiée
en argument.
Pour remplir des formes, utiliser les primitives fillXXX.
Couleur
Si on la spécifie, il faut la mentionner avant de dessiner (cela
ne change pas la couleur des éléments déjà
dessinés).
On appelle un setColor sur une instance de Graphics.
Polices
Si on la spécifie, on faut la mentionner avant de dessiner.
Sur tous les systèmes on trouve les polices Serif, SansSerif
et Monospaced.
On peut récupérer les polices disponibles sur le système
par Toolkit.getDefaultToolkit().getFontList();
exemple : new Font("SansSerif",Font.BOLD,
24);
Clipping
restreint la région géographique surlaquelle un contexte
graphique peut agir. Par défaut, la clipping region contient tout
le composant.
setClip(x,y,width,height);
HTML et Applets
<BODY>
...
<APPLET CODE=... class WIDTH=x
HEIGHT=y>
<PARAM NAME=nom VALUE=valeur>
...
</APPLET>
</BODY>
Définition
Le marqueur APPLET comprend impérativement :
-
le tag CODE qui spécifie le nom de la classe qui débute l'exécution.
-
les tags WIDTH et HEIGHT qui définissent la taille de l'applet.
Attention, la page HTML peut contraindre ses tailles à d'autres
valeurs.
Optionnellement, on peut spécifier
-
CODEBASE : permet de spécifier un répertoire ou un http de
base où aller chercher les éléments.
-
ALT : texte alternatif affiché si l'applet ne peut se lancer.
-
HSPACE, VSPACE : contrôle la position de l'applet dans la page HTML.
-
ALIGN (LEFT, RIGHT) : alignement.
-
NAME : on peut nommer l'applet, ce qui peut servir pour communiquer avec
d'autres applets de la même page.
-
ARCHIVE permet de spécifier un ou des .jar qui contiennent plusieurs
classes. Les différents JAR sont séparés par des virgules.
Pour CODE et CODEBASE, on peut spécifier des URL ou des noms de
répertoires.
Paramètres
Pour passer des paramètres à l'applet, il faut le faire par
la mention PARAM
-
le nom est NON case sensitive
-
la valeur est CASE sensitive, et elle est comprise en tant que String.
Récupérer les paramètres dans l'applet : il
faut utiliser getParameter(nom), ce qui retourne une String (ou null).
Input and output
Généralités
Tout ce qui concerne l'input ou output se trouve dans java.io
Cela peut lever des IOException.
Les chaines représentées en interne sont en Unicode (16
bits). Pour l'input et l'output, on utilise l'UTF (taille variable).
File
La classe File ne fait qu'encapsuler une string représentant un
fichier ou un répertoire.
Il n'y a pas de création de fichier/répertoire.
Le garbage collecting d'une instance de file ne détruit pas
le fichier/répertoire
Cette classe permet de :
-
tester l'existence d'un fichier/répertoire (exists)
-
travail sur les chemins (getAbsolutePath,
getCanonicalPath, getName, getParent)
-
savoir si on a affaire à un fichier/répertoire (isFile, isDirectory)
-
lister les fichiers et répertoires (list, l'instance de File doit
être un répertoire).
-
récupérer des attributs (canRead, canWrite, length)
-
effacer un fichier : delete
-
créer un répertoire : mkdir
-
renommer un fichier / répertoire : renameTo
RandomAccessFile
La classe RandomAccessFile permet d'accéder aux fichiers
: recherche de position dans le fichier, lecture, écriture.
Ce modèle d'accès de fichiers est totalement incompatible
avec le modèle stream / reader / writer.
Il n'y a pas de création de fichier/répertoire, et
le garbage collecting ne détruit pas le fichier/répertoire.
On peut :
-
ouvrir un fichier en lecture ou lecture / écriture : RandomAccessFile(String
file, String mode) ou RandomAccessFile(File file, String mode);
-
où le mode est "r" ou "rw".
-
gérer la position dans un fichier : getFilePointer, seek, length
-
lire :
-
read : lecture du prochain byte, -1 à la fin du fichier
-
read(byte dest[]) : lecture de
bytes pour remplir le tableau dest (si possible). Retourne le nb de bytes
lu (ou -1).
-
read(byte dest[], int offset, int len)
: lecture de len bytes à partir de l'offset et mis dans le tableau.
Retourne le nb de bytes lus (ou -1).
-
byte readByte, int readInt, float readFloat
etc : lecture de types primitifs
-
écrire : même style de fonction avec write.
Streams
Dans les streams de bas niveau , on peut faire les opérations
:
-
création
-
read : read(), read(byte dest[]), read(byte dest[], int offset, int len)
-
write : idem mais en écriture
-
close : permet de libérer les ressources associées au fichier.
Ils sont dérivés des classes abstraites InputStream et
OutputStream.
Exemple : FileInputStream, PipedInputStream...
Les streams de haut-niveau (DataInputStream, BufferedInputStream...)
sont obligatoirement chainés à des streams de bas-niveau
(ou à un autre stream de haut niveau lui même chainé
à un stream de bas niveau).
Ils sont dérivés des classes FilterInputStream ou
FilterOutputStream (elles mêmes dérivées de InputStream
et OutputStream).
Il faut fermer ces streams dans l'ordre inverse de leur chainage.
La classe BufferedInputStream en particulier permet de lire de gros
blocs de données.
La classe DataInputStream permet de lire directement les données
attendues : readUTF(), readInt()...
Reader / writer
Ce sont des streams orientés lecture / écriture de chaines
Unicode.
Ils sont dérivés des classes abstraites Reader et
Writer.
Exemple : FileReader, PipedReader, StringReader.
On peut :
-
lire :
-
read() lecture du prochain caractère
-
read(char dest[])
-
read( char dest[], int offset, int len)
-
écrire : même type de méthodes
Les readers / writers de haut niveau doivent être chainés
à des readers / writer de plus bas niveau.
Ils sont également dérivés de Reader et Writer.
Exemple : BufferedReader, LineNumberReader, PushbackReader, PrinterWriter
...
Collections (interface Collection ... java.util)
Généralités
Une collection permet de regrouper des objets de manière générique.
On ne se préoccupe pas à ce niveau d'ordre, de duplication,
d'implémentation.
Exemples de collections : Vector, Hashtable, array...
La nouveauté en Java 1.2 est d'avoir mis en place les collections
framework : ie pour chaque collection on a une interface, une (des) implémentations
et des algorithmes. On permet à l'utilisateur d'une collection de
manipuler sa collection sans avoir à connaitre les détails
de son implémentation.
Toutes les collections dérivent de l'interface Collection, mis
à part les Map (qui dérivent de Map) qui ne sont pas de "vraies"
collections. L'interface Collection sert essentiellement à passer
une collection de manière générale (il n'existe pas
d'implémentation de la classe Collection).
On trouve également dans Collection :
-
les fonctions de manipulation standard : add, remove (n'enlève à
priori qu'une seule occurence), contains
-
des fonctions sur la taille : size, isEmpty
-
des opérations de travail de gros : addAll (union), removeAll, retainAll
(intersection), clear, containsAll
-
des opérations de conversions vers des tableaux : toArray
-
et un itérateur : iterator. Cela retourne un Iterator (interface).
L'Iterator est un peu comme une Enumeration, sauf qu'elle permet de manière
sure d'ôter des éléments dans une collection en même
temps que de la parcourir. L'interface Iterator contient les méthodes
next (donne le suivant élément), hasNext (suivant existe),
et remove (optionnel - permet d'effacer le dernier élément
retourné par next. N'appeler qu'une seule fois !).
De plus, toutes les collections ont un constructeur qui prend une collection
en argument :
exemple : List l = ArrayList(c);
Remarque de programmation : faites toujours référence
à votre collection en utilisant l'interface, et non pas le type
d'implémentation utilisé. Ainsi, vous pourrez facilement
changer d'implémentation de votre collection....
Exemple : Set s = new Hashset(); // où Hashset est une
implémentation de l'interface Set.
import java.util.*;
class Example {
public static void main(String
args[]){
Set s = new HashSet();
// une implémentation non ordonnée de Set.
s.add(new String("Axelle"));
s.add(new String("Ludo"));
s.add(new String("Croco Junior"));
//
System.out.println("s="+s);
for (Iterator i = s.iterator();
i.hasNext() ; ){
System.out.println("Parcours
: "+i.next());
}
}
}
List :
c'est une autre collection qui regroupe des éléments ordonnés,
dans laquelle on peut avoir des doublons. Les implémentations les
plus fréquentes sont ArrayList ou LinkedList. Ces collections utilisent
les méthodes définies dans l'interface Collection + des méthodes
concernant la position des éléments.
iterator retourne un ListIterator (possibilité d'accéder
aux index, et aux précédents : nextIndex, previousIndex,
hasPrevious...). Attention, le previous après une succession de
next retournera l'objet retourné par le dernier next. En effet,
il faut considérer que le pointeur se trouve tjrs entre deux élements.
Les différences avec la classe Vector :
-
Vector est une classe (une réelle implémentation).
-
get, set, add, remove (Vector : elementAt, setElementAt, insertElementAt,removeElementAt)
: les noms sont plus courts
-
l'appel aux méthodes prend d'abord la position en paramètre
: a.set(i,Objet) tandis que pour Vector, c'est a.setElementAt(Object,i);
Map
mappe des clés (uniques) vers des objets. Les implémentations
les plus courantes sont HashMap et TreeMap.
Les différences avec la class Hashtable :
-
Hashtable est une classe, tandis que Map est une interface.
-
la méthode contains() de Hashtable recherchait une valeur et non
une clé, elle a été renommée en containsValue()
dans Map.
-
on peut effacer tout en effectuant une itération.
Dans Map, on trouve les méthodes :
-
basic operations : get, put, containsValue, containsKey, isEmpty, size
-
bulk operations : clear, putAll
-
collection view : ensemble de clés keySet, les valeurs (pas un Set)
values, et les paires (clé, valeur) : entrySet (seul moyen pour
effectuer une itération de Map).. et méthodes pour travailler
sur les paires : getKey, getValue, setValue. (interface Entry).
Exemple :
for (Iterator i=m.entrySet().iterator();
i.hasNext(); ) {
Map.Entry
e = (Map.Entry) i.next();
System.out.println("clé:
"+e.getKey() + " valeur: " + e.getValue());
}
multimap : chaque clé est mappée vers plusieurs valeurs.
Cela peut s'implémenter en mappant chaque clé vers un objet
List.