Rappels

Les principes fondamentaux de la syntaxe sont repris dans les chapitres suivants (les répétitions avec des chapitres précédents sont inévitables)

Syntaxe : principes des déclarations de premier niveau

En principe un système de génération de code pourrait organiser ses unités de compilation de la manière qui lui semble la plus propice. Dans le cas d’une organisation de fichiers .java dans une hiérarchie de répertoires un tel fichier contient au plus une déclaration de classe publique de premier niveau.

Le nom du fichier doit correspondre au nom de cette classe publique (facilite le travail de lien entre le .class et le .java voir, par ex., l’option -sourcepath de javac).

Il peut y avoir d’autres classes de portée package dans ce fichier (des classes fonctionnellement dépendantes de la classe principale et qui ne sont pas marquées public). Il peut aussi ne pas y avoir de classe public dans le fichier, une classe principale de portée package donne alors son nom au fichier.

[Note]

Les unités de compilation peuvent permettre de définir d’autres types (interfaces, énumérés, annotations) mais ces cas seront traités ici séparément.

[Note]

Lorsqu’on utilise des fichiers texte comme unité de compilation (c’est le cas le plus général) il convient de faire attention aux caractères accentués. Bien que, normalement, les fichiers source ne sortent pas du cadre de l'équipe qui les crée il peut être utile de spécifier de manière explicite le mode de codage du fichier texte (par ex. format texte UNIX -un seul caractère de séparation de ligne- et codage UTF8 ou ISO8859-15 -codage standard de la région d’origine- … éviter des formats exotiques on non-standard -comme cp1252-)

Plan d’ensemble d’une déclaration de classe

<directive d'appartenance à un package>
<directives import>

<en-tête déclaration classe> {
   // MEMBRES

   // Membres d'instance
   <déclarations des champs -variables membres d'instance->
   < déclarations des classes membres>
   < méthodes d'instance>

   // Membres de classe
   < variables partagées >
   < classes, interfaces, énumérés, annotations membres de classe>
   < méthodes statiques >

   // Blocs  de premier niveau
   < Blocs statiques>
   < Blocs d'instance >

   // CONSTRUCTEURS
   <constructeurs>
}

Toutes les déclarations de premier niveau (sauf les blocs spéciaux) peuvent éventuellement, faire l’objet d’une annotation (qui précède l’entité annotée). Les commentaires javadoc peuvent également précéder ces mêmes entités.

A l’intérieur du code de la classe l’ordre des déclarations est (presque) sans importance et fera l’objet de conventions de codage propre à l'équipe projet.

Toutefois :

  • Pour les variables membres initialisées par une expression une évaluation a lieu dans l’ordre des déclarations. En cas d’interdépendance il vaut mieux avoir un ordre correct :
class W {
   int iy = ix ;
   int ix = 9 ;
}

Le code ne se compile pas ("illegal forward reference") - mais int iy = this.ix ; se compile mais donne un résultat erroné : 0 ! (voir spécifications l’usage du "nom simple" en rValue ne permet pas ici une référence anticipée; )

  • l’ordre des blocs de premier niveau compte également puisqu’ils seront exécutés dans l’ordre de leur définition.

Les packages: nommage, directive d’appartenance

Dans la mesure où elle est basé sur l’utilisation de "composants" l’exécution de code Java implique la collaboration (éventuellement dynamique) de codes venant de fournisseurs différents.

Il ne doit pas y avoir de conflit entre les noms de types : Il y a en fait deux types nommés List dans les librairies standard java …. mais ce n’est pas leur nom complet! Les types définis par les programmeurs ont un nom canonique qui comprend le nom d’un package.

Il est illégal de créer un type qui ne soit pas hébergé dans un package ( à moins que ce soit un test local ou une petite démonstration).

Un package représente un cercle de responsabilité avec un périmètre étendu sur plusieurs codes. Il représente un ensemble de fonctionnalités sur un domaine particulier et les codes d’un même package peuvent avoir des interdépendances ("cliques" de classes). Il n’est pas contre pas conseillé d’avoir des interdépendances fortes entre packages (un code dépend d’un package A et s’appuie sur des codes d’un package B lesquels s’appuient à leur tour sur d’autres codes de A).

Du fait de cette cohérence des codes d’un package on ne livre jamais un code isolé (même au niveau d’un changement de version de ce seul code): on déploie toujours un package complet. En fait on déploie un module qui est un ensemble de packages partageant des responsabilités apparentées.

On pourrait dire qu’un package est sous la responsabilité des programmeurs d’une même équipe : ces programmeurs se connaissent entre eux et ont des relations privilégiées. Les règles d'évolution des codes sont différentes des règles qui régissent les évolutions des APIs publique.

Les types définis par l’utilisateur doivent avoir un nom canonique qui est garanti unique. Donc un nom de package est composé :

  • obligatoirement d’un nom d’organisation (nom unique internet) -les librairies standard de Java ont des noms particuliers-
  • optionellement d’une hiérarchie d’unités organisationelles (les grandes organisations ne peuvent pas forcément gérer les noms de manière centralisée).
  • d’une hiérarchie fonctionnelle qui décrit le rôle particulier du package.

Par convention de codage on tend à mettre la hiérarchie des noms en minuscules et des règles particulières permettent d’adapter des noms de domaines aux règles de la syntaxe Java (par exemple: remplacement de "-" par "_"; idem si élément de hiérarchie contient un mot-clef java ou commence par un chiffre on rajoute "\_" devant). Si la présence de caractères accentués s’avère nécessaire une convention permettra de nommer des répertoires du système de fichier représentant ces packages. (par exemple: org/saisons/@00e9t@00e9 pour org.saisons.été)

Pour les correspondances entre répertoires et nom de package voir ultérieurement le chapitre sur l’organisation pratique des répertoires de développement et l’organisation en modules.

Exemples de noms canoniques de classe:

   com.acme.produits.trucs.CycloRameur
   fr.grosse_boite.division_financiere.calculs.swaps.Swap
   org.oecd.admin.Swap
   java.awt.Button
   javax.swing.table.TableColumn

Pour qu’une classe fasse partie d’un package la première ligne significative de son code source doit contenir une directive package.

package com.acme.produits.trucs ; // directive de compilation

public class CycloRameur {
   //code
   public CycloRameur(java.awt.Color color) {
      //code
   }
}

Le fichier package-info.java

Il est possible de documenter et d’annoter un package en créant un fichier de nom package-info.java et contenant une directive d' appartenance au package précédée des annotations et documentation correspondantes.

Pour la documentation javadoc penser à présenter une première ligne synthétique (terminée par un caractère ".") qui présente le rôle général du package (ce principe est également valable pour les autre commentaires javadoc).

Exemple de fichier package-info.java

/**
 * Provides the classes necessary to create an applet and the classes an applet uses
 * to communicate with its applet context.
 * <p>
 * The applet framework involves two entities:
 * the applet and the applet context. An applet is an embeddable window (see the
 * {@link java.awt.Panel} class) with a few extra methods that the applet context
 * can use to initialize, start, and stop the applet.
 *
 * @since 1.0
 * @see java.awt
 */
// ici annotation possible!
package java.lang.applet;

On peut également créer un fichier de documentation générale d’une application, d’une librairie, et, plus généralement d’un ensemble de packages au travers d’un fichier html overview.

Dans la mesure où les commentaires sont en français penser à préciser le mode de codage du fichier source lors de l’invocation de l’utilitaire (par exemple javadoc -encoding ISO-8859-15). D’autres options permettent de "localiser" (mettre entièrement en français) les éléments de la documentation : voir la documentation de javadoc.

directives import

Les noms canoniques de types sont ceux qui sont utilisés par le pseudo-code du binaire. On pourrait les utiliser dans le code source en écrivant:

   new com.acme.produits.trucs.CycloRameur(java.awt.Color.Blue) ;

