J2SE 1.5 Tiger


précédentsommairesuivant

Les améliorations et nouveautés du langage

Un des leitmotivs de Sun et des utilisateurs de Java depuis l'avènement de cette technologie, est de simplifier un langage qui, à force d'enrichissement depuis le JDK 1.1, est devenu très complexe. Ceci est d'autant plus vrai que les programmes deviennent de plus en plus imposants par leurs designs et leurs fonctionnalités.

Avec Tiger, Sun a souhaité apporter une réelle réponse à ce problème.
La plus grande partie des nouveautés apportées au langage va d'ailleurs dans ce sens, en améliorant la lisibilité, la simplicité et la rapidité des développements.

Désormais, nul besoin de transtyper les types primitifs. L'autoboxing s'en charge pour vous.
Une nouvelle boucle for simplifiée fait aussi son apparition et l'ellipse est introduite pour représenter un nombre variable d'arguments, comme le fait la sortie formatée printf chère au coeur des développeurs C. Et ce n'est pas tout; un nombre impressionnant de ces petites choses qui changent la vie d'un développeur est au rendez vous.

Nous allons dresser dans ce chapitre une liste non exhaustive des améliorations les plus intéressantes.

L'autoboxing des types primitifs

Le langage Java s'appuie sur des types primitifs pour décrire les types de base. Il s'agitb essentiellement des types représentant des chiffres (byte, short, int, long, double et float), des booléens (boolean) et des caractères (char).
Cependant, en Java, tout se doit d'être « objet ».

Ainsi, un code comme celui qui suit lève une erreur de compilation avec J2SE 1.4:

 
Sélectionnez

List list = new ArrayList();
int myInt = 100;
list.add(myInt); //erreur de compilation ici (java.lang.ClassCastException)

Pour résoudre cette erreur; il faut encapsuler l'entier dans une classe référence (wrapper) comme la classe Integer :

 
Sélectionnez

list.add(new Integer(myInt)) ;

Ce type de transformation (aussi appelé boxing) peut rapidement s'avérer pénible. D'autant que le processus inverse (appelé unboxing) est nécessaire pour retrouver son type initial. Avec Tiger, cette conversion explicite devient obsolète. En effet, Tiger introduit l'autoboxing qui convertit de manière transparente les types de bases en références.

La table de conversion qui suit donne les correspondances entre type de base et wrapper (enrobeur) :

Type primitif Wrapper
boolean Boolean
byte Byte
double Double
short Short
int Integer
long Long
float Float
char Character

On peut ainsi écrire directement :

 
Sélectionnez

List list = new ArrayList();
int myInt = 100;
list.add(myInt);

Le compilateur se chargera de transtyper automatiquement la variable dans son type enrobé.

Les itérations simplifiées : la nouvelle boucle for

En java, une tâche récurrente et fastidieuse est l'itération sur une collection (au sens large c'est-à-dire autant sur un objet Collection que sur un tableau).

En effet, pour parcourir une collection, il faut instancier un itérateur sur celle-ci et pour chaque itération récupérer l'élément dans un objet conteneur. De plus, chaque élément doit être transtypé dans son type d'origine pour être utilisé en tant ue tel.

L'expression actuelle du for est assez puissante et peut être utilisée afin de parcourir des collections ou des tableaux. Cependant, elle n'est pas optimisée pour une simple itération, car l'itérateur ne sert qu'à obtenir les éléments.

 
Sélectionnez

List stringList = new ArrayList();
stringList.add("foo");
stringList.add("bar");
...
for(Iterator it = stringList.iterator(); it.hasNext(); )
{
  String currentString = (String)it.next();
  // Do something with the string...
}

Tiger introduit une manière supplémentaire d'utiliser le mot clé for, qui s'avère être beaucoup plus légère et facile à mettre en oeuvre.

Sa syntaxe est la suivante :

 
Sélectionnez

for(FormalParameter : Expression) Statement

Expression doit être un tableau ou une instance de la nouvelle interface java.lang.Iterable. L'interface java.util.Collection étend donc maintenant Iterable.

