L’actualité de l’OAMP
Java 1.5

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));
Integer i = (Integer)(v.get(0));

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 <Integer> essaiInteger;
   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>
  { private int p0;
    private E[] pile;
	
	  public Pile(E[] pile)
	    { this.pile = pile;
	      p0 = 0;
	    }
		
    public void empiler(E e) throws Exception
		{ if (p0 >= pile.length)
           throw new Exception("debordement");
		  pile[p0++] = e;
      }
	  
    public E depiler() throws Exception
	    { if (p0 <= 0)
	         throw new Exception("pile vide");
	      E e = pile[--p0];
	      pile[p0] = null;
	      return 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); 

Auto boxing et unboxing

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.