Bien sûr nous préfererions écrire :

   new CycloRameur(Color.Blue) ;

et laisser le compilateur résoudre le "vrai" nom de la classe pour nous!

Pour permettre cela il faut utiliser des directives de compilation: celles ci se trouvent après la directive package et avant toute déclaration de type:

import <hierarchie_package>.<NomClasse> ;

import <hiérarchie_package>.* ;

import <hierarchie_package_et_classes>.<NomClasseMembre> ;
// membre d'instance ou membre de classe

import <hierarchie_package_et_classes>.* ;

Exemples d’utilisation:

import java.util.HashMap ;

import java.util.AbstractMap.SimpleImmutableEntry ;


import com.acme.produits.trucs.CycloRameur ;
import java.awt.Color ; // preferer les import explicites
import java.util.* ; // import implicite

public class ShowBiz {
   // quelque part dans le code
      new CycloRameur(Color.BLUE) ;
}
  • On n’a pas besoin d’utiliser import pour des types appartenant au package courant (le package de la classe courante). Même chose pour les classes du package fondamental java.lang.
  • l’usage de import explicite suppose que la classe nommée soit "accessible" au compilateur. On ne peut faire la même déclaration deux fois (erreur de compilation). Il n’est pas légal de définir localement un type de premier niveau qui porte le même nom "simple" qu’un type importé
  • un "import à la demande" (notation \*) permet , par contre, des déclarations redondantes. Il est également possible d’avoir un type présent dans le périmètre de cet import dont la déclaration est cachée par un import explicite ou même par un type local ou un type du package courant.
  • Il vaut mieux toujours préférer des imports explicites: la notation "étoilée peut conduire à des conflits (Il y a deux types nommés List et si on utilise import java.util.\* ; et import java.awt.\* ; il y a conflit de nommage). De plus on peut se tromper au niveau de l’expansion de la notation "*" : la directive import java.util.\* ; ne permet pas de résoudre le nom de java.util.logging.Logger (l’expansion ne concerne pas les sous-packages).
  • import est une directive de compilation: il n’y a aucun effet au runtime. Le compilateur cherche à charger le pseudo-code de la classe correspondante et vérifie s’il est utilisé correctement.

import static

Le compilateur peut aussi compléter automatiquement les noms des membres statiques:

import static <nom_canonique_classe>.<membre_static> ;


import static <nom_canonique_classe>.* ;

Utilisations:

import static java.lang.Math.PI ;
import static java.math.RoundingMode.*;
import static java.util.AbstractMap.SimpleImmutableEntry;

import com.acme.produits.trucs.CycloRameur
import java.awt.Color ; // preferer import explicite
import static java.awt.Color.* ; // nom canonique du type est nécessaire
import java.util.* ;

public class ShowBiz {
   // quelque part dans le code
      new CycloRameur(BLUE) ; // argument traduit en java.awt.Color.BLUE
}
[Attention]Attention

Ne pas abuser de cette facilité! Il serait peu opportun de donner aux méthodes statiques un aspect de fonction (qui n’existent pas en Java).

Déclaration de classe

/**
 *  Présentation javadoc.
 *  balises javadoc  (@quelquechose)
 */
<annotations>
<modificateurs> class <NomClasse> [ extends <AutreClasse>]
   [ implements <UneInterface> [, <AutreInterface>]]{
}
Nom
Doit être unique dans un package donné. Les règles générales sur les identifiants s’appliquent. Par convention on mettra systématiquement la première lettre en majuscule.
Modificateurs

Les classes de premier niveau peuvent être public ou package friendly (valeur par défaut). Les classes internes peuvent être en plus static, private, protected.

Une classe peut être marquée final et interdire toute opération d’héritage.

Une classe peut être marquée strictfp : toutes les opérations obéissent aux règles de calculs fp-stricts.

Une classe peut être marquée abstract : elle l’est obligatoirement si elle définit au moins une méthode abstraite ou si elle hérite d’une classe abstraite sans spécialiser au moins une des méthodes abstraites exigées par la super-classe (même règle si la classe implémente un interface).

Extends
La définition différentielle s’applique à partir d’une classe "accessible" au compile-time et non final (on ne peut pas non plus avoir un type énuméré ici). On ne peut faire une telle définition qu'à partir d’une seule classe-mère.
Implements
Une liste d’interfaces accessibles (et sans répétition possible du nom canonique). Il est possible d’avoir des interfaces partageant une méthode avec même signature et même type retour (la seule méthode réalisante sera considérée comme répondant au contrat des deux interfaces) mais ce n’est pas possible si le type de retour diffère.
public class UneChose  {
}

class Utilitaire {
}

public final class Valeur {
}

public strictfp class Calculs {
}

public abstract class Vehicule {
}

public class  UneClasse extends AutreClasse
   implements UneInterface, UneAutreInterface, EncoreUneInterface {
}

Exemple de javadoc :

/**
 * A class representing a window on the screen.
 * For example:
 * <pre>
 *    Window win = new Window(parent);
 *    win.show();
 * </pre>
 *
 * @author  Sami Shaio
 * @version 1.14, 10/18/06
 * @see     java.awt.BaseWindow
 * @see     java.awt.Button
 */

Champs: variables membres d’instance