Avec cette nouvelle manière d'utiliser le for, nul besoin d'instancier explicitement un itérateur sur la collection.

De plus, le type des éléments contenus dans la collection est défini dans le corps de la boucle, évitant ainsi le recours au transtypage explicite.

 
Sélectionnez

for(String currentString: stringlist)
{
  // Do something with the string...
}

On le voit, cette nouvelle forme simplifie le développement et améliore la lisibilité des programmes.

Il existe cependant une limitation à celle-ci, il n'est pas possible de l'utiliser lorsque l'on souhaite modifier la collection, puisque aucun itérateur n'est accessible.

On peut aussi déplorer l'utilisation d'un mot clé ayant déjà une utilisation propre. Sun rétorque à cela que l'introduction d'un nouveau mot clé (foreach ?) est très (trop ?) coûteuse.

Les types énumérés type-safe : le nouveau mot clé enum

Un type énuméré est un type dont la valeur fait partie d'un jeu de constantes définies. Jusqu'ici, le type enum du C avait été omit en Java.

Une façon simple de créer un type énuméré est de définir une classe ne contenant que des constantes de type int :

 
Sélectionnez

public class Numbers
{
  public static final int ZERO = 0;
  public static final int ONE = 1;
  public static final int TWO = 2;
  public static final int THREE = 3;
  ...
}

Cependant, celle-ci comporte plusieurs désavantages :

  • Elle n'est pas « type-safe », c'est-à-dire qu'elle n'empêche pas d'affecter une valeur inconnue à la variable de type int, entraînant des erreurs à l'exécution, qui ne sont donc pas levées à la compilation.
  • Pour ajouter une valeur possible au type, il faut éditer la classe et tout recompiler.
  • L'utilisation de ces constantes est souvent préfixée par le nom du type, sans pour autant posséder d'espace de nommage propre au sein du programme.
  • Elle ne fournit pas de correspondance entre la valeur de la constante et sa description (comme il est d'usage de le faire pour une collection d'exceptions par exemple).
  • D'un point de vue purement objet, ce n'est pas une classe, car un des paradigmes objet définit une classe comme un ensemble qui encapsule des données et des méthodes permettant de traiter ou manipuler celles-ci. Ici, il n'y a pas de méthodes.

Un type énuméré « type-safe » est un type énuméré qui provoque des erreurs de compilation si l'on tente de lui affecter des valeurs non permises. Il existe bien entendu des patterns permettant de créer ces types énumérés, qui de plus contiennent des méthodes de traitement et leur associent des descriptions.

Le J2SE 1.5 fournit lui un type énuméré « type-safe » en standard via le mot clé enum:

 
Sélectionnez

public enum Numbers {ONE, TWO, THREE, ...};

Pour chaque type décrit dans l'énumération, une classe complète (valeur et méthodes utilitaires) est générée (à la différence des énumérations en C/C++ qui ne sont que des entiers).

De plus une classe supplémentaire est générée (Numbers.class ici), qui implémente l'interface Comparable et Serializable, qui contient des variables statiques correspondant aux valeurs, et qui définit ou redéfinit un certain nombre de méthodes ( values(), toString(), valueOf(String s) , hashCode(), compareTo(), ...).

Ainsi, nous utilisons les énumérations Java de la manière suivante :

 
Sélectionnez

Numbers num = Numbers.ONE;

Au lieu de :

 
Sélectionnez

int num = Numbers.ONE;

Cette construction présente les avantages suivants :

  • Elle est "type-safe".
  • Elle fournit un espace de nommage pour chaque énumération, évitant le recours à l'utilisation d'un préfixe dans les noms des constantes.
  • Etant des objets à part entière, chaque constante peut changer son implémentation sans obliger à changer le code source des classes qui l'utilisent et donc, sans obliger à recompiler.
  • Les valeurs, au lieu d'être de simples nombres, peuvent être des messages explicites.

De plus, le mot clé enum remplaçant le mot clé class, nous pouvons imaginer tout type de constructeur, permettant ainsi de créer des énumérations complexes :

 
Sélectionnez

