9/12/05
Java 1.5
Java 1.5 apporte de substantielles modifications au langage Java. Les évolutions précédentes touchaient aux API, sans modifier le langage lui-même.
Les nouveautés :
Généricité
Typage
Java fait la différence entre objets et types primitifs. Une méthode accepte en paramètres soit un objet, soit un type primitif.
Pour passer un type primitif en paramètre à une méthode qui prend un objet, il faut en faire un objet.
Exemple : int -> Integer
Vector v = new Vector(); |
|
v.add(new Integer(5));
|
Java 1.4 |
| v.add(new Double(3.141592)); i = (Integer)(v.get(1)); |
// provoque ClassCastException à l'exécution |
Le typage sert justement à filtrer ce genre d'erreur dès la compilation !
Typage 1.4
Pile générique
class Pile
{ private int p0;
Object[]pile;
public Pile(int n)
{ pile = new Object[10];
private p0 = 0;
}
public void empiler(Object o) throws Exception
{ if (p0 >= pile.length)
throw new Exception("debord");
pile[p0++] = o;
}
public Object depiler() throws Exception
{ if (p0 <= 0)
throw new Exception("vide");
Object o = pile[--p0];
pile[p0] = null;
return o;
}
} |
Pile pile = new Pile();
pile.empiler(new String("chaine"));
pile.empiler(new Integer(5));
pile.empiler(new Integer(7));
Object o = pile.depiler();
int k = ((Integer)o).intValue();
System.out.println(o.getClass() + " k " + k);
o = pile.depiler();
k = ((Integer)o).intValue();
System.out.println(o.getClass() + " k " + k);
o = pile.depiler();
k = ((Integer)o).intValue();
// ClassCastException
System.out.println(o.getClass() + " k " + k); |
Résultats :
class java.lang.Integer 7 k 7
class java.lang.Integer 5 k 5
java.lang.ClassCastException: java.lang.String
L'erreur est filtrée, mais à l'exécution seulement : la recherche de la cause est beaucoup plus difficile
Cette approche de la généricité exclut le typage !
Types génériques
La solution consiste à mettre en place un mécanisme semblable au passage de paramètres à une procédure :
Définition
Un type générique est un méta-type, qui remplace la totalité ou une partie des types connus.
Convention : on utilise une seule lettre en majuscule. Elle remplace un nom de type habituel :
Variable d'un type générique : Tableau formel : |
E e; E[ ] tab; |
Attention : il n'est pas possible de créer un tabelau générique !
Spécialisation d'une classe
| Passage d'un type formel E en paramètre à une classe : | class Essai<E> |
| Le type E est alors connu dans toute la classe | |
| Déclaration d'un objet de cette classe : | Essai |
| Création : | essaiInteger = new Essai |
Exemple
| Un type collection accepte des Object | Java 1.4 |
| Il peut être spécialisé pour n'accepter qu'un seul type donné | Java 1.5 |
| Vector <Integer> w = new Vector<Integer>(); | type paramétré |
Pile générique
class Pile<E> |
// MAUVAISE déclaration
Pile pile = new Pile(new Integer[10]);
// passe à la compilation
pile.empiler(new String("chaine"));
// produit ArrayStoreException
// BONNE déclaration
Pile<Integer> pile =
new Pile<Integer>(new Integer[10]);
// bloque à la compilation
pile.empiler(new String("chaine"));
pile.empiler(new Integer(5));
Object o = pile.depiler();
int k = ((Integer)o).intValue();
println(o.getClass() + " " + o + " k " + k); |
Il est impossible de mettre un type primitif dans la pile : barrière entre objet et type primitif (pour des raisons d'efficacité)
L'encapsulation d'un int dans un Integer est fait à la main de façon systématique. Par conséquent, le compilateur doit pouvoir le faire. Java 1.5 le fait.
| Emballage d'un type primitif dans la classe wrapeur correspondante : | auto boxing |
| Récupération du type primitif : | auto unboxing |
Avec la pile précédente :
pile.empiler(15);
k = pile.depiler();
System.out.println("objet : k " + k); |
API
Les API Java 1.5 utilisent types génériques et auto boxing.
Soit un Vector v :
Java 1.4 |
Java 1.5 |
Vector v = new Vector(); Integer i; |
Vector<Integer>
w = new Vector<Integer>(); |
v.add(new Integer(5)) // bon
v.add(7) // NON
v.add("abcd"); // accepté
i = (Integer)(v.get(0)); // bon
int k = i.intValue();
i = (Integer)(v.get(1)); // cast except. |
w.add(new Integer(5)); // bon
w.add(7); // bon
w.add("abcd"); // REFUSE
i = (Integer)(w.get(0)); // bon
int k = w.get(1); // bon |
Dans l'instruction v.add(7); le type primitif passé est automatiquement enveloppé dans un Integer ; le cast est inutile.
w ne peut accepter que des Integer ; il ne peut plus accepter des Object.
Compilation
Restriction
Vector<Integer> intVect = new Vector<Integer>(); // type paramétré Vector<Object> objVect = intVect ; // alias objVect.add(new Object()); // Integer i = intVect .get(0); // ce n'est pas un Integer ! |
Pour prévenir ce genre d'erreur, Java 1.5 interdit l'alias.
Ceci signifie que Vector<Integer> n'est pas un sous-type de Vector<Object>, bien que Integer soit un sous-type de Object !
Object o = new Integer(2); // correct Vector<Object> u = new Vector<Integer>(); // FAUX !
L'exemple donné ci-dessus ne passe pas à la compilation : l'alias est interdit.
Généralement, si Type2 est un sous-type de Type1,
AutreType<Type2> N'EST PAS un sous-type de AutreType<Type1>
Type de substitution
Problème : pour permettre l'utilisation de tout type, on peut écrire :
Essai<Object>
Ceci est un leurre !
La classe Essai n'acceptera que les Object, et non ses sous-classes.
Pour résoudre ce problème, java 1.5 propose le type de substitution (wildcard). Il est noté ?
Essai<?>
On peut accéder à ces éléments grâce au type Object, mais pas par un autre...
Sous-types génériques
Si seuls les sous-type d'un type donné sont acceptables, on peut limiter à cette hiérarchie par :
Essai<? extends Type>
Exemple
Dessiner une liste de figures géométriques
public static void dessiner(ArrayList<? extends Figure> figures)
{ for (Figure f : figures) // nouvelle boucle
System.out.println(f); // ici, il est impossible de remplir la liste
}
ArrayList<Figure> figures = new ArrayList<Figure>();
figures.add(new Figure("Rectangle")); // ici on peut remplir la liste
figures.add(new Figure("Cercle"));
figures.add(new Figure("Triangle"));
dessiner(figures);
Sortie
Rectangle
Cercle
Triangle
Méthodes génériques
Soit à recopier un tableau dans une Collection
public static void recopie(Object[] tab, Collection<?> c)
{ for (Object o : tab) // for each Object o in tab
c.add(o);
}
Ceci n'est pas accepté par le compilateur : le type Object n'est pas compatible avec le type inconnu ! Les deux paramètres ne s'accordent pas.
Définition
Une méthode générique est une méthode paramétrée par un ou plusieurs types génériques.
Le type paramètre est placé juste avant l'en-tête ; rien à voir avec le type de retour. Il permet de représenter une dépendance entre les types de plusieurs objets :
public static <T> void recopie(T[ ] tab, Collection<T> c)
{ for (T t : tab)
c.add(t);
}
Appel
Object[] tableau = new Object[3]; tableau[0] = new Object(); tableau[1] = new Object(); tableau[2] = new Object(); |
|
// ArrayList implémente Collection ArrayList col = new ArrayList(); recopie(tableau, col); System.out.println(col); |
// Vector aussi Vector vect = new Vector(); recopie(tableau, vect); System.out.println(vect); |
[java.lang.Object@ded0fd,
java.lang.Object@6a9d42,
java.lang.Object@7a84e4] |
[java.lang.Object@ded0fd,
java.lang.Object@6a9d42,
java.lang.Object@7a84e4] |
Utilisation
Lorsque des dépendances existent entre les types des obkets, il faut utiliser une méthode générique. Sinon, on se sert d'un type inconnu.
Transition
Attendre que tout le monde soit passé à 1.5 n'est pas une solution :
Il y a une certaine compatibilité. La mixité est possible. Quels sont les problèmes ?
Utilisation d'un paquetage 1.4 dans du code 1.5
Un type style 1.4 (sans paramètre) utilisé dans du code 1.5 est nommé type brut (raw type).
Le retour d'un raw type vers un type paramétré produit un unchecked warning. Il ne bloque pas la compilation. Le programmeur doit donc être prudent.
Les types bruts ressemblent aux type inconnus (?), mais le contrôle est plus lâche.
Utilisation d'un paquetage 1.5 dans du code 1.4
Arguments variables
La méthode main() peut recevoir un nombre quelconque d'arguments. Pour les lui passer, il faut les emballer dans un tableau :
public static void main(String[] args)
Java 1.5 introduit les arguments variables (en nombre)
public void methode(int n, boolean b, Object... parametres)
Exemple
public static void m(int n, Object... parametres) { System.out.println("m " + n); for (int i = 0; i < parametres.length; i++) System.out.println(parametres[i]); System.out.println(); }
Appel
m(4, "premiere", "seconde");
m(12, "chaine une", "chaine deux", "chaine trois", "chaine quatre");
String[ ] tab = {"chaine une", "chaine deux",
"chaine trois", "chaine quatre"};
m(24, tab); // produit un warning !
Boucle pour
La boucle pour présente des inconvénients :
Amélioration
void effacer(Collection<TimerTask>c)
{
for (Iterator<TimerTask> i = c.iterator(); i.hasNext(); )
i.next().cancel();
}
La nouvelle forme s'inspire de la notation mathématique : forAll...
for (Type t : variable)
se lit : pour tout Type t dans variable...
Exemples
parametres est une Collection d'Object
for (Object o : parametres)
System.out.println(o);
int sum(int[] a);
remplissage de a...
int result = 0;
for (int i : a)
result += i;
Utilisation
La boucle for each n'est pas utilisable partout. Si on doit accéder directement à l'itérateur, elle est inutilisable.
Enumérations
C'est le paradigme de Pascal (type saison = {printemps, ete, automne, hiver}). Cette facilité n'existait pas en Java. Il fallait définir des constantes entières, c'est-à-dire faire un codage manuel . Pour définir des couleurs par exemple :
public final int ROUGE = 1; public final int VERT = 2; public final int BLEU = 3;
Définition
Une énumération est un ensemble d'identificateurs affecté à une variable :
enum Couleur = {ROUGE, VERT, BLEU};
Exemple
public enum Couleur = {COEUR, PIQUE, CARREAU, TREFLE}
public enum Valeur = {AS, ROI, DAME, VALET, DIX, NEUF, HUIT, SEPT}
private Carte(Couleur couleur, Valeur valeur)
{ this.couleur = couleur;
this.valeur = valeur;
}
boucle :
for (Valeur valeur : Valeur.values())
Attention :
new Carte(valeur, couleur) provoque une erreur de compilation. Ce serait une erreur d'exécution avec les constantes entières.
Enumérations complexes
Autre forme
public enum Operation
{ PLUS { double eval(double x, double y) { return x + y; } },
MOINS { double eval(double x, double y) { return x - y; } },
FOIS { double eval(double x, double y) { return x * y; } },
DIV { double eval(double x, double y) { return x / y; } };
// Do arithmetic op represented by this constant
abstract double eval(double x, double y);
}
Cette présentation regroupe le nom de l'élément et l'opération qu'il contrôle.
Sans lui, il faut écrire une méthode séparée, et la mettre à jour en même temps que l'énumération (ajout d'un nouvel élément).
L'utilisation de cette forme évoluée supprime donc un risque sévère d'erreur.
EnumSet et EnumMap
Le premier est une implémentation d'ensemble. Le second permet une association de clé d'accès.
Import statique
C'est une facilité pour éviter des répétitions désagréables.
On importe des classes, ou des paquetages : import ...
Ceci concerne les éléments publics de la classe (son interface).
L'import static permet de simplifier la notation :
import static java.lang.Math;
permet d'écrire PI au lieu de Math.PI.
Les imports statiques améliorent la lisibilité... à la condition d'en user avec parcimonie !
Si on abuse de cette facilité, on provoque des ambigüités (collisions de noms), ce qui oblige à changer les noms.
Annotations
Ce sont des marques placées dans le programme :
@identificateur
Elles sont traitées par un précompilateur. Elles ne modifient pas le source.
Elles produisent des données relatives au programme. Leur source est dans le programme lui-même, donc meilleure adéquation.