Les champs sont des données ou des dispositifs qui constituent "l'état" de l’objet tel qu’il est accessible par du code Java. (Détail: certains objets sont des "monades" dont une partie de l'état est géré par un code de réalisation non accessible depuis Java: par ex. les Threads les dispositifs d’entrée/sortie, etc. typiquement ces objets ne sont pas serializable)

   /**
    * description javadoc
    * balises javaodc
    */
   <annotations>
   <modificateurs> <Type> <nom> ;
   <modificateurs> <Types> <nom> = <expression> ;
Nom

Les règles générales sur les identifiants s’appliquent. Par convention on mettra systématiquement la première lettre en minuscule.

Le nom doit être unique dans le contexte de la classe.

Type

Doit être "accessible" au moment de la compilation (pour permettre au compilateur d’opérer des vérifications d’utilisation).

Toute expression d’initialisations doit donner un résultat affectable au type déclaré

double val = 33 ; // promotion scalaire
Number nb = new Double(666.66) ; // sous-classe
List<Produit> liste = new ArrayList<Produit>() ; // type interface
Modificateurs
  • Les modifieurs de portée de premier niveau sont possibles: private, protected , public ( package friendly par défaut) .
  • final indique une constante. Celle-ci peut être directement initialisée, ou initialisée par un code de constructeur ou de bloc d’initialisation (variables blank final). Les initialisations par des constantes de compile_time doivent amener à déclarer le champs static final.
  • volatile : voir chapitre sur les Threads.
  • transient: champs non-automatiquement pris en compte par le processus de linéarisation (serialization). voir chapitre correspondant aux E/S objet.
   private int nombre ;

   int compteur :

   private final int taille ;
   // initialisée dans tous contructeurs ou par bloc d'instance

   public final long timeStamp = System.currentTimeMillis() ;

   private transient Client client ;

   private volatile boolean stopRequest ;

Exemple javadoc :

  /**
     * The X-coordinate of the component.
     *
     * @see #getLocation()
     */
    int x = 1263732;

A la création des objets tous les champs d’instance non explicitement initialisés sont initialisés avec des valeurs par défaut:

  • 0 pour les nombres
  • '\u0000' pour les caractères
  • 0. pour les flottants
  • false pour les booléens
  • référence null pour les objets. (cette référence pouvant être facilement testée il est recommandé de ne pas marquer un objet membre non initialisé par des valeurs bizarres comme "" -chaine vide- ou "non initialisé" , etc.)

CODES APPELANT :

   int hauteur = rectangle.height ;
   //
   if (this.client != null ) { °°° }
   // this implicite
   System.out.println(client) ; // this.client
   // dans constructeur avec paramètre prixHT ;
   this.prixHT = prixHT ;

Champs : variables partagées et constantes

Lors du chargement de la classe ( load time) les variables "de classe" sont initialisées. Soit leur valeur est mise à zéro selon les règles d’initialisations de variables membres soit leur valeur est le résultat d’expressions d’initialisations associées (voir ci-après le cas des blocs static).

Les codes de load time sont exécutés :

  • lors de la première utilisation d’une quelconque entité de la classe
  • si la classe est chargée par Class.forName(nomClasse) (cet appel permet donc de tester explicitement les initialisations correctes -en particulier à partir de ressources-).

On a ainsi des variables partagées:

   public static final double TAUX_TVA = 1.196 ;

   private static int compteurInstances ;

   static final BigDecimal TVA_NORMALE ;
   // initialisée dans un bloc static au chargement de la classe

Ces variables sont généralement manipulées au travers d’une qualification passant par le nom de la classe :

Math.PI
TVA.TVA_NORMALE

Il est toutefois possible de les accéder au travers d’une instance du type déclarant (mais risque d’apporter de la confusion) :

maTVA.TVA_NORMALE
[Avertissement]Attention

Une constante comme : public static final double TAUX = 1.196 ; est une constante de compile time.

Il s’ensuit que le compilateur est libre de mettre directement la valeur correspondante dans tout code qui utilise cette constante.

Le résultat est que si on modifie la déclaration de cette valeur et si on ne recompile pas tous les modules utilisateurs on obtient des valeurs qui ne seront pas modifiées (puisqu’elles sont "en dur" dans le code appelant). Préférer des énumérés pour ces valeurs: ils seront toujours consultés au runtime.

Les variables statiques sont initialisées au load-time dans un ordre particulier:

  • D’abord les initialisations de valeurs par défaut (zéro et ses équivalents) ou les constantes de compile-time sur les champs static final (il est impossible à un programme vicieux d’obtenir une valeur par défaut pour un champ static final qui est initialisé par une constante de compile-time). Ces opérations font partie de la phase de "préparation".
  • Ensuite les expressions d’initialisations (et les "blocs statiques" -voir ci-après-) sont exécutés dans l’ordre de leur déclaration. Ceci se produit dans une phase différente: celle des initialisations.

       static int X = 8 ;
       static int Y = X ; // vaut 8
    // Détails
    class Cst { // cas complexes à éviter!
       static int M = X ; /* _compiler_error_ */
       static int N = Cst.x ; // Ok mais N vaut zéro !
       static int X = 7 ;
       static int P = Cst.Y ; // Ok vaut 8 ! */
       static final int Y = 8 ;
    }
[Note]Détails
  • La combinaison static transient est autorisée … mais c’est une tautologie.
  • Comme les initialisations des variables partagées se font dans le contexte d’un ClassLoader il est parfaitement possible d’avoir plusieurs ClassLoaders actifs au sein d’une application complexe et donc d’avoir plusieurs versions de la même classe et donc, éventuellement, des valeurs qui évoluent différemment pour la "même" variable partagée qui existe sous différentes incarnations dans des ClassLoader différents!
  • Notons un autre type de "variable partagée" : les objets ThreadLocal et InheritableThreadLocal qui sont partagés par tous les codes d’un même Thread. (voir aussi les ThreadLocalRandom).
  • Un cas particulier d’objet final est celui pour lesquels les services sont rendus par la plate-forme sous jacente: on peut demander à la plateforme de changer cet objet sans changer la vision que Java en a. C’est le cas des "constantes" System.out, System.in, System.err qui, bien que marquées final sont modifiables par des méthodes comme System.setOut() (write protected fields avec comportement différent des "vrais" champs final au regard des optimisations de la JVM).
[Note]Le pseudo-champ statique .class

Les instances de la class java.lang.Class sont utilisées par les opérations de programmation dynamique ou pour accéder aux services du ClassLoader qui a chargé la classe (recherche de ressources par ex.).

Pour obtenir ces instances on peut invoquer la méthode Class.forName(nomClasse) ou invoquer la méthode instance.getClass(). On peut aussi les obtenir au travers d’une notation spéciale qui a l’aspect d’une variable statique:

Class<MaClasse> classeDeMaClasse = MaClasse.class ;
Class<int[]> clazzTab = int[].class ;
// extensions  de la notation
Class clazzint = int.class ;
Class<Integer> clazzint2 = Integer.TYPE ;

Constructeurs

Un constructeur est un code qui sert à initialiser un objet (après allocation implicite). Il porte obligatoirement le même nom que la classe (et il n’y pas de déclaration de résultat).

Pour Java un constructeur n’est pas une méthode et il n’est pas membre de la classe. (au niveau de la JVM c’est considéré comme une procédure de nom <init>).

Une classe qui n’a pas de constructeur est dotée d’un constructeur par défaut sans paramètres. Ce constructeur par défaut n’est pas généré si au moins une déclaration de constructeur apparaît dans la classe.

   /**
    * description javadoc
    * @param nom explication
    * @throws TypeException explication
    * @see référence
    */
   [<annotations>]
   [<modificateurs>] <Nom Classe> ( [<liste symboles arguments>] ) [<directives throws>]{
      // bloc constructeur
   }
Paramètres

Liste de symboles (<type valeur>) éventuellement préfixés par une annotation ou un marqueur final. Le type de chaque paramètre et sa position dans la liste fait partie de la signature caractéristique du constructeur (une classe peut avoir plusieurs constructeurs -surcharge- mais ne peut pas en avoir deux avec la même signature-).

Le nom des paramètres constituent une déclaration de variable en portée dans le code associé. On ne peut pas définir de variable de ce nom dans le code -mais il peut y avoir un nom identique entre un paramètre et une variable membre : confusion à éviter-. (détail: en fait il est possible d’avoir le nom d’un paramètre "caché" (shadowed) par un autre symbole défini dans une classe locale au code).

Cas particulier: le dernier paramètre peut être déclaré comme concernant un nombre variable d’arguments (voir ci-dessous).

Déclaration throws

directive de propagation d’une ou plusieurs exceptions. Obligatoire pour les exceptions controlées, facultatives pour les exceptions de runtime et les Errors. Cette obligation peut découler d’une présence dans le code d’une instruction susceptible de déclencher une telle exception (et ceci dans le code du constructeur ou dans un bloc d’initialisation d’instance -voir ci-dessous-)

Détail: il est légal de faire une déclaration d’exception controlée même si une telle exception n’est pas propagée par le code associé.

En cas de sortie intempestive du code du fait du déclenchement d’une exception l’objet n’est pas initialisé : donc si un constructeur est susceptible de propager une exception tous les constructeurs des sous-classes qui y font appel doivent obligatoirement propager au minimum la même exception.

Détail: si le code du constructeur passe une référence à une structure extérieure et ensuite déclenche une exception on peut quand même se retrouver avec un objet "incomplet" (voir situation analogue avec remarque sur synchronized).

Modificateurs

Les modifieurs de portée de premier niveau sont possibles: private, protected , public ( package friendly par défaut) . Le constructeur par défaut a la même portée que la classe dans laquelle il est déclaré (sauf pour les enums où il est implicitement private).

Un constructeur ne peut être static, abstract, final. native. Il ne peut être strictfp (c’est la classe corrrespondante qui doit l'être) ni synchronized. Sur ce dernier point une mise en garde: les spécifications considèrent que pendant l’exécution d’un constructeur la référence en cours d’initialisation n’est, en général, pas disponible au reste du code; toutefois il y a un risque dans certains cas particuliers lorsque this est rattaché à une structure externe par le code même du constructeur (par exemple: un composant graphique est rattaché à une hiérarchie de disposition). Dans ce cas il conviendrait d'être prudent pour éviter des accès concurrents sur l’objet non encore complétement initialisé.

Structure du code

Le code du constructeur est particulier en ce qu’il est divisé en trois zones:

  • zone 1 : appel de this(°°°) ou de super(°°°) - s’il n’y a rien le compilateur cherche à ajouter un super() implicite-.
  • zone 2 : pas de code ici mais un appel à toutes les expressions d’initialisations des variables membres (et aux blocs d’instance -voir ci-après-).
  • zone 3 : le reste du code du constructeur.

    Noter qu’un return sans argument est possible à l’intérieur du code.

    Si la classe comporte des variables d’instance final non initialisées (blank final variables) le compilateur vérifie que tous les constructeurs de la classe réalisent ces initialisations.

   public NomClasse() {
      °°°
   }

   public NomClasse(String arg1, int arg2  ) {
      °°°°
   }

   public NomClasse(String[] arg1) throws Exception1, Exception2 {
      °°°°
   }

   NomClasse() {
      °°°°
   }

Exemple javadoc :

 /**
     * Creates a <code>URL</code> object from the <code>String</code>
     * representation.
     * <p>
     * This constructor is equivalent to a call to the two-argument
     * constructor with a <code>null</code> first argument.
     *
     * @param      spec   the <code>String</code> to parse as a URL.
     * @exception  MalformedURLException  If the string specifies an
     *               unknown protocol.
     * @see        java.net.URL#URL(java.net.URL, java.lang.String)
     */
    public URL(String spec) throws MalformedURLException {

CODES APPELANT :

   TextField saisie = new TextField() ;
   Produit prod = new Livre(nISBN, titre, auteur, prix) ;

   // dans un code de constructeur
   this( id, nom, prix) ;
   // dans un autre code de constructeur
   super(id, prix) ;

Méthodes d’instance

Les méthodes déclarent du code exécutable qui peut être ensuite invoqué en passant des paramètres conformes à la signature de cette méthode. C’est au compilateur de choisir la méthode à exécuter lorsqu’il manipule du code appelant (ceci peut s’avérer complexe en cas de surcharge ou de type paramétré).

Les méthodes d’instance dépendent (en général) de l'état de l’objet courant.

   /**
    * description javadoc
    * @parm nom explication
     * @throws TypeException explication
    * @return explication
    */

   [<annotations>]
   [<Modificateurs>] <Type> <nomMéthode> ([<liste symboles arguments>] ) [<directive throws>]{
      <corps méthode>
   }

   [<Modificateurs>] abstract | native <Type> <nomMéthode> ([<liste symboles arguments>] ) [<directive throws>];


   [<Modificateurs>] void <nomMéthode> ([<liste symboles arguments>] ) [<directive throws>]{
      <corps méthode>
   }

   [<Modificateurs>] abstract | native void <nomMéthode> ([<liste symboles arguments>] ) [<directive throws>];
Nom

Les règles générales sur les identifiants s’appliquent. Par convention on mettra systématiquement la première lettre en minuscule.

Il est possible de définir deux méthodes de même nom mais pas de même signature (surchage -overloading-). On peut même avoir une méthode d’instance et une méthode statique de même nom. (les règles de la surcharge sont expliquées avec celles de la spécialisation pour simplifier l’exposé). On notera que le type retour ne fait pas partie de la signature (seul l’ordre et le type des paramètres compte en plus du nom de la méthode).

Résultat
void si la méthode est de nature procédurale. Sinon la méthode rend au code appelant un résultat du type déclaré (ou covariant : voir chapitre héritage). Si la méthode déclare un résultat toutes les sorties "normales" du code associé doivent se faire au travers d’un return avec un paramètre de type compatible (affectable au type déclaré).
Paramètres
Mêmes contraintes et propriétés que pour les constructeurs.
Déclaration throws
analogue à la directive de déclaration pour les constructeurs.
Responsabilité
modifieurs de portée de premier niveau sont possibles: private, protected , public ( package friendly par défaut) . -voir les règles spécifiques aux spécialisations de méthodes avec l’héritage-
Autres modifieurs
  • final : méthode ne peut être spécialisée (voir héritage)
  • synchronized : code protégé par un moniteur (voir Threads).
Méthodes abstraites

Les méthodes abstract ou native n’ont pas de "corps" de définition puisque celui-ci est défini dans d’autres codes. Une méthode abstract aura un code concret dans une sous-classe; une méthode native connaît une réalisation dans un autre langage (C, assembleur, etc.)

Ces méthodes ne peuvent être strictfp

Une méthode abstract ne peut être private, final , synchronized . (native et abstract sont exclusif l’un de l’autrre).

[Note]

Pour les champs, méthodes, constructeurs il est de coutume de mettre les modificateurs selon l’ordre suivant : <modificateurs de portée> , abstract static final synchronized native strictfp. (précisé par les spécifications mais non controlé par les compilateurs).

   public Prix getPrixTTC() {
      °°°
   }

   public final void setPrixHT(BigDecimal val) throws ExceptionValNegative {
      °°°
   }

   public synchronized void retirerDuStock(int nb) throws StockException {
      °°°
   }

   public String get(final int index) {
      °°°
   }

   public abstract double getTauxTVA() ;

   public native String[][]  getMatrice(int dim1, int dim2) ;
[Note]Détails
  • Méthodes et champs peuvent porter le même nom (il n’y a pas d’ambiguité syntaxique au niveau de l’utilisation). A déconseiller

    Une erreur classique :

    void NomClasse() // "faux" constructeur
  • Archaïsme toléré (mais fortement déconseillé par les spécifications) :

    String rendTableauStrings()[] { °°°° }

Pour les problématiques de spécialisation, surcharge, masquage voir le chapitre sur l’héritage.

Exemple javadoc :

 /**
     * Returns the character at the specified index. An index
     * ranges from <code>0</code> to <code>length() - 1</code>.
     *
     * @param     index  the index of the desired character.
     * @return    the desired character.
     * @exception StringIndexOutOfRangeException
     *              if the index is not in the range <code>0</code>
     *              to <code>length()-1</code>.
     * @see       java.lang.Character#charValue()
     */
    public char charAt(int index) {

CODES APPELANT:

   Produit instanceProduit = catalogue.get(reférenceProduit) ;
   Prix prix = instanceProduit.getPrix() ;
   try {
      instanceProduit.acheter(1) ; // méthode void
   } catch (ExceptionDisponibilite exc) {
      °°°°
   }
   °°°°°
   // dans un code de constructeur
   this.setPrix(prix) ;
   // dans un code de méthode
   String cr = super.toString() ;

Méthodes de classe

Ces méthodes correspondent à des service rendus en dehors de toute influence de l'état d’une instance.

Comme dans tous les codes static on n’a pas accès à this.

Les aspects type-retour, paramètres, directives de propagation d’exception sont similaires à ceux des méthodes d’instance.

On ne peut pas déclarer une méthode de classe abstract

Détail: par contre on peut déclarer une méthode de classe final! Dans ce cas le masquage ( hiding) par une autre méthode sera interdit (voir ce point avec les explications sur le masquage dans le chapitre sur l’héritage).

Si la méthode est synchronized le moniteur est associé à l’objet de type Class associé à la classe courante (voir chapitre sur les Threads).

   public static void main(String[] args) {
      °°°°
   }

   static String getConfigProperty(String nom) throws ExceptionConfiguration {
      °°°°
   }

   public static native double getCalcul(int parm, double parm2) ;

   public synchronized void changeConfig(String desc) {
      °°°°
   }

CODES APPELANT :

   String stVal = Config.getConfigProperty(saisie) ;

   Integer val = Integer.valueOf(33) ;

   // pas conseillé:: appel au travers d'une instance
   val.toString(66) ; // méthode statique .... confus!
[Note]

Les appels de méthode (que ce soit des méthodes d’instance ou des méthodes de classe) peuvent être récursifs: le code de la méthode peut se rappeler lui-même.

import static java.math.BigInteger.* ;

public static BigInteger ackerman(BigInteger mx, BigInteger ny) {
   // parametres doivent etre positifs
   if( mx.equals(ZERO)) {return ny.add(ONE); }
   if(ny.equals(ZERO)) { return ackerman(mx.subtract(ONE),ONE) ;}
   return ackerman(mx.subtract(ONE), ackerman(mx, ny.subtract(ONE))) ;
}

Une telle exécution est susceptible de déclencher une StackOverflowException (dépassement de la taille de pile). Il faut alors relancer la machine virtuelle avec une taille de pile plus importante (et, dans le cas présent, se limiter à des valeurs très petites pour l’appel initial de mx et ny).

Arguments en nombre variable

Il est possible de spécifier comme dernier paramètre d’un appel des arguments en nombre variable (varargs).

   <constructeur ou méthode> ( <paramètres ,> <Type> ... <nom>) {
      <code constructeur ou code méthode>
   }

Pour le code réalisant ce paramètre est un tableau (éventuellement de taille zéro: mais son existence est pratiquement garantie).

   public static int opEntière (char op, String ... args) {
      int res = 0 ;
      // autre facilité d'écriture possible ici
      for (int ix = 0 ; ix < args.length ;ix++){
         int arg = Integer.parseInt(args[ix]) ;
         switch(op) {
            case '+' : res += arg ; break
            °°°°// suite code
         }
      }
      return res ;
   }

Du point de vue des codes appelant on a plusieurs possibilités:

  • passer autant d’arguments de ce type que nécessaire
  • passer un tableau de ce type
  • ne passer aucun argument (génère un tableau de taille zéro pour le code réalisant)
   char op ; String[] tbStr ; String strVal ;
   °°°°// codes d'initialisations
   int res = MaClasseExemple.opEntière( op, tbStr)) ;
   °°°°// codes
   res = MaClasseExemple.opEntière( op, "1", "-23", strVal)) ;
   // LEGAL
   res = MaClasseExemple.opEntière( op) ;
[Avertissement]Détail

La présence d’argument variables peut considérablement compliquer le travail du compilateur chargé de choisir un code surchargé. (éviter donc des définitions risquant de conduire à des ambiguités)

La combinaison arité-variable/boxing pose aussi des problèmes très complexes pour la résolution des surcharges par le compilateur. Il est bien entendu conseillé d'éviter ce genre de conflit dont la résolution n’est pas immédiatement évidente à la maintenance du code.

Principes:

  • si la valeur est un scalaire : on recherche d’abord une signature avec exactement ce scalaire, ensuite vers un scalaire "supérieur" par conversion implicite, ensuite vers l’objet d’encapsulation exactement correspondant, et ensuite vers une super-classe de cet objet.
  • si la valeur est un objet on recherche d’abord une signature avec exactement cet objet, ensuite des super-classes de cet objet, ensuite un scalaire correspondant exactement, et ensuite un scalaire par conversion implicite.
  • la levée des ambiguités se fait en deux passes: on fait d’abord comme dans du java "classique" (sans autoboxing et sans varargs) et si on n’a pas trouvé de méthode correspondante on applique les recherches du 1.5. (mais des ambiguités encore possibles → erreur de compilation )
  • dans le cas d’arité multiple possible on cherche d’abord la méthode avec une arité prédéfinie.
  • Problème: que se passe-t’il si une liste vide apparait à l’emplacement d’une liste variable? Le compilateur génère un tableau avec 0 éléments mais la résolution de la signature devient difficile: quel est le type de rien? a priori le compilateur recherche le type le plus spécifique (mais des ambiguités sont encore possibles).

Autre aspect: on peut avoir des surcharges qui empiètent sur des définitions avec argument variable. A titre d’exemple voir la documentation de la classe java.util.EnumSet et les differentes versions de la méthode of.

Autre aspect: les types paramétrés posent problème dans les varargs:

public List<T> recherhe(Collection<T> col, T... args) {

va poser problème car on ne peut créer de manière fiable un tableau de type T : Le compilateur générera des émissions de Warning (voir l’annotation java.lang.SafeVarargs)

Blocs spéciaux

Bloc statique

Les variables statiques sont initialisés au load time. Mais on peut aussi écrire du code qui sera exécuté à ce moment (par exemple pour initialiser ces variables avec des codes complexes).

class PackCst {
   static final Properties DAOPROPS = new Properties() ;
   static {
       Class clazz = PackCst.class ;
       InputStream is = clazz.getResourceAsStream("config/dao.properties");
       try {
           DAOPROPS.load(is);
       } catch (Exception exc) {
      throw new AssertionError("resource config/dao.properties not properly deployed"+exc);
       }
   }
}

Un tel code s’inscrit dans un contexte statique donc pas d’accès à this mais on ne peut pas non plus avoir de return ni de propagation d’exception controlée. (Une exception dans ces blocs ou dans une expression d’initialisation donne une ExceptionInInitializerError).

Les blocs statiques sont exécutés en séquence dans l’ordre de leur définition et dans l’ordre avec les définitions des variables statiques initialisées (voir ce point avec les variables statiques).

Nota: ces blocs n’existent pas dans les définitions d’interfaces.

Bloc d’instance (instance initializers)

Les variables membres d’instances sont initialisées à un moment bien précis ("Zone 2" des constructeurs : après l’appel de this ou de super mais avant l’exécution de tout autre code). (voir aussi ordre des déclarations et des évaluations chapitre 8.3.2 des spécifications -point abordé précédemment-).

Il est possible d’associer des codes complexes à cette phase d’initialisation en codant un bloc d’instance (instance initialization block).

Un tel code permet par exemple :

  • de réaliser des initialisations complexes de champs (avec, éventuellement, des try/catch) en écrivant le code directement à proximité de la déclaration de variable elle même.
  • à mutualiser du code complexe entre constructeurs différents sans passer par une méthode private ou final (avec l’inconvénient de ne pas pouvoir passer des paramètres à ce code). -voir exemple ci-après-
  • à mettre du code de construction de l’instance là où on ne peut pas écrire un code de constructeur (classes anonymes!)
public class Formulaire extends Panel {
   private Label invite = new Label() ;
   private TextField question = new TextField(40) ;
   // autres composants initialisés à la construction

   /* DISPOSITION */ {
      // code de disposition des composants
   }

   public Formulaire (String inv, Repondeur rep) {
      super(new BorderLayout()) ;
      // codes initialisations exécutés ICI
      invite.setLabel(inv) ;
      // gestion des événements
   }

   public Formulaire (String inv, RepondeurIterable rep) {
      super(new BorderLayout()) ;
      // codes initialisations exécutés ICI
      invite.setLabel(inv) ;
      // gestion des événements différente
   }

}

Dans un tel code :

  • on ne peut pas avoir d’instruction return
  • si ce code propage une exception contrôlée alors tous les codes des constructeurs de la classe propagent cette exception. Toutefois cette règle ne s’applique pas dans le cadre de la création de classes anonymes: ce sera le code englobant la création de la classe anonyme qui aura en charge la capture de l’exception (le code utilisant l’instance disposera au compile time des informations nécessaires et n’a donc point besoin d’informations véhiculées par la directive de compilation throws).

Types définis dans une classe

On peut définir des types de second niveau.

  • classes membres d’instance
  • classes, interfaces, énumérés et annotations membres de classe.

Voir le chapitre correspondant sur les "types enchassés".

Syntaxe : les codes

Java est un langage dit "impératif" les codes d’exécution consistent en une série d’instructions (statement). Ces codes sont les corps des méthodes et constructeurs et les blocs spéciaux (blocs d’instance, blocs static).

Les codes peuvent être décomposés en:

  • déclarations de variables locales et déclarations de classe locales (ce dernier point sera abordé dans le chapitre sur les classes enchassées). Pour des raisons purement techniques liées à la syntaxe de Java on ne considérera pas ces déclarations comme des instructions proprement dites ( même si elles comportent des initialisations)
  • instructions "expressives" : affectations, incrémentations/décrémentations, appels de méthode, créations d’instance.
  • instructions d’assertion (voir chapitre sur les Exceptions)
  • blocs , blocs synchronized, blocs try/catch/finally , structures de contrôle (if/else, switch, while, do/while, for). -Détail: pour comprendre la grammaire il faut considérer ces blocs et structures comme des instructions-.
  • instructions de déroutement (return, throw, break, continue).
  • l’instruction vide (";" isolé).
   int x = 10 ; // déclaration ... pas une instruction
   x++ ; // instruction
   x + 1 ; /* _compiler_error_ not a statement */
   Integer.toString(x) ; // instruction même si ça ne fait rien!

Le compilateur détecte les instructions "injoignables" (unreachable statements) c’est à dire des instructions qui ne pourront jamais être exécutées.

   long res = System.currentTimeMillis() ;
   °°°
   return res;
   System.out.println(res) ; /* _compiler_error_ unreachable statement */
   if( arg < 0 ) {
      throw new IllegalArgumentException("negative arg") ;
      arg = 0 ;/* _compiler_error_ unreachable statement */
   }
   while(false) {
      System.out.println("hello") ; /* _compiler_error_ unreachable statement */
   }

Une exception toutefois à ce principe : la compilation conditionelle

   public static final boolean DEBUG = false ;
   °°°°
   if(DEBUG) {
      System.out.println("On est passé par là!") ;
      // CE CODE N'EST PAS COMPILE
      // PAS UNE ERREUR
   }
[Note]

Le compilateur n’est pas conçu pour détecter des instructions injoignables derrière un appel de System.exit(n)

Déclarations locales

On peut déclarer un symbole en tout point d’un code (pas nécessairement au début). Les variables locales doivent être déclarées et initialisées avant toute utilisation.

Les noms des variables ont une portée locale au bloc de définition. Les paramètres des constructeurs et méthodes sont également locaux (ainsi que les paramètres des blocs catch et les variables déclarées dans les en-têtes des boucles for). Les noms doivent être accédés par une désignation simple (pas de désignation qualifiée comme this.nom ou Classe.nom).

Les identifiants peuvent "obscurcir" (shadowing) un nom de membre et il faut alors lever les ambiguités:

public class Versement {
   private BigDecimal montant ;
   private Currency monnaie ;
   °°°°
   public Versement(BigDecimal montant, Currency monnaie) {
      this.montant = montant ;
      this.monnaie = monnaie ;
      °°°
   }

Les variables locales peuvent être déclarées final ; Les paramètres final ne peuvent faire l’objet d’une affectation dans le code réalisant; les autres variables locales final ne pourront faire l’objet que d’une seule affectation par cheminement possible du code.

   final int iz ;
   if ( x >=0 )  { iz = 1 ; }
   else { iz = -1 ; }

On ne peut pas définir un symbole local qui porte le même nom qu’un autre symbole local qui soit "en portée".

   for(int ix = 0 ; ix < tb.length ; ix ++ )
      int ix = Integer.parseInt(tb[ix]) ;
      /* _compiler_error_ ix already defined */
      °°°
   }

par contre:

   for(int ix = 0 ; ix < tb.length ; ix ++ )
      °°°
   }
   °°°
   for(int ix = 0 ; ix < tb.length ; ix ++ )
      °°°
   } // LEGAL : ix est uniquement en portée dans la boucle!

if/else

   if( <expression_booléenne> )
      <instruction>

   if( <expression_booléenne>)
      <instruction>
   else
      <instruction>
  • L’argument du if doit toujours être un boolean (ou un Boolean ou une expression qui renvoie une valeur logique). -Il n’est pas possible d’utiliser un int comme en C/C++ -
  • L’instruction peut être une instruction simple ou une instruction composite (bloc, structure : voir ci-dessus la définition des "instructions" pour la grammaire Java).
   Climatisation rapport = appareilMesure.getReport() ;
   if( rapport.temperature > 40 ) {
      loggerCourant.log(WARNING, "temperature trop élevée", rapport) ;
   }
   // autre partie du code toujours exécutée
   if( rapport.temperature > 40 ) {
      loggerCourant.log(WARNING, "temperature trop élevée", rapport) ;
   } else {
      loggerCourant.log(INFO, "rapport VMC", rapport) ;
   }
   if( rapport.temperature > 40 ) {
      loggerCourant.log(ERROR, "temperature trop élevée", rapport) ;
   }  else if (rapport.temperature > 30 ) {
      loggerCourant.log(WARNING, "attention à la temperature", rapport) ;
   } else {
      loggerCourant.log(TRACE, "rapport VMC", rapport) ;
   }

L’utilisation systématique d’un bloc comme instruction associée aux if/else simplifie la maintenance. Elle permet aussi une meilleure visibilité sur des constructions non-triviales:

if(quelquechose)
   if(autreChose)
      instruction1 ;
else //dangling else
   instruction2 ;

Ici les règles de la grammaire stipulent qu’en fait le else porte sur le dernier if (else sans soutien -dangling else-) -l’exemple est donc mal formatté-

if(quelquechose) {
   if(autreChose){
      instruction1 ;
   }
} else { // ambiguité levée
   instruction2 ;
}

switch

Une structure switch permet un aiguillage direct vers une partie de code

   switch(<expression_selection>) {
      <etiquettes1> <code1>
      <etiquettes2> <code2>
      .....
   }
  • L’expression de sélection rend un entier ou une valeur affectable à un entier int (donc byte, short, char et les types d’encapsulation Byte, Short, Character, Integer) ainsi que des Strings. Si la valeur rend null -dans le cas d’un objet d’encapsulation- une NullPointerException se déclenche. Il est également possible d’avoir une valeur de sélection qui soit de type énuméré (le compilateur va alors chercher l'ordinal de l’instance).
  • Les étiquettes sont de la forme : case <constante> : ou case <nomInstanceEnum> : ou +default: +.

    Si on utilise une constante elle doit être affectable au type du sélecteur (exemple: si le sélecteur est un caractère on peut avoir case 'A' : ; case 100 : -réduction de constante entière-; mais pas case -1 :!). On considère que si l’expression rend un objet d’encapsulation c’est le type scalaire correspondant qui est le type du sélecteur (donc pas d'étiquette avec un autre objet qu’une constante d'énuméré).

    Dans le cas où le sélecteur est d’un type énuméré, les noms de constantes énumérés sont utilisées comme étiquette. Il n’est pas strictement obligatoire de citer toutes les constantes de l'énuméré mais c’est vivement conseillé (dans le cas où il n’y a pas d'étiquette default)

    L'étiquette default (facultative) précise un branchement pour tous les cas non explicitement prévus.

  • Au moment de l'évaluation du switch l’exécution se branche au point qui correspond à la valeur du sélecteur (ou default si cette valeur ne fait pas partie de la liste des étiquette -et si default est précisé-). Le code qui suit est ensuite exécuté jusqu'à la fin de la structure.

    En règle générale on préfère limiter l’exécution au seul ensemble d’instructions associé à l'étiquette et interrompre le déroulement par un déroutement break (ou return, ou throw).

    0n peut forcer cette préférence en demandant au compilateur de nous signaler tout oubli de cette règle (option -Xlint:nofallthrough ).

   int annéeCourante = °°° ;
   int mois = °°°° ;
   //
   int nbJours = 0 ;
   switch(mois) {
      case 1 : case 3 : case 5 ; case 7: case 8 : case 10 : case 12 :
         nbJours = 31 ;
         break ;
      case 4 : case 6 : case 9 : case 11 :
         nbJours = 30 ;
         break ;
      case 2 :
         if( java.time.chrono.IsoChronology.INSTANCE.isLeapYear(annéeCourante)) {
            nbJours = 29 ;
         } else {
            nbJours = 28 ;
         }
         break ;
      default :    // ne devrait pas se produire
         curLogger.log(ERROR,  "mois illégal "+ mois) ;
         // voir notion de "assert"
   }

On peut utiliser comme étiquette des constantes de compile-time

   int code = °°° ;

   switch(code) {
      case DEPART:// constantes "final"
         .... // faire quelque chose
         break;
      °°°° // etc.
   }

Utilisation d’un énuméré:

   Urgence urgence ;
   °°°° ;
   switch(urgence) {
      case BASSE : // doucement, doucement ....
         break ;
      case NORMALE : // "just do it"
         break ;
      case HAUTE : // à faire tout de suite!
         break :
   }

while/do-while

Ces structures comprennent des instructions qui sont exécutées "en boucle" jusqu'à ce qu’un test d’arrêt fasse sortir l’exécution de la structure. Le test peut être en "haut" de boucle ou en "bas" de boucle.

Il est également possible de sortir de manière "abrupte" (par déroutement , ou déclenchement d’exception).

   // boucle test haut
   while(<expression boolénne>) <instruction>
   // boucle en test bas
   do <instruction> while( <expression boolénne> ) ;

Exemple:

// pour accumuler des chaînes
StringBuilder stb = new StringBuilder() ;
// pour chercher une console interactive associée à la JVM
Console console = System.console() ;
if(console != null) {
   String ligne ;
   // readLine rend null en fin d'interaction
   // pas d'IOException -mais cas spécial d'IOError-
   while (null != (ligne = console.readLine())) {
      stb.append(ligne.trim()).append("\n") ;
   }
}
// suite du code

Ici on a un "test haut": on teste avant de rentrer dans la boucle et en bas de boucle on revient au test haut avant de re-exécuter le bloc.

   ArrayList<Manager> conseil ;
   °°° // initialisations diverses
   Iterator<Manager> it = conseil.iterator() ;
   while(it.hasNext()) {
      Manager man = it.next() ;
      Departement dpt = man.getDépartement() ;
      °°° // autres codes
   }

do/while :

public class ControleurTemperature {
    // dans un code
   Capteur capteur;
        Climatisation infoCapteur ;
   // ... encore du code
        do {
            infoCapteur = capteur.rapport() ;
        } while(infoCapteur.temperature < TEMP_MAX );
   // bon il est temps de rafraichir

}

Ici on a un test en "bas de boucle": on commence par toujours rentrer dans le bloc. Le test permet de savoir s’il faut repartir dans une itération.

Boucles for

for( <initialisations>; <TestHaut> ; <opérations itération> )
   <instruction>

La structure for est une boucle à test haut. Chacune des trois zones de l’en-tête est optionelle :

  • Les <initialisations> définissent éventuellement des variables et les initialisent. Ces variables ne seront en portée dans l’en-tête qu’après leur définition et seront en portée dans l’instruction associée.
  • Le <TestHaut> est une expression rendant un boolean (éventuellement par unboxing). Tant que cette expression est vraie on exécute l’instruction (qui est plus généralement un bloc d’instructions). Si cette expression n’est pas présente le test est considéré comme vrai (et on ne pourra sortir de la boucle que par un déroutement).
  • Les <opérations d’itération> sont exécutées en "bas" de boucle (si elles sont définies).
public class AffichageTableau {

    public void afficher(String[] tableau) {
   for(int ix = 0 ; ix < tableau.length ; ix ++) {
      System.out.println(tableau[ix]) ;
   }
    }
}
   int[] tb = new int[6] ;
   for(int ix= 0; iy = tb.length ;
      ix < tb.length ;
         ix++, iy-- )
      tb[ix] = iy ; // instruction du for
   for(;;) { // boucle infinie "forever"
      // code
   }

Boucle foreach

Il s’agit ici d’une forme moderne de la boucle for

   for([final] <symbole> : <structure_donnée>) <instruction>

Dans laquelle:

  • La <structure_donnée> est soit un tableau soit un objet Iterable
  • Le <symbole> est en portée dans l’instruction et prendra successivement toutes les valeurs contenues dans la structure de données. Le type du symbole doit être "affectable" depuis le type des éléments de la structure de donnée.

Si on a un tableau , l’expression :

<Labels> for( [final] Type nom : <expression>) <instruction>

se traduit par

Type[] tb = <expression> ;
<Labels> // voir ci-après les étiquettes
for(int i = 0 ; ix < tb.length ; i++ ) {
   [final] Type nom = tb[i] ;
   <instructions>
}
public class AffichageTableau {

    public void afficher(String[] tableau) {
   for(String chaine : tableau) {
      System.out.println(chaine) ;
   }
    }
}

On voit avec la définition ci-dessus que si on écrit :

   Vehicule[] places ;
   Vehicule véhiculeAGarer ;
   °°° ;

   for(Vehicule garé : places) {
      if(garé == null) {
         // incorrect: ne modifie pas "places"
         garé = véhiculeAGarer ;
         return ;
      }
   }

Si on a un objet Iterable<Type> , l’expression :

<Labels> for( [final] Type nom : <expression>) <instruction>

se traduit par :

<labels>
for(Iterator<Type> it = <expression>.iterator() ;
   it.hasNext() ; ; ) {
   [final] Type nom = it.next() ;
   <instruction>
}
   ArrayList<Manager> conseil ;
   °°°° // initialisations diverses
   for (Manager man : conseil) {
      // si pas de membre null
      Departement dpt = man.getDépartement() ;
      °°°
   }

break, continue

On peut sortir "abruptement" d’une boucle par un return ou un déclenchement d’exception.

On peut également explicitement sortir de la boucle par un break;

   Console console = System.console() ;

   if(console != null ) {
      while( true ) { // boucle infinie
         String ligne = console.readLine())) {
         if( null == ligne) { break ;} //  on sort!
         console.printf("on a lu : ", ligne.trim() ) ;
      }
   }