public enum Numbers
{
  ONE("première occurrence ", 1),
  TWO("deuxième occurrence ", 2),
  THREE("troisième occurrence ", 3);
  
  Numbers(String description, int value)
  {
    this.description = description;
    this.value = value;
  }
  
  private string description;
  private int value;
  
  public String getDescription()
  {
    return this.description;
  }
  
  public int getValue()
  {
    return this.value;
  }
}

Les méthodes à arguments variables : l'ellipse

Quel développeur n'a pas été un jour confronté au problème du traitement d'un nombre variable de paramètres ?

Ce problème récurrent, bien que facilement contournable par le passage en paramètre d'un tableau de données dans les signatures des méthodes, nous oblige à spécialiser celles-ci. Ce qui n'est pas sans enlever une certaine généricité ou oblige à utiliser le polymorphisme, tout en compliquant les interfaces.

Par exemple :

 
Sélectionnez

void doSomething(Object[] args)
{
  // do something
}

// invoke the method
doSomething(new Object[]{"arg1", "arg2", "arg3"});

Tiger introduit l'ellipse "..." pour remédier à cela. Il s'agit d'un opérateur qui informe le compilateur que le nombre d'arguments peut être variable.

La précédente signature devient alors :

 
Sélectionnez

void doSomething(Object… args)
{
  // do something
}

// invoke the method
doSomething("arg1", "arg2", "arg3");