Une autre instruction de déroutement est continue : elle est spécifique aux boucles et renvoie l’exécution en "bas" de boucle (donc aux instructions d’iteération dans le cas d’un for).

   while(null != (ligne = console.readLine())) {
      // ici on lit un commentaire
      if(ligne.startsWith("#")) { continue ;}
      // sinon on fait des tas de choses avec la ligne lue...
      // ......
   }

Etiquettes

Les <instructions> (au sens de la définition donnée précédement: instructions simples et assertions, blocs, blocs synchronized, blocs try , if, switch, boucles ) peuvent être précédées d’une suite d'étiquettes (label).

Exemples d'étiquettes:

LABEL5 :
GLASS :

Ces étiquettes peuvent servir de cible à des breaks étiquetés.

// l'exemple ne montre pas les try/catch
LECTURE : while (true) {
   for (int ix = 0; ix <tb.length; ix ++) {
      int lu = inputStream.read() ; //
      switch (lu){
         case -1 :
            break LECTURE ; //saut en dehors du while
         case '\n' : case '\r' :
            tb[ix] = '|';
            continue ;
         case '\t' :
            tb[ix] = ' ';
            break ;
         default :
            tb[ix] = lu
      } // switch
   } // for
} // while

L’exécution du break qui comporte un paramètre de type étiquette tente de transférer l’exécution en dehors de l’instruction étiquetée ("tente" car des transfert complexes, en particulier lorsqu’ils doivent obligatoirement passer par des blocs finally peuvent provoquer des sorties de code plus radicales).

Des dispositifs complexes (et rares) peuvent être mis en oeuvre:

OPERATION: {
   °°°° // codes
   while(condition) {
      °°°°
      if(! test) {break OPERATION ; }
      °°°°
   }
   °°° // codes après sortie "normale" de la boucle
}

Il existe également des continue etiquetés.

Dans ce cas les étiquettes cibles ne peuvent être associées qu'à des boucles. Si la boucle cible est un for seules les opérations d’itérations de la boucle cible seront exécutées (en d’autres termes si on a un continue FOR_EXTERNE; dans un +for les opérations d’itération en "bas" de la boucle la plus interne ne seront pas exécutées).

[Note]

A la limite on pourrait utiliser des étiquettes pour modifier la lisibilité d’un code:

public class PanierMarchand {
   °°°
   public void ajouter(Produit choix) throws ExceptionStock {
      choix.deStocker(1) ; // throws  ExceptionStock
      SI_STOCK_OK : {
         this.add(choix) ;
         °°°° ; // autres codes
      }
   }

Syntaxe: l’encapsulation automatique (autoboxing)

images/bluebelt.png

Voici un extrait de code java1.4 (utilisant la méthode statique format de la classe java.text.MessageFormat ) :

int nb ;long max ;boolean reussi ;
°°°°// code
String message = MessageFormat.format
    ("Le prof. Shadoko vous parle: essai {0} (sur {1} prévus) {2}!",
         new Object[] {new Integer(nb), new Long(max),
                    reussi?"réussi":"raté"});

Le résultat de System.out.println(message) quand la langue de l’utilisateur est le Français de France (java.util.Locale avec pour valeur fr_FR):

Le prof. Shadoko vous parle: essai 234 (sur 1 000 000 prévus) raté!

(on notera le séparateur des milliers spécifique au contexte culturel, avec le Locale en_US l’affichage donnerait 1,000,000).

Le même code en java 1.5 pourrait s'écrire :

String message = MessageFormat.format
   ("Le prof. Shadoko vous parle: essai {0} (sur {1} prévus) {2}!",
           nb, max, reussi?"réussi":"raté");

Dans cette méthode le nombre d’arguments qui suit la chaîne de définition du format est variable (puisqu’il dépend de cette description elle même). Jusqu'à présent le seul moyen de définir le service et de passer les arguments était d’utiliser un tableau d’objets.

Cette facilité d'écriture s’appuie donc sur deux nouveautés de la version 1.5 :