// invoke the method in an other way
doSomething("arg1", "arg2);

Cette notation, en plus d'apporter de la généricité, permet d'alléger légèrement le travail du développeur.

Attention : l'ellipse de Tiger ne s'utilise pas de la même manière que l'ellipse du C++.
En effet, l'ellipse du C++ prévoit que l'on peut passer n'importe quel type d'argument dans la signature. En Java, le type précédant directement l'ellipse définit le type des arguments susceptibles d'être ajoutés.

Nous allons en montrer un exemple concret dans le chapitre suivant qui concerne la nouvelle méthode de sortie standard formatée: le très connu printf(...).

La sortie standard formatée : la (nouvelle) méthode printf()

Tiger introduit une nouvelle méthode permettant de produire des messages formatés sur la sortie standard. Celle-ci vient en complément de l'actuelle méthode System.out.print().

Cette méthode s'appuie directement sur le mécanisme d'ellipse vu précédemment et fonctionne comme la très connue fonction printf(...) en C.

Avec l'actuelle méthode println(), nous avons :

 
Sélectionnez

double a = 5.6d ;
double b = 2d ;
double mult = a * b ;

System.out.println(a + " mult by " + b + " equals " + mult);

Avec le nouveau printf(...), nous aurons :

 
Sélectionnez

double a = 5.6d ;
double b = 2d ;
double mult = a * b ;

System.out.printf("%lf mult by %lf equals %lf \n", a, b , mult);

Les deux méthodes produisent le même message sur la sortie standard :

 
Sélectionnez

> 5.6 mult by 2 equals 11.2

Mais ce n'est pas tout. Cette nouvelle méthode permet bien plus, comme des formatages avancés :

 
Sélectionnez

double a = 5.6d ;
double b = 2d ;
double mult = a * b ;

System.out.printf("%3.2lf mult by %3.2lf equals %3.2lf\n", a, b , mult);

Le message standard sera dans ce cas :

 
Sélectionnez

> 005.60 mult by 002.00 equals 011.20	

La gestion des flux standards : le Scanner

L'API Scanner fournit des fonctionnalités de base pour lire tous les flux standards.
Cette classe peut être utilisée pour convertir du texte en primitives ou en Strings.
De plus, étant donné qu'elle est basée sur le package java.util.regex, elle offre un moyen simple d'appliquer des expressions régulières sur des flux, des fichiers de données, des Strings, ou tout autre objet implémentant la nouvelle interface Readable.

Par exemple, pour lire l'entrée standard, il suffit d'invoquer la méthode next() du Scanner :

 
Sélectionnez

Scanner scanner = Scanner.create(System.in);

try
{
  String s1 = scanner.next(Pattern.compile("[Yy]"));
  // only match Y or y
}
catch(InputMismatchException e)
{
  System.out.println("Expected Y or y");
}

scanner.close();

Ce bout de code simple permet de capter toutes les frappes de Y ou y sur le clavier si celui-ci est l'entrée standard.

Il est à noter que la méthode next() est bloquante si aucune donnée n'est disponible dans le flux.

Le Scanner peut aussi être utilisé à la place du classique StringTokenizer. Il peut utiliser tous types de délimiteurs grâce à l'adjonction des expressions régulières :

 
Sélectionnez

String input = "1 test 2 test red test blue test";

Scanner s = Scanner.create(input).useDelimiter("\\s*test\\s*");

System.out.println(s.nextInt());
System.out.println(s.nextInt());
System.out.println(s.next());
System.out.println(s.next());

s.close();

Ces instructions produisent la sortie suivante:

 
Sélectionnez

>1
>2
>red
>blue

Quiconque a déjà utilisé les StringTokenizer pour découper ou parser un fichier se rend tout de suite compte de la puissance du Scanner.

Les imports statiques

Toujours dans leur volonté de simplifier les codes source écrits en Java, les personnes en charge des spécifications de Tiger ont retenu celle des imports statiques.

La proposition vise à introduire une variante de l'import pour rendre visibles des méthodes et des variables statiques de la même manière que l'on importe des classes ou des interfaces.
Cette fonctionnalité permet de passer outre l'anti-pattern de l'interface constante (voir Effective Java de Joshua Bloch, le lead manager de Tiger).

La forme retenue pour la déclaration d'un tel import est la suivante :

 
Sélectionnez

import static TypeName . Identifier ;
import static TypeName . * ;

Son but principal, outre simplifier le code source, est de pallier au problème de l'espace de nommage déjà évoqué dans le chapitre sur les types énumérés (préfixage obligatoire) et de permettre d'ajouter des constantes et méthodes de portée globale (quasiment l'équivalent des #define en C, avec des différences tout de même).

Jusqu'ici la seule façon de faire était de les définir en tant qu'attributs statiques d'une classe conteneur, et de faire ensuite explicitement référence à cette classe en utilisant le nom de ce conteneur comme espace de nommage.

L'exemple typique est la classe Math de l'API standard dans laquelle toutes les méthodes et tous les attributs sont statiques:

 
Sélectionnez

Math.PI ;
Math.sin(x) ;
Math.cos(x) ;
...

Tiger assouplit l'utilisation de ces classes en autorisant les imports statiques. Cela permet de traiter les membres des classes statiques en tant que classes globales (pour le fichier courant) de la même manière que nous le faisons pour les classes classiques.

La conséquence directe est la suppression des préfixes obligatoires lorsque l'on fait référence aux membres statiques.

On peut ainsi écrire :

 
Sélectionnez

import static java.lang.Math.PI ;
import static java.lang.Math.sin();
// ou pour importer toutes les références statiques
import static java.lang.Math.* ;
...
PI ; // fait référence à Math.PI
...
sin(x) ; // fait référence à Math.sin()
cos(x) ; // fait référence à Math.cos()
...

Un bémol est toutefois à apporter à cette amélioration.

Si nous utilisons cette facilité sans prendre garde, nous pouvons arriver à des situations de conflits d'espaces de nommage. C'est pourquoi je recommande de ne pas utiliser cette fonctionnalité qui n'apporte pas de révolution et qui est sujet à introduire des erreurs (régressions en cas de reprise d'un code pré-existant).

D'autant plus que les éditeurs actuels savent très bien gérer cela.

Les imports statiques ne devraient être utilisés que pour des petites classes où il n'y a pas de conflit d'espace de nommage.


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

Ce document est issu de http://www.developpez.com et reste la propriété exclusive de son auteur [Lionel Roux]. La copie, modification et/ou distribution, de tout ou partie de celui-ci, par quelque moyen que ce soit, est soumise à l'obtention préalable de l'autorisation de l'auteur.
Tout manquement à cette licence donnerait systématiquement lieu à des poursuites, en accord avec la loi sur l'économie numérique (LEN).