  • La définition de méthodes à arité variable (vararg)
  • La conversion automatique entre classes d’encapsulation et primitifs scalaires correspondants (autoboxing, auto-unboxing)

Au niveau du pseudo-code quelles sont les différences induites par les facilités d'écriture?

Extrait du pseudo-code de la version "ancienne":

11: ldc #4; //String Le prof. Shadoko vous parle: essai {0} (sur
                {1} prévus) {2}!
13: iconst_3
14: anewarray #5; //class java/lang/Object
17: dup
18: iconst_0
19: new #6; //class java/lang/Integer
22: dup
23: iload_1
24: invokespecial#7; // java/lang/Integer."<init>":(I)V
27: aastore
28: dup
29: iconst_1
30: new #8; //class java/lang/Long
33: dup
34: lload_2
35: invokespecial#9; // java/lang/Long."<init>":(J)V
38: aastore
39: dup

Le même extrait généré par le compilateur qui tient compte de la facilité d'écriture:

11: ldc #4; //String Le prof. Shadoko vous parle: essai {0} (sur
                {1} prévus) {2}!
13: iconst_3
14: anewarray #5; //class java/lang/Object
17: dup
18: iconst_0
19: iload_1
20: invokestatic#6; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
23: aastore
24: dup
25: iconst_1
26: lload_2
27: invokestatic#7; //Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
30: aastore
31: dup

Dans le second cas on a bien un tableau d’objet dans lequel le compilateur installe automatiquement des objets d’encapsulation de scalaires (Integer, Long) qu’il a généré automatiquement.

La différence est qu’il a plutôt employé des méthodes "fabrique" comme Integer.valueOf() en préférence au constructeur ( les constructeurs correspondants sont maintenant obsolete - Deprecated )

Les principes mis en oeuvre par le compilateur sont les suivants :

  • Lorsqu’une expression nécessite la présence d’un objet alors qu’un primitif scalaire est présent dans le code, alors on génère automatiquement un objet de la classe d’encapsulation correspondante (autoboxing)
  • Lorsqu’une expression nécessite un primitif scalaire alors qu’un objet d’un type d’encapsulation est présent dans le code, alors on génère sur cet objet la méthode d’extraction de valeur correspondant exactement au type encapsulé (auto-unboxing).

Le schéma général des opérations implicites:

type encapsulation Type "emballage" extraction
double x Double.valueOf(x) Double ref.doubleValue()
float x Float.valueOf(x) Float ref.floatValue()
long x Long.valueOf(x) Long ref.longValue()
int x Integer.valueOf(x) Integer ref.intValue()
short x Short.valueOf(x) Short ref.shortValue()
byte x Byte.valueOf(x) Byte ref.byteValue()
char x Character.valueOf(x) Character ref.charValue()
boolean x Boolean.valueOf(x) Boolean ref.booleanValue()

Relation "Affectable à" (pas d’isomorphisme!)

byte -> short -> int -> long -> float -> double
char -> int
Byte -> Number
Short -> Number
Integer -> Number
Long -> Number
Float -> Number
Double -> Number

L’application automatique de ces règles de transformation n’est pas tout à fait intuitive et demande de l’attention de la part du programmeur. En effet dans un sens les objets d’encapsulation n’héritent pas les uns des autres (un Float n’est pas "affectable" à un Double) tandis que dans l’autre sens les règles de conversion implicite s’appliquent sur les scalaires (un float est "affectable" à un double).

Object obj = 1.0 ; //OK Double généré
Number nbr = 1.0 ; // OK Double généré
Double dbl = 1.0 ; Integer ing = 1 ; // OK
Double dbl2 = 1 ; //NON: Integer non affectable à Double
int x = ing ; // OK
double y = ing ; //OK: extraction + conversion int->double
ing = ing + ing ; //OK: extractions+addition+encapsulation
byte bt = 22 ; Byte btt = bt ; // OK
btt++ ; // OK
btt = btt + 1 ; // NON !extraction + addition -> int

Noter également les règles de réduction implicite des constantes:

Byte bbt;
int val = 1 ;
bbt = val // NON!
bbt = 1 // OUI! (réduction)

Notre conseil: n’appliquer ces commodités d'écriture qu’avec discernement dans des expressions pour lesquelles le sens est clair et évident pour tout lecteur du code.

Caches sur les valeurs encapsulées

L’identité n’a pas les mêmes propriétés selon que l’on compare des primitifs scalaires et des objets. En effet :

int x = 1000 ; int y = 1000 ;
Integer ix = x ; Integer iy = y ;
System.out.println((x==y) && (ix==iy)) ; // FALSE!

En partie pour cette raison la spécification demande que, suite à une opération d’encapsulation automatique, pour tout Boolean, Byte, pour tout Character situé dans la plage ASCII et pour tout Short ou Integer dont les valeurs sont comprises entre -128 et 127 l’identité soit conservée entre les objets. (L’emploi de méthodes fabriques pour ces générations permet de gérer un cache des objets -comme pour les Strings conservées dans le pool des constantes-. C’est pour cela que le programmeur est maintenant obligé d’employer ces méthodes - et que les constructeurs ont été frappé d’obsolescence -).

int x = 127 ; int y = 127 ;
Integer ix = x ; Integer iy = y ;
System.out.println((x==y) && (ix==iy)) ; // TRUE!