Java

Sans connaître Java, je n'aimais pas le langage. Mais pour haïr en connaissance de cause, je me suis plongé dedans et j'ai tenté de l'utiliser. Maintenant, je le déteste encore plus : voici pourquoi.

Un peu de discours marketing avant de commencer...

Avant d'être un langage orienté objet, c'est un langage "orienté marketing". Il présente néanmoins certains intérets. Voici quelques unes de ses caractéristiques.

Java est un langage lent. Il convient donc de restreindre son utilisation aux domaines où la vitesse n'est pas un point capital -- la plupart du temps, c'est bon, et de toutes manières, si jamais il y a des parties du programme qui doivent s'exécuter le plus vite possible, on les écrira dans un autre langage, et uniquement elles.

Java possède une bibliothèque standard beaucoup plus complète que le C++. Par contre, il y a certains manques, comme l'héritage multiple ou les types génériques (ça c'est vraiment très génant -- les concepteurs du langage s'en sont d'ailleurs finalement apperçus et prévoient de les rajouter dans Java 3).

On constate que le temps de développement d'un projet en Java est deux fois moindre qu'en C++. Ainsi, même s'il faut acheter des machines plus puissantes pour faire tourner les programmes, on paye moins les programmeurs et l'entreprise qui fait ce choix est quand même gagnante.

Néanmoins, Java n'est pas la panacée : pour des projets pas trop long, des langages interprétés, comme Perl (ou Ruby, Python) permettent un développement beaucoup plus rapide et une vitesse d'exécution tout à fait comparable. [Je rappelle, si besoin est, que Perl n'est pas limité à la programmation Web et est encore moins un synonyme de CGI (on peut faire des CGI dans le langage que l'on veut, par exemple en Java).]

En conclusion, on choisira Java pour de grands projets (plus de six mois, plus de six programmeurs) pour lesquels la vitesse d'exécution n'est pas primordiale. Si la vitesse d'exécution est primordiale, on choisira le C++ ou une approche hybride Java/C++ (ou même Perl/C, pourquoi pas). Si le projet est petit, on choisira un langage "interprété" Perl/Python/Ruby. (S'il s'agit de programmation Web, il existe des FrameWorks tout à fait efficaces, comme HTML::Mason (Perl), AxKit (Perl), Zope (Python).)

Aux programmeurs Java qui ne voient pas l'intéret des langages "interprétés" (je mets des guillemets, parce qu'en fait ils ne sont pas plus interprétés que Java : ils sont compilés en pseudo-code puis exécutés par une machine virtuelle) je rappelerais que ces langages constituent l'un des "modèles de conception" (Design pattern), l'Interpréteur et que Python s'intègre très bien avec Java, car il existe un interpréteur Python en Java (Jython). Il permet ainsi de laisser à l'utilisateur final un moyen de configuration puissant ; il permet aussi de faciliter le débugguage des programmes en Java et accélère l'écriture des tests. Pour plus de détails, voir les livres de B. Eckel.

http://www.mindview.net/Books/
http://penserenjava.free.fr/

Avertissement

Le reste de ces notes s'adresse à des gens connaissant déjà la programmation orientée objet, par exemple en C++ ou Eiffel. Si ce n'est pas le cas, regardez le livre de B. Stroustroup (C++), Meyer (Eiffel) ou B. Eckel (Java).

Critiques de Java

Un langage tout-objet ?

Le langage a été conçu en pensant que tout est un objet, alors qu'en fait tout n'est pas un objet : les types primitifs (int, double, char) et les tableaux ne sont pas des objets.

Un langage hétéroclite

Le langage présente des vestiges de son évolution. Par exemple, les classes Hashtable et HashMap, ou ArrayList et Vector remplissent exactement les mêmes fonctions, avec des noms et des interfaces différentes ; la plupart des méthodes de java.util.Date ont été remplacées par java.text.DateFormat et java.util.Calendar, mais pas toutes ; idem avec AWT et Swing : le premier est suplanté par le second, mais il faut quand même l'utiliser ; pour effectuer des conversions chaine-nombre, onutilise parfois (double)Double.valueOf("1e-6").doubleValue(), parfois (double)Double.parseDouble("1e-6"), etc. Plus je connais Java, plus je rencontre ces hétérogénéités.

Par ailleurs le nom des méthodes n'est pas uniforme : pour convertir une liste en tableau, on utilise toArray, pour convertir un tableau en liste, asList, pour convertir une chaine en nombre valueOf ou parseLong ou longValue.

Mêmes incohérences en javadoc : si la balise @throws est un verbe conjugué, la balise @return est à l'infinitif.

J'ai dit plus haut que l'un des avantages de Java était l'uniformité du style de programmation, qui en faisait un choix pertinent pour les projets faisant intervenir de nombreux programmeurs : il semblerait que ce ne soit pas vraiment le cas.

Des programmes pas si courts que ça

On compare souvent Java et C++, mais c'est une mauvaise comparaison : le C++ est un langage très proche de la machine, qu'il est pertinent d'utiliser pour les applications qui demandent une certaine vitesse d'exécution. Au contraire, Java est un langage (compilé puis) interprété par une machine virtuelle (la Machine Virtuelle Java, ou JVM), adapté aux situations où la vitesse n'est pas un problème : il convient de le comparer à des langages ayant les mêmes caractéristiques et la même vitesse, comme Perl et Python (tous deux compilés puis interprétés par une machine virtuelle).

Java s'avère alors être un langage extrèmement lourd : dans l'exemple que je donne un peu plus bas, un même programme est cinq fois plus long en Java qu'en Perl. Si je regarde des "FrameWorks" pour construire des sites Web, Enhydra (Java) et Zope (Python), je suis horrifié (entres autres) de la manière dont les programmeurs Java remplissent un tableau avec les résultats d'une requète à une base de données.

Voici un exemple en Zope (Python)

<table>
  <tr><th>...</th><th>...</th></tr>
  <dtml-in expr="photoArchive.objectValues()">
  <tr>
    <td><dtml-var sequence-item></td>
    <td><a href="<dtml-var absolute_url>"><dtml-var title></a></td>
  </tr>
</table>

Voici maintenant le même exemple avec Enhydra (Java) : on ne met pas la boucle en HTML (mais par contre, quand on écrit le HTML, il faut penser qu'il y aura une boucle).

<table>
  <tr><th>...</th><th>...</th></tr>
  <tr id="result">
    <td id="sequenceItem">12673</td>
    <td id="title">Foo Bar</td>
  </tr>
</table>

mais on la met dans un fichier Java séparé, qui doit lire tout le fichier HTML, l'analyser, et qu'il faut ensuite modifier à l'aide de DOM (DOM, c'est très bien pour faire des choses compliquées, mais effectuer des remplacements dans un fichier HTML qui sert de modèle, ça devrait être très simple).

welcome = (WelcomeHTML)comms.xmlcFactory.create(WelcomeHTML.class);

// On récupère les éléments qu'on veut modifier
HTMLTableRowElement row = welcome.getElementResult();
row.removeAttribute("id");
HTMLElement number = welcome.getElementNumber();
number.removeAttribute("id");
HTMLElement title = welcome.getElementTitle();
title.removeAttribute("id");
Node table = row.getParentNode();

// On remplit la table
for( int i=0 ; i<result.length; i++ ){

  // Ce qui suit n'a aucun sens : on fait référence à des 
  // noeuds de l'arbre DOM, identifiées par la valeur
  // de l'attribut ID, mais cet attribut n'est plus là !
  welcome.setTextNumber(result[i].number);
  welcome.setTextTitle(result[i].title);

  // Après la ligne suivante, on va se retrouver avec deux
  // lignes absolument identiques. Que va-t-il se passer
  // lors de l'itération suivante : que va-t-on modifier ?
  // la première de ces copies ? la deuxième ?
  table.appendChild(row.cloneNode(true));

}

table.removeChild(row);
 
...

On pourrait penser, à juste titre, que ce problème vient d'Enhydra et n'est pas inhérent à Java (on pourrait, par exemple, faire hériter la classe WelcomeHTML, qui représente le fichier HTML, d'une classe dans laquelle on aurait ajouté une méthode pour remplir les tableaux), mais en fait, ce genre de lourdeur est omniprésente dans les projets en Java. On peut les éviter, mais il semblerait que ce soit difficile ou peu naturel.

Les types génériques (ou types paramétrés ou "templates")

Il n'y a pas de types génériques (pourtant, ils sont suffisemment importants pour faire partie d'UML). Néanmoins, on en a quand même parfois besoin, par exemple, si on veut manipuler des listes d'entiers. Voici la manière dont on déclare une liste d'entiers en C++.

list<int> l;

Et voici une manière de procéder en Java (le code est adapté d'un exercice tiré d'un cours de Java -- je n'oserais pas écrire de telles choses).

L'idée est de partir d'une classe représentant des listes (ici, j'ai pris ArrayList), et d'en faire dériver une nouvelle classe qui redéfinisse les méthodes dont on aura besoin.

public class FooList extends ArrayList {
  ...
  public boolean add(Foo foo) {
	return super.add(foo);
  }
  ...
}

Mais ça ne marche pas comme on s'y attend : on n'a pas redéfini mais juste surchargé les méthodes en question. On peut préalablement les redéfinir (pour qu'elles ne marchent plus), puis les surcharger (pour qu'elles marchent quand même si l'argument a le bon type).

public class FooList extends ArrayList {
  ...
  public boolean add(Object foo) throws Exception {
	throw new Exception("Type mismatch");
  }
  public boolean add(Foo foo) {
	return super.add(foo);
  }

Mais ça ne marche pas, car la méthode add de ArrayList n'envoie pas ce genre d'exception. On ne peut pas s'en tirer avec de l'héritage : on va donc implémenter l'héritage à l'aide de la composition (c'est beaucoup mieux que précédemment, car si on se trompe de type, on a une erreur à la compilation et pas à l'exécution).

public class FooList {
  private ArrayList list;
  public FooList() {
    list = new ArrayList();
  }
  public boolean add(Foo foo) {
    return list.add(foo);
  }
}

Pour bien faire les choses, on écrirait une interface, très semblable à AbstractList, mais qui lèverait des exceptions (je ne le fais pas, c'est trop long : il y a beaucoup trop de méthodes).

Pour faire les choses encore mieux, c'est la classe AbstractList que l'on essaierait de réécrire, car elle est indépendante de l'implémentation de la liste.

public class FooList extends FooAbstractList {
  FooList () {
    list = new ArrayList();
  }
}

abstract class FooAbstractList {
  protected AbstractList list;
  public boolean add(Foo foo) {
    return list.add(foo);
  }
}

Si on peut faire cela « proprement » en Java, c'est quand même beaucoup plus compliqué qu'en C++...

L'héritage multiple

L'héritage multiple (qui lui aussi fait partie d'UML) peut poser des problèmes : imaginons qu'une classe D hérite de classes B et C, chacune héritant d'une même classe A. Si on appelle, sur un objet de D, une méthode de A redéfinie dans B et C, que faut-il faire ? S'il est possible d'expliquer à un compilateur ce qu'il faut faire, ce genre de situation est à éviter car, lorsque le code va évoluer, on perdra de vue ce genre de problème, et les bugs deviendront difficiles à traquer.

Par conséquent, il convient d'éviter de faire hériter une classe D de deux classes B et C assez proches -- par contre, si les classes B et C sont complètement différentes, il n'y a aucun problème.

Les concepteurs de Java sont arrivés à une conclusion différente : ils ont carrément interdit l'héritage multiple. Ensuite, ils ont du en rajouter un peu (mais juste l'héritage multiple d'interfaces, i.e., de classes qui ne contiennent aucun code, juste des déclarations de fonctions). Comme ça ne suffisait pas, ils se sont mis à expliquer dans la documentation (à propos de la classe Thread) comment on peut contourner ce manque. Voici ce que l'on aimerait écrire.

class B { ... }
class C { void foo() { ... } ... }
class A extends B, C { ... }

Et voici ce que l'on est contraint d'écrire (c'est l'une des manières d'implémenter l'héritage dans les langages qui en sont dépourvus).

class B { ... }
interface CSimplifiedInterface { void foo(); }
class C implements CSimplifiedInterface { ... }
class A extends B implements CSimplifiedInterface {
  C c;
  void foo() { c.foo(); }
  ...
}

Les constructeurs par défaut

Même Sun reconnait que c'est une mauvaise idée, mais c'est toujours là. Il faut toujours définir explicitement ses constructeurs.

Autres critiques

http://www.onjava.com/pub/a/onjava/2002/07/31/java3.html

Documentation

La documentation officielle de Sun

La documentation est plutôt bien faite, mais elle est difficilement accessible.

for i in \
  http://java.sun.com/j2se/1.4/docs/api/                  \
  http://java.sun.com/docs/books/tutorial                 \
  http://developer.java.sun.com/developer/onlineTraining/ \
  http://java.sun.com/j2ee/tutorial/1_3-fcs/              \
  http://java.sun.com/webservices/tutorial.html           \
  http://java.sun.com/docs/books/jls/second_edition/html  
do
  wget -R pdf,zip,class -r -l 20 -np -nc -x -k -E -Q 1000M $i
done

Cours

http://www2.cnam.fr/~lignelet/java/
http://lmi17.cnam.fr/~barthe/OO/

Listes de liens

http://directory.google.com/Top/Computers/Programming/Languages/Java/FAQs,_Help,_and_Tutorials/Tutorials/
http://dmoz.org/Computers/Programming/Languages/Java/

Articles

O'Reilly nous propose régulièrement des articles sur Java,

http://www.onjava.com/

de même qu'IBM

http://www-106.ibm.com/developerworks/java/

Les livres de B. Eckel

http://www.mindview.net/Books/

Examens blanc de la "Certification Java"

http://www.google.fr/search?hl=fr&ie=UTF-8&oe=UTF-8&q=java+certification+mock+exams&btnG=Recherche+Google&meta=

L'examen en question d'intéresse à des points de détails du langage, dont tout programmeur sain d'esprit saura rester éloigné (par exemple, quelles sont les couleurs utilisées par java doc, que se passe-t-il si on ne met pas de constructeur, que se passe-t-il si on ne met pas de parenthèses dans une expression complexe, que font les opérateurs <<, <<<, etc.)

Premiers pas

Installation

C'est compliqué.

Commençons par un peu de vocabulaire. Un JRE (Java Run-time Environment), c'est ce qu'il faut pour exécuter des programmes en Java (déjà compilés). Un JDK (Java Development Kit), c'est ce qu'il faut pour écrire des programmes en Java (un compilateur, avec bien sûr aussi un JRE). Un IDE (Integrated Desktop Environment), c'est un cliquodrome, construit autour d'un éditeur de textes, pour aider à l'écriture des programmes en Java (ou en un autre langage) -- ce n'est pas nécessaire, un éditeur de texte rudimentaire suffit. J2SE, J2EE, J2ME signifient Java 2 Standard Edition, Java 2 Enterprise Edition, Java 2 Micro Edition -- nous utiliserons uniquement J2SE.

Les numéros de version sont aussi troublants : c'est bien Java 2, mais le JDK a pour version 1.4.

Il y a plusieurs JDK, par exemple ceux de Sun, IBM (je n'ai rien trouvé sur leur site -- j'ai dû me perdre) et Blackdown. Installons par exemple celui de Sun.

su -p
cd /usr/local/lib
~/src/Java_Sun/j2sdk-1_4_0_01-linux-i586.bin

Comme les différentes versions de Java sont incompatibles, on risque d'avoir besoin d'en utiliser plusieurs différentes. Le script Perl suivant permet de ne pas se perdre parmi ces versions.

#! /usr/local/bin/perl -w
use strict;

# Liste of available JDKs
my %choices = (
  1.3 => { DIR => '/usr/local/lib/jdk1.3',
           CLASSPATH => [],
       },
  1.4 => { DIR => '/usr/local/lib/j2sdk1.4.0_01',
           CLASSPATH => [],
       },
);

# Usage
die "usage $0 [". join('|', (keys %choices)) ."]"
  unless defined($ARGV[0]) and exists $choices{$ARGV[0]};

# Setting the environment variables
$ENV{PATH} = $choices{$ARGV[0]}->{DIR} ."/bin:".
             $choices{$ARGV[0]}->{DIR} ."/lib:".
           $ENV{PATH};
$ENV{CLASSPATH} = $ENV{CLASSPATH} || "";
$ENV{CLASSPATH} = join( ':',  '.',
                              @{ $choices{$ARGV[0]}->{CLASSPATH} },
                              glob( $choices{$ARGV[0]}->{DIR} . "/lib/*.jar" ),
                              glob( $choices{$ARGV[0]}->{DIR} . "/jre/lib/*.jar" ),                      )
                  .":".
                $ENV{CLASSPATH};
my $shell = $ENV{SHELL} || "/bin/sh";

# Printing the environment variables
foreach (qw/SHELL PATH CLASSPATH/) {
  print STDERR "$_:\n";
  foreach (split(':', $ENV{$_})) {
    print STDERR "  $_\n";
  }
}

# Forking
system $shell;

print STDERR "WARNING: The java environment variables are no longer set!\n";
print STDOUT "WARNING: The java environment variables are no longer set!\n";

On peut alors compiler et exécuter un premier exemple

javac Foo.java
java Foo.class

IDE

Il existe des cliquodromes pour faciliter la programmation en Java, par exemple NetBeans ou Emacs (d'ailleurs, NetBeans peut servir à plein d'autres choses : on peut le reprendre et le modifier pour concevoir d'autres types de cliquodromes). Ils permettent tous de s'y retrouver parmi les innombrables fichiers et classes ; certains aident aussi le programmeur à construire des cliquodromes.

http://www.netbeans.org/

*

*

*

*

Il y en a d'autres : Eclipse (IBM, libre -- mais je ne trouve jamais rien sur leur site), JBuilder (Borland, il en existe une version gratuite).

Compilation, exécution

On met généralement une classe par fichier, les fichiers étant regroupés en "paquetages" (les répertoires). La commande javac permet de compiler une classe (on part d'un fichier *.java et on obtient un fichier *.class). On peut ensuite exécuter une classe à l'aide de la commande java (il faut pour cela que la classe en question contienne un point d'entrée, i.e., un méthode main).

for i in *.java
do
  javac $i
done
java HelloWorld

Squelette de programme

Le point d'entrée du programme est une classe avec une méthode statique appelée main.

class FooBar {
  public static void main (String args []) {
    System.out.println("Hello World");
  }
}

Pour la sortie d'erreur (que j'utilise énormément lors du débugguage) :

System.err.println(...);

javadoc

On peut mélanger la documention et le code (c'est ce qu'on appellait autrefois la "programmation littéraire" -- par exemple, TeX (le programme) ou LaTeX (les macros) sont programmés ainsi) : la documentation se trouve dans des commentaires de la forme

/** bla bla bla */

alors que les commentaires habituels ressemblent à

// bla bla bla

ou à

/* bla bla bla */

Cette documentation décrit les différentes classes et méthodes. On utilise certains mots-clefs, préfixés par @, pour indiquer les arguments d'une méthode, ou ce qu'elle renvoie.

Cette documentation est écrite à l'intention des utilisateurs des classes en question et ne doit donc comporter aucune référence à l'implémentation (pour cela, utiliser des commentaires normaux).

Voici quelques exemples, tirés de la documentation de Sun. Typiquement, juste avant de définir une méthode, on indique la description de la méthode en une phrase (on précise éventuellement cela avec quelques avec quelques paragraphes supplémentaires), on indique les arguments, ce que renvoie la fonction, et les exceptions (pour les exceptions, on peut utiliser indifféremment @throws ou @exception -- on remarquera encore ici une incohérence dans le langage : @throws est conjugué mais pas @return...) qu'elle envoie.

/*
 * Return the pieces at the location.
 * @param x location in the board.
 * @param y location in the board.
 * @return  flags indicating what pieces are in this location.
 *          Bit flags; combinations of WALL, PUSHER, STORE, PACKET.
 */

On met aussi ces commentaires juste avant les variables de classe ou d'instance.

/**
 * enables this component to be a dropTarget
 */
DropTarget dropTarget = null;

On met aussi ces commentaires au début d'un fichier ou juste avant la définition d'une classe.

/**
 * An applet that plays a sequence of images, as a loop or a one-shot.
 * Can have a soundtrack and/or sound effects tied to individual frames.
 *
 * @author Herb Jellinek
 * @version 1.5, 29 Nov 1995
 */

On peut utiliser du pseudo-HTML, avec des balises comme <code> ou <p>, ou des entités comme &nbsp; ou &amp;.

/**
  * Load Images from resource files using
  * <code>Image.createImage</code>.
  * The first image is loaded to determine whether it is a
  * single image or a sequence of images and to make sure it exists.
  * Subsequent images are loaded on demand when they are needed.
  * If the name given is the complete name of the image then
  * it is a singleton.
  * Otherwise it is assumed to be a sequence of images
  * with the name as a prefix. Sequence numbers (n) are
  * 0, 1, 2, 3, .... The full resource name is the concatenation
  * of name + n + ".png".
  * <p>
  * Subsequent images are loaded when they are needed. See
  * <code>next</code> and <code>previous</code> for details.
  * @param name the name or prefix of the resource image names
  * @exception IOException is thrown if the image or the first
  * of the sequence cannot be found.
  * @exception OutOfMemoryError if no memory can be allocated for
  * the image.
  */
  void loadImage(String prefix) throws IOException {
    ...
  }

 /**
   * Draw a border of the selected style.
   * Style:
  * <OL>
   * <LI> Style 0: No border is drawn.
   * <LI> Style 1: A simple border is drawn
   * <LI> Style 2: The border is outlined and an image
   * is created to tile within the border.
   * </OL>
   * @param g graphics context to which to draw.
   * @param x the horizontal offset in the frame of the image.
   * @param y the vertical offset in the frame
   * @param w the width reserved for the image
   * @param h the height reserved of the image
   */
      private void paintBorder(Graphics g, int style, int w, int h) {
              if (style == 1) {

On peut mettre des renvois à d'autres classes.

/**
 * Run the animation. This method is called by class Thread.
 * @see java.lang.Thread
 */

Dans un projet, on peut générer la documentation (au format HTML) à l'aide de la commande javadoc. Ilsemblerait que cela s'utilise ainsi :

for i in **/*.java
do
  javadoc $i
done

Caractéristiques du langage

Types et classes

En Java, tout n'est pas un objet : les types de base (int, long, double, char (caractère unicode, que l'on peut définir par '\u1234'), boolean) qui ne sont pas des objets, les tableaux non plus.

Il y a néanmoins des classes (Integer, Long, qui peuvent encapsuler ces types.

Long a = Long.parseLong("173623683");
foo = new JTextField( a.toString() );
float f = (new Float("1.4e-6")).floatValue();
if (Character.isISOControl(c)) { ... }
if (Character.isWhitespace(c)) { ... }
if (Character.isLetterOrDigit(word.charAt(0))) { ... }
if (Character.isUpperCase(myName.charAt(i))) { ... }
foo = new JLabel(new Character(c).toString(), JLabel.CENTER);

this

Si on a besoin de faire référence à l'objet courrant, on peut utiliser le mot-clef "this". C'est utile uniquement si on veut passer l'objet courrant en argmument d'une fonction.

super

Dans un constructeur, on est parfois amené à appeler le constructeur de la classe parente :

super();

Attention, il faut que ce soit la première ligne du constructeur.

Constructeur par défaut

Il faut toujours définir explicitement ses constructeurs : ce n'est pas obligatoire, car Java va créer automatiquement un constructeur par défaut s'il n'y en a pas, mais c'est une très mauvaise habitude de programmation, qui risque de poser des problèmes pour les utilisateurs de la classe et pour ses programmeurs lors de modifications ultérieures -- même Sun conseille de les définir explicitement.

private, protected, public

Les variables et les méthodes peuvent être déclarées private (uniquement accessibles dans la classe), protected (uniquement accessible dans la classe, ses classes dérivées et le paquetage (répertoire) courrant), public (accessibles de partout), ou rien du tout (accessibles dans la classe et le paquetage courrant)

Destructeurs

Il n'y a pas de destructeur, i.e., il n'y a pas de méthode qui est automatiquement appelée quand un objet meurt. (Par contre, il y en a une qui est appelée quand un objet passe par le ramasse-miette -- mais un objet peut y passer longtemps après avoir été détruit, voire même jamais).

Héritage

Il y a deux types de classes : les classes (concrètes) et les interfaces (classes abstraites, i.e., avec des méthodes qui sont juste déclarées, mais pas définies -- on laisse ce soin aux classes dérivées).

Une classe ne peut hériter que d'une seule autre classe (contrairement à C++ ou Perl, il n'y a pas d'héritage multiple). Par contre, une classe peut hériter de (on dit "implémenter") plusieures interfaces.

Voici un exemple tiré de la documentation :

public final class SSLHelloImpl_Stub
  extends java.rmi.server.RemoteStub
  implements Hello, java.rmi.Remote
{
  ...
}

Les interfaces peuvent aussi servir à définir des constantes.

interface States {
  int BEFOREINITIALIZATION = 0;
  int WAITING = 1;
  int COUNTINGDOWN = 2;
  int PLAYING = 3;
  int CHECKINGFORWINNER = 4;
  int GAMEOVER = 5;
}
...
class RingMaster implements States {
  ...
  private int state = BEFOREINITIALIZATION;
  ...
}

abstract

On peut aussi déclarer une classe comme "abstract", cela signifie qu'elle contient à la fois des méthodes définies et des méthodes déclarées mais pas définies, et qui devront être définie dans les sous-classes. Comme une interface, on ne peut pas l'instancier.

abstract class A {
  void foo () { ... } // concrete
  abstract void bar ();    // abstract
  ...
}

class B extends A {
  void bar () { ... }
}

native

Permet de déclarer une méthode qui n'est pas en Java.

En Java, on utilise la JNI (Java Native Interface), qui est détaillées dans le tutoriel de Sun -- ça n'est pas si compliqué que ça : on commence par écrire le code Java, on le compile, ensuite on utilise

javah -jni

pour obtenir le fichier en-tête (*.h) du programme C à écrire, on écrit le programme C, on le compile, on le met dans une bibliothèque partagée (*.so) et on charge cette bibliothèque partagée depuis Java

System.loadLibrary("hello");

La partie difficile, passée sous silence par le tutoriel de Sun, c'est la conversion entre les types C et les types Java (mais, comme en Perl, il doit y avoir des fonctions pour faire ça simplement).

final

Pour une variable, indique qu'elle est constante ; pour une méthode, indique qu'elle ne peut pas être redéfinie dans une sous-classe ; pour une classe, indique qu'elle ne peut pas avoir de sous-classe.

static

Pour une variable, indique que c'est la même variable pour toutes les instances de la classe.

Pour une méthode, indique qu'elle peut être appelées sans aucune instance (par exemple, la fonction main, ou les méthodes de Arrays.*).

Pour une classe interne, indique qu'on peut l'instancier même si on n'a aucune instance de la classe externe.

Le mot-clef static peut aussi modifier un bloc de code, dans une classe, en dehors de toute méthode : ce code sera exécuté une fois, quand la classe est initialisée (généralement, on utilise cela pour initialiser des variables statiques).

class Bar {
  static {
    ... // some code
  }
}

Paquetages

On peut dire à Java que certaines classes sont très proches les unes des autres : il suffit de les mettre dans un même répertoire (par exemple Toto) et de rajouter une ligne au début de chaque fichier :

package Toto;

On peut ensuite (attention à l'ordre : d'abord "package", puis les "import", puis le code) utiliser les classes contenues dans ce paquetage en mettant au début du fichier

import Toto.*;

Généralement, on s'arrangera pour éviter les problèmes de conflit de noms en ajoutant le nom de son site Web (ou de son adresse électronique) au début. Par exemple, je pourrais mettre mon paquetage Toto dans un répertoire

fr/jussieu/math/zoonek/Toto

et je l'incluerait avec la ligne

import fr.jussieu.math.zoonek.Toto.*;

Type à la compilation et à l'exécution

Chaque variable possède deux types : celui avec lequel elle a été déclarée (que j'appelerai type à la compilation : le compilateur le connait) et celle avec lequel elle a été initialisée (que j'appelerai type à l'exécution : on n'a pas vraiment de moyen simple de connaitre ce type avant l'exécution). Le type à l'exécution est une sous-classe du (i.e., il est plus précis que)type à la compilation.

Quand on appelle une méthode sur un objet a,

a.foo();

on utilise le type à l'exécution pour savoir dans quelle classe il faut chercher la méthode foo(). En d'autres termes,

// Exemple de masquage : une sous-classe redéfinit une
// méthode déjà présente dans la surclasse.
interface A          { String foo (); }
class B implements A { public String foo() { return "B"; } }
class C extends B    { public String foo() { return "C"; } }
class D {
  public static void main (String [] args) {
    A b = new B();
    A c = new C();
    System.out.println( b.foo() );
    System.out.println( c.foo() );
  }
}

Affiche B puis C.

Type à la compilation et à l'exécution (suite)

Par contre, si on appelle une fonction du genre

foo(a);

c'est le type à la compilation de a qui va intervenir (ça n'est pas logique -- c'est du Java). En d'autres termes,

// exemple de surcharge (des fonctions différentes
// qui ont le même noms et qui sont distinguées
// par le type et le nombre de leurs arguments)
class A { }
class B extends A { }
class C {
  // Normalement, il ne faut pas mettre des "static" partout
  static String foo(A a) { return "A"; }
  static String foo(B b) { return "B"; }
  public static void main (String [] args) {
    A a = new A();
    A b = new B();
    System.out.println( foo(a) );
    System.out.println( foo(b) );
  }
}

renvoie A et A.

Clonage

On peut copier un objet (et on obtient comme résultat un objet du même type (à l'exécution) que l'objet de départ). On utilise cela assez souvent pour des types comme des listes ou des tables de hachage. Mais attention à ne pas utiliser cela directement pour des tableaux à plusieures dimensions !

this.env = (Hashtable)env.clone();
byte[] ber = ...;
this.bytes = (byte[])ber.clone();
this.args = (String[])origArgs.clone();

Par contre, pour les classes que l'on définit soi-même, il faut redéfinir cette méthode (sinon, il risque de prendre celle de la classe Object, ce qui peut poser des problèmes...

Voir aussi, ailleurs dans ce document, l'interface Serializable, qui permet de faire des « copies profondes ».

Exceptions

En C, quand une erreur se produit (par exemple une division par zéro), la fonction va généralement renvoyer un objet de la nature attendue, mais avec une valeur prédéterminée signalant la survenue d'une erreur. C'est maladroit pour deux raisons : d'une part le programmeur doit penser à vérifier qu'il n'y a pas de division par zéro, d'autre par il faut penser à vérifier la valeur de retour (il y a certaines failles de sécurité, dans des logiciels sensibles, qui proviennent de ce genre d'erreur).

En java (ou en C++), c'est prévu : si quelque chose d'imprévu se produit, une exception est levée. Lorsqu'une méthode lève une exception, l'objet qui l'a appelée la reçoit et agit en conséquence : il peut soit ne rien faire (et donc renvoyer l'exception plus haut -- il faut alors le préciser lors de la définition de la méthode),

public static void main ( String args [] )
throws java.io.IOException
{
  ...
  File inFile = new File(args[0]);
  BufferedReader in = new BufferedReader(new FileReader(inFile));
}

soit transformer cette exception en une autre exception qui indique plus précisément ce qui s'est passé (et il envoie cette expection plus haut),

public Disc() throws DiscRackBusinessException {
  try {
    this.myDO = DiscDO.createVirgin();
  } catch(DatabaseManagerException ex) {
    throw new DiscRackBusinessException("Error creating empty Disc", ex);
  } catch(ObjectIdException ex) {
    throw new DiscRackBusinessException("Error creating empty Disc", ex);
  }
}

soit il fait ce qu'il faut pour corriger cette erreur.

try {
  windowClassObject = Class.forName(windowClass);
} catch (Exception e) {
  // The specified class isn't anywhere that we can find.
  label.setText("Can't create window: Couldn't find class "
                + windowClass);
  button.disable();
  return;
}

Il ne faut en AUCUN cas faire ce que suggère la documentation de Sun, i.e., attraper l'exception et l'oublier.

try {
  port = Integer.parseInt(portField.getText());
  doIt(port);
} catch (NumberFormatException e) {
  //No integer entered.  Should warn the user.
}

Si on ne veut ni traiter l'exception ni l'envoyer plus haut, on peut simplement afficher un message d'erreur (mais de toute manière, Java va en afficher un).

try {
  ...
} catch (Exception e) {
  System.err.println("Exception caught: " + e.getMessage());
}

Ici, la méthode getMessage récupère la chaine de caractères qui a été donnée en argument du constructeur de l'exception (mais la plupart du temps, il est préférable de définir ses propres classes d'excpetions, qui héritent de Exception).

try {
  ...
  throw new Exception("Foo bar");
  ...
} catch (Exception e) {
  if( e.getMessage().equals("Foo Bar") ){
    ...
  }
}

Dans certains cas très rares, on peut ignorer l'expection :

try {
  requestedWidth = Integer.parseInt(windowWidthString);
} catch (NumberFormatException e) {
  //Use default width.
}
							 
try {
  sleep((int)(Math.random()*10000));
} catch (InterruptedException e) {}

On peut aussi ajouter un bloc finally qui sera exécuté dans tous les cas (on l'utiliser pour faire un peu le ménage, par exemple pour fermer des fichiers).

Il y a aussi des assertions (i.e., des vérifications utilisées lors du débugguage mais qui disparaissent de la version finale du logiciel). Par défaut, les assertions sont desactivées.

assert false; // should never reach this point
assert (x==1) : x; // throws an exception and gives the value of x
switch(a) {
  case 1: ...; break;
  case 2: ...; break;
  default: // should not happen
  assert false;
}

Mais attention : par défaut, les assertions sont desactivées : il faut les acriver explicitement. Bien sûr, on ne peut pas les activer en rajoutant une option lors de la compilation ou de l'exécution -- ce serait trop simple.

Il faut les activer à l'aide d'une des méthodes suivantes (attention, il faut appeler ces méthodes suffisemment tôt, car sinon elles n'auront pas l'effet escompté, sans que pour autant on ait le moindre message d'erreur -- à croire que Java est un langage conçu pour les programmeurs qui ne commettent pas d'erreur...).

public void setDefaultAssertionStatus(boolean enabled);
public void setClassAssertionStatus(String className, boolean enabled);
public void setPackageAssertionStatus(String packageName,
                                      boolean enabled);

x(Bien lire la documentation avant de les utiliser : moi, je n'ai pas compris.)

La documentation explique aussi comment s'assurer que les assertions sont bien activées (on s'attendrait à utiliser la réflexivité de Java, mais non).

static {
  boolean assertsEnabled = false;
  assert assertsEnabled = true; // Intentional side effect
  if (!assertsEnabled)
    throw new RuntimeException("Asserts must be enabled!");
}

Types paramétrés (ou types génériques, ou templates)

Non, il n'y en a pas. J'explique un peu plus haut comment les implémenter, mais c'est abominable.

Exemple

Traduisons le programme (Perl) suivant en Java.

#! perl -w
use strict;
die "Usage: $0 file.txt"
  unless scalar @ARGV == 1;
my %map;
while(<>) {
  next unless m/^\s*(\S+?)\s+(.+)/;
  $map{$1} = $2;
}
foreach my $key (sort (keys %map)) {
  print "$key => $map{$key}\n";
}

Comme je n'ai pas l'habitude de programmer en Java, c'est probablement très maladroit. (Le programme en Perl est un peu plus robuste : il regarde les espaces blancs, et pas simplement les espaces, il accepte plusieurs espace au lieu d'un seul, et il enlève les éventuels espaces qui se trouvent au début de la ligne -- mais ça n'est pas grave -- je ne savais pas que Java connaissait les expressions régulières quand je n'ai écrit.)

import java.io.*;
import java.util.*;

class FilesAndHashes {
  public static void main ( String args [] )
  throws java.io.IOException
  {
    // Check the arguments
    if( args.length != 1 ){
      System.err.println("Usage: FilesAndHashes file.txt");
      System.exit(1);
    }

    // Open a file
    File inFile = new File(args[0]);
    BufferedReader in = new BufferedReader(new FileReader(inFile));

    Map map = new HashMap();
    String line;
    while( (line = in.readLine()) != null ) {

      // Read the line, split it at the first space
      // and put it in a hash
      int i = line.indexOf(' ');
      if( i != -1 ) {
        map.put(line.substring(0,i), line.substring(i+1, line.length()));
        System.out.println("Key: " + line.substring(0,i) +
                           " Value: " + line.substring(i+1,line.length()));
      } else {
        System.err.println("Malformed line? " + line);
      }
    }
    in.close();

    // Get the keys
    Set keys = map.keySet();
    // Turn them into a List
    Object [] keyArray = keys.toArray();
    // No, I said a List, not an Array.
    List keyList = Arrays.asList(keyArray);

    // Sort the hash
    Collections.sort( keyList );

    // Print it
    for (Iterator i=keyList.iterator(); i.hasNext() ; ) {
      String k = (String) i.next();
      String v = (String) map.get(k);
      System.out.println(k + " => " + v);
    }
  }
}

On constatera que le programme est à peu près cinq fois plus long en Java qu'en Perl...

Classes utiles

Entrées-Sorties

Dans le paquetage java.io.

Ouvrir un fichier et le lire ligne par ligne.

File inFile = new File(args[0]);
BufferedReader in = new BufferedReader(new FileReader(inFile));
String line;
while( (line = in.readLine()) != null ) {
  ...
}

Pour écrire, c'est pareil : il suffit de remplacer Reader par Writer.

La classe BufferedReader permet de lire un flux d'entrée ligne par ligne, contrairement à la classe FileReader.

Il existe aussi des classes FileInputStream, BufferedInputStream (idem avec ...OutputStream), qui lisent des octets et pas des caractères -- si on se limite aux langues occidentales, codées en ASCII ou en ISO-8859-?, ce n'est pas trop grave, mais il vaut quand même mieux penser à l'internationalisation.

Il y a aussi des choses dans le paquetage java.nio (locks, non-blocking I/O, etc.).

Listes

L'interface List possède deux implémentations : LinkedList et ArrayList.

Il y a aussi une classe Vector, qui me semble faire double usage avec ArrayList (ce doit être un vestige des premières versions de Java).

La méthode add permet d'ajouter des éléments à une List.

List l = new ArrayList();
l.add(...);

On peut trier une List à l'aide de la commande sort (il faut que la classe à laquelle appartiennent les éléments de la liste implémente l'interface Comparable -- c'est le cas pas exemple des String).

String [] anArray = ...;
List aList = Arrays.asList(anArray);
Collections.sort(aList);

Si on veut trier des choses plus complexes, on peut soit définir une classe qui implémente cette interface Comparable, soit donner un deuxième argument à la fonction sort : en C on parlerait de "pointeur" sur une fonction qui effectue cette comparaison, en Lisp, on parlerait de fonctin lambda).

Collections.sort(winners, new Comparator() {
  public int compare(Object o1, Object o2) {
    return ((List)o2).size() - ((List)o1).size();
  }
});

il y a aussi des méthodes shuffle, reverse.

On peut parcourrir une List avec des Iterator (voir l'exemple plus haut).

La non-classe Array

Les tableaux ne forment pas une classe.

int i[] = new int[10];
char c[] = new char[] {'a', 'b', 'c', 'd', 'e', 'f', 'g' };

On peut convertir cette non-classe Array en une List ainsi :

Object [] anArray = ...;
List aList = Arrays.asList(anArray);

On peut les manipuler avec les méthodes de java.lang.Arrays :

System.arraycopy 
Arrays.sort
Arrays.binarySearch
Arrays.equals

Parfois, ces non-objets se comportent comme des objets.

Tables de hachage

L'interface Map possède deux implémentations, HashMap et TreeMap.

(Il y a aussi Hashtable, qui est un peu plus ancien mais pas encore obsolète...)

Map hash = new HashMap();
hash.put(..., ...);
...
Set keySet = hash.keySet();
Object [] keys = keySet.toArray();
  
while (k.hasMoreElements()) {
  Foo f = (Foo) k.nextElement();
  Bar b = (Bar) hash.get(f);
  ...
}

Nous avons vu plus haut comment trier les clefs d'une table de hachage.

Il existe aussi une classe LinkedHashMap, qui est une table de hachage avec un ordre sur les éléments (il y a beaucoup de gens qui aiment ce genre de chose).

Ensembles

L'interface Set possède deux implémentations, HashSet et TreeSet.

Les types de base

On dispose des types long, double, boolean. Ce ne sont pas des classes, mais il existe des classes Long, Double, Boolean qui les encapsulent. Pour faire la conversion dans un sens, on utilise simplement le constructeur ; dans l'autre sens, on utilise les méthodes longValue, doubleValue, etc.

Le type String correspond aux chaines de caractères. Pour les concaténer, on utilise l'opérateur +. Parmi les méthodes utiles, citons length, indexOf, lastIndexOf, equals, compareTo (mais il vaut mieux utiliser la classe java.text.Collator, qui tient compte de la locale) startsWith, endsWith, trim (enlève les espaces au début et à la fin), substring, valueOf (pour convertir un objet en chaine), toLowerCase, matches (pour vérifier si une chaine est conforme à une expression régulière : l'expression régulière est simplement donnée comme chaine de caractères -- ce sont les mêmes expressions régulières qu'en Perl, mais il faut mettre un peu plus d'antislashes), replaceFirst, replaceAll, split (qui regarde son argument comme une exception régulière).

s.matches("\\bJava\\b");

Il y a aussi des objets pour manipuler des expressions régulières dans java.util.regex.

La classe String n'est pas un bon choix si on doit modifier sont contenu : la classe StringBuffer est là pour cela. L'argument du constructeur est la taille initiale de la chaine (par défaut 16 caractères).

public static String toHexString(int i) {
  StringBuffer buf = new StringBuffer(8);
  do {
    buf.append(Character.forDigit(i & 0xF, 16));
    i >>>= 4;
  } while (i != 0);
  return buf.reverse().toString();
}

(Attention, la méthode equals() de StringBuffer ne donne pas le résultat attendu)

Tout objet peut implémenter une méthode toString, qui le transforme en chaine, ce qui permet de l'imprimer avec System.out.print();

class Foo {
  Foo() {}
  public String toString() { return "Not printable"; }
}

Conversions

Chaque classe peut implémenter une méthode toString, qui donne une représentation imprimable de l'objet. Dans l'autre sens, on peut utiliser la méthode statique valueOf.

String a = String.valueOf(42);
String b = Int(42).toString();

Pour convertir une chaine en double

double a = Double.parseDouble("1.4e-6");
double b = Double.valueOf("1.4e-6").doubleValue();

Hexadécimal, octal :

String s = Integer.toHexString("32"); // voir aussi java.text.*
long v = Long.decode("0xff").intValue();
long v = Long.decode("0377").intValue();

Mathématiques

Quelques constantes

Math.PI
Math.E

Quelques fonctions

Math.sin
Math.atan
Math.sqrt
Math.floor
Math.ceil
Math.round
Math.random(); // entre 0 et 1

Pour des nombres aléatoires répartis selon une distribution gaussienne, on peut regarder dans java.util.Random.

Pour des nombres vraiment aléatoires, on peut regarder dans java.security.SecureRandom.

On peut aussi manipuler des nombres entiers ou décimaux, avec les classes BinInteger ou BigDecimal.

BigInteger prime = 
  BigInteger.probablePrime(1024, new java.security.SecureRandom());

Il y a aussi un peu de cryptographie dans java.security.*.

Date

La classe java.util.Date permet de récupérer la date courrante et aussi de faire un peu d'arithmétique sur les dates. Mais en fait, elle est obsolète : il est conseillé d'utiliser java.util.Calendar et java.text.DateFormat à la place.

Réseau

Il est possible de manipuler des sockets (voir java.net.* pour la partie client, et en particulier ServerSocket pour la partie serveur), mais la plupart du temps, quand on a besoin du réseau, c'est pour des connections HTTP.

import java.net.*;
import java.io.*;
URL url = new URL("http://www.yahoo.fr/");
InputStream in = url.openStream(); // corps de la réponse 
                                   // demander l'en-tête.

XML

L'ensemble des classes pour manipuler XML s'appelle JAXP (mais ce sigle est réservé à la documentation, dans le code, on trouvera simplement javax.xml : c'est pour mieux dérouter le programmeur).

On peut utiliser SAX (i.e., on peut voir le fichier XML comme un flux d'évènements "de type "balise ouvrante" ou "balise fermante", auxquels on peut réagir).

import javax.xml.parsers.*;
...
// On définit startElement, etc.
class MyHandler extends DefaultHandler { ... }
...
SAXParserFactory parserFactory = SAXParserFactory.newInstance();
parserFactory.setValidating(true);
parserFactory.setNamespaceAware(true);
SAXParser parser = parserFactory.newSAXParser();
MyHandler handler = new MyHandler();
parser.parse(new File("foo.xml"), handler);

Est-ce que quelqu'un peut m'expliquer pourquoi on utilise newInstance() et pas new ?

A FAIRE

On peut aussi utiliser DOM, i.e., considérer le document comme un arbre, au travers duquel on navigue, en faisant éventuellement un peu de bouturage.

java.io.*;
import javax.xml.parsers.*;
import org.w3c.dom.*;
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder parser = factory.newDocumentBuilder();
Document document = parser.parse(new File(args[0]));
...

On peut aussi utiliser XSLT (i.e., décrire les transformations à appliquer à un fichier XML à l'aide d'un fichier XML).

import javax.xml.transforms.*;
import javax.xml.transforms.stream.*;
import javax.xml.transforms.dom.*;
import org.w3c.dom.*;
  
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder parser = factory.newDocumentBuilder();
Document document = parser.parse(new File(args[0]));

File xsltfile = new File("1.xslt");
Source xsltSource = new StreamSource(xsltfile);
Source source = new DOMSource(document);
Result result = new StreamResult(System.out);

TransformerFactory tf = TransformerFactory.newInstance();
Template transformation = tf.newTemplates(xsltSource);
Transformer transformer = transformation.newTransformer();
transformer.transform(source, result);

Threads

Les Threads (en français, on pourrait parler de filaments, ou de fils d'exécution), ressemblent beaucoup à des processus différents qui émaneraient du programme que l'on écrit et qui s'exécuteraient simultanément. La principale différence, c'est qu'ils ont accès de manière beaucoup plus simple aux données du programme (pas besoin de mémoire partagée, de sémaphores ou autres IPC : tout est déjà prévu et relativement simple). Si les processus fils n'ont pas besoin d'accéder aux données du père, les Threads ne servent pas à grand-chose, mais si c'est le cas, ils s'avèrent utiles.

La classe Timer permet de lancer une fonction après un certain temps (par exemple, si on veut fermer une fenêtre après une dizaine de secondes, pour que l'utilisateur n'ait pas à cliquer un peu partout pour la fermer), ou à un instant donné, ou de manière répétitive.

La classe Thread contient une méthode run (vide : il faut dériver la classe et redéfinir cette méthode), une méthode start (pour lancer le Thread). Comme Java interdit l'héritage multiple, on ne peut pas procéder ainsi si notre classe hérité déjà d'autre chose.

Pour contourner ce défaut du langage, on peut aussi implémenter les Threads en utilisant l'interface Runnable (c'est plus compliqué, mais les concepteurs du mangage ne nous laissent pas le choix). On écrit alors une classe MyThread qui implémente cette interface, et qui définit donc des méthodes run() et start(). La méthode stat() ressemble alors à :

private Thread myThread = null;
public void start() {
  if (myThread == null) {
    myThread = new Thread(this, "Clock");
    myThread.start();
  }

On peut modifier la priorité d'un Thread pour qu'il s'exécute plus rapidement que les autres.

Il y a une méthode yield que l'on peut appeler dans un Thread pour dire à la machine virtuelle Java que c'est un bon endroit pour passer à un autre Thread.

Le mot-clef synchronized indique une méthode qui bloque l'objet courrant : aucun autre Thread ne peut accéder à l'objet tant que cette méthode n'est pas terminée.

public synchronized int get() { ... }

On peut faire cela avec juste un morceau de code :

...
synchronized(this) {
  ...
}
...

Un thread ne peut pas se bloquer lui-même : une méthode synchronisée a le droit d'en appeler une autre.

Une méthode synchronisée peut être amenée à attendre qu'un certain évènement se produise (grace à l'appel d'une autre méthode, par un autre Thread) : elle appelle alors la fonction wait(), qui laisse d'autres Threads accéder à l'objet (il n'est plus bloqué), et qui attend que ces Threads appellent la fonction notifyAll (ou notify).

while (available == false) {
  try {
    wait();
  } catch (InterruptedException e) { ...  }
}
available = false;
notifyAll();
...

La fonction wait accepte un ou deux argument qui disent combien de temps il faut attendre.

Il faut faire TRES attention à ne pas créer de blocage...

Les Threads peuvent être mis dans des groupes, afin que l'on puisse les manipuler tous ensemble (c'est aussi utile que les groupes de processus sous Unix (si, si, ça existe)).

Un Thread peut éventuellement attraper l'exception ThreadDeath qui lui ordonne de mourir.

La méthode stop (obsolète) tue un Thread violemment, la méthode interrupt le tue un peu moins violemment, la méthode destroy (obsolète) le tue encore plus violemment. La méthode suspend (obsolète) arrête momentanément un Thread : il faudra le relancer explicitement, avec la méthode resume (obsolète). Ces méthodes sont obsolètes, car elles ne relâchent pas les verrous et peuvent conduire à des bloquages.

La méthode yield, à l'intérieur d'un Thread, indique que c'est un bon endroit pour passer à un autre Thread. La commande sleep interromp le Thread pour un certain nombre de milisecondes.

Un Thread peut être plus ou moins prioritaire qu'un autre : on règle cela à l'aide des méthodes getPriority et setPriority.

Généralement, on essaye de se limiter à un certain nombre de Threads, au besoin en recourrant à un "pool de Threads" (on crée une dizaine de threads et, à chaque fois qu'on a besoin d'un Thread, on regarde si l'un des dix est libre, sinon, la tache est mise en attente (dans une file d'attente) jusqu'à ce qu'un Thread se libère).

Réflexivité

Comme au bon vieux temps de Perl 4 où l'on farfouillait allègrement dans la table de hachage contenant toutes les variables (et aussi les fonctions) du programme, pour les modifier de la manière la plus sale qui soit, il est possible de regarder ce que contient une classe (méthodes, héritage, etc.).

Ce principe de réflexivité peut paraitre très beau de prime abord, mais en fait, ça sert souvent à faire des choses très sales (mais ça peut aussi servir, donc c'est bien que les concepteurs du langage nous aient laissé cet outil).

import java.lang.reflect.*;

...
System.out.println("J'appartiens à la classe " + getClass().getName());
try { foo.getClass().newInstance(); }

Voici une application utile de la réflexivité : un générateurs de code peuvent recourrir à la réflexivité pour proposer à l'utilisateur les différentes méthodes de la classe qu'il souhaite utiliser (ou encore, lors de la conception d'un cliquodrome, les différents attributs modifiables et les les différents évènements auxquels le Widget peut réagir -- pour cela, on doit respecter certaines conventions en choisissant le nom des méthodes lorsque l'on écrit la classe : une telle classe s'appelle alors un Bean).

Voici une autre application : on peut l'utiliser pour charger une classe à l'exécution et pas à la compilation. On peut ainsi changer facilement la classe utilisée (par exemple, une classe permettant d'accéder à une certaine base de données, ou quelque chose qui dépendrait du système d'exploitation) sans avoir à tout recompiler. [J'ai déjà vu du code de ce genre, en particulier pour accéder à des bases de données, mais le nom de la classe à charger est mise dans une chaine de caractère codée en dur dans le fichier -- je ne comprends donc pas dut tout à quoi ça sert.]

Swing

Il y a deux ensembles de classes pour créer des infterfaces graphiques en Java : AWT (ancien, pas encore obsolète, mais à éviter) et Swing (le nom de presque toutes les classe commence par la lettre J).

La documentation nous demande d'éviter d'utiliser AWT, et d'éviter encore plus d'utiliser à la fois AWT et Swing. Par contre, cette même documentation nous dit qu'il faut faire commencer les programmes utilisant Swing par

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

C'est nécessaire, car on a quand même besoin de quelques morceaux de AWT -- mais il faut faire attention à ne prendre de AWT que ce qui n'est pas dans Swing. Heureusement que Java a la réputation d'être un langage propre...

Voici quelques classes à connaitre

JFrame    une fenêtre 
JDialog   une fenêtre dépendant d'une autre fenêtre
JLabel    du texte dans une fenêtre
ImageIcon une classe que l'on peut mettre en argument
          de JLabel pour avoir une image
JButton   un bouton
JCheckBox une boite à cocher ou pas
  
JPanel    un conteneur vide (hbox, vbox, grille)
JTextField un endroit où on peut taper un peu de texte

Voici l'exemple tiré du tutoriel.

import javax.swing.*;        

public class HelloWorldSwing {
  public static void main(String[] args) {
  
    // Créer une fenêtre
    JFrame frame = new JFrame("HelloWorldSwing");
  
    // Créer un label
    final JLabel label = new JLabel("Hello World");
    
    // Attacher le label à la fenêtre
    frame.getContentPane().add(label);
    
    // Dire comment réagir à certains évènements
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    
    // Décider de l'endroit où on va mettre les différents
    // éléments du JFrame (je crois que c'est récursif : 
    // si le JFrame contenait d'autres conteneurs, cette ligne
    // mettrait aussi de l'ordre à l'intérieur de ceux-ci)
    frame.pack();
    
    // Afficher le JFrame
    frame.setVisible(true);

  }
}

Dans les applications réelles, on définierait un objet qui hériterait de JFrame et on mettrait la méthode statique main dans une autre classe.

Comme d'habitude, le plus compliqué, c'est de positionner les Widguètes (un Widguète (en anglais, widget -- ça n'est pas la peine de chercher dans un dictionnaire : le mot n'existe pas), c'est un morceau d'un cliquodrome, par exemple un bouton, un morceau de texte, une image, un champ à remplir, etc.) là où on veut.

Une vbox :

JPanel checkPanel = new JPanel();
checkPanel.setLayout(new GridLayout(0, 1));
checkPanel.add(chinButton);
checkPanel.add(glassesButton);
checkPanel.add(hairButton);
checkPanel.add(teethButton);

Une hbox :

checkPanel.setLayout(new GridLayout(1, 0));

Un tableau à deux colonnes :

checkPanel.setLayout(new GridLayout(0, 2));

Un tableau à 4 lignes et deux colonnes :

checkPanel.setLayout(new GridLayout(4, 2));

On peut aussi préciser qu'il faut un peu d'espace (en pixels) entre les boites que l'on empile :

checkPanel.setLayout(new GridLayout(4, 2, 10, 10));

En fait, il y a plein de Layouts autres que GridLayout, mais il suffira à peu près toujours.

On peut aussi s'amuser à faire divers réglages cosmétiques (bulles d'aide, bordure, choix de l'UI, etc.) : voir le tutoriel de Sun.

L'autre point délicat dans la programmation des cliquodromes, c'est la gestion des évènements : le programme doit réagir aux mouvements et aux clics de souris de l'utilisateur. En Java, on va considérer différents types d'iévènements (action (quand l'utilisateur fait ce qu'il est sensé faire avec le widguète), clavier, souris, focus, etc.) et créer un "Listener" pour chacun de ces types. Un Listener, c'est une instance d'une classe qui implémente une interface, contenant quelques fonctions, qui seront appelées selon le type de l'évènement.

La plupart du temps, ça ressemble à ça (la drôle de syntaxe signifie qu'on définit une classe qui hérite de ActionListener, dans laquelle on redéfinit la méthode actionPerformed, et qu'on en prend une instance ; cette classe ne porte pas de nom (elle est anonyme), et elle a accès aux champs privés (finaux) de la classe ambiante).

textEuros.addActionListener(new ActionListener() {
  public void actionPerformed(ActionEvent e) {
    euros = (double) Double.parseDouble( textEuros.getText() );
    francs = euros*RATE;
    textFrancs.setText((new Double(francs)).toString());
  }
});

Parfois, on n'utilise pas des "Listeners", mais des "Adapters" : les Listeners sont des interfaces, il faut donc implémenter toutes les méthodes (dans l'exemple précédent, il n'y en avait qu'une) ; les "Adapters" sont des implémentations de ces interfaces dans lesquelles toutes les méthodes sont vides, il suffit donc d'implémenter la méthode que l'on veut (par exemple, click de souris) sans mentionner les méthodes qui ne nous intéressent pas (par exemple, relachement du bouton de la souris, entrée de la souris dans le Widguète, sortie de la souris, etc.).

public class MyClass extends Applet {
  ...
  someObject.addMouseListener(new MouseAdapter() {
    public void mouseClicked(MouseEvent e) {
      ...
    }
  });
  ...
  }
}

java.nio

La nouvelle bibliothèque d'Entrées-Sorties (I/O, en anglais), avec entre autres les sockètes non bloquantes.

Beans

(Tout d'abord, vous n'avez probablement pas besoin des Beans : on les rencontre quand on fait de la programmation répartie (J2EE) ou quand on écrit des classes que l'on veut voir reconnues par un générateur de code. Ensuite, je n'ai pas vraiment compris ce que c'est.)

Pour construire un cliquodrome, on utilise souvent des "cliquodromes à construire des cliquodromes" (anjuta/glade pour Gtk+, kdevelop pour Qt, netbeans pour Java -- sous WinDoze, je crois que c'est ça qu'on appelle la "Programmation Visuelle"). Les Beans, c'est simplement un ensemble de conventions sur le nom des méthodes d'une classe pour qu'elle soit automatiquement reconnue par ce genre de logiciel (et en fait, on peut généraliser cela à des "cliquodromes à construire autre chose que des cliquodromes", qui portent des noms très divers : "générateurs de code", "Application Helpers", "Application Wizards", etc.).

On peut aussi voir un Bean comme une classe utilisée dans la programmation évènementielle (car les Beans communiquent à l'aide d'évènements).

Un Bean, c'est une classe dont les méthodes respectent les conventions de nommage suivantes, qui permettent de lui associer des propriétés ou de la faire réagir à des évènements (les ingénieurs de chez Sun appellent ces conventions un Design Pattern : ils semblent ignorer que le terme a déjà un sens complètement différent en informatique -- on parlera simplement de convention de nommage) :

getMachin() Récupère la valeur de la propriété "machin" (attention aux majuscules)
setMachin() Fixe cette valeur
isMachin()  Si la propriété est booléenne, 
            on a le choix entre isMachin et getMachin

addFooBarListener(FooBarListener)     Pour recevoir un FooBarEvent
removeFooBarListener(FooBarListener)

On peut écrire les Beans à la main (car c'est une classe tout à fait normale, elle a juste des méthodes qui portent des noms particuliers) ou à l'aide du BDK (Beans Development Kit).

On peut regarder le contenu d'un Bean (généralement, c'est le générateur de code qui fait ça, pas nous). (On récupère ainsi le nom des propriétés, des méthodes pour les lire ou les modifier, le nom des autres méthodes publiques, le nom des évènements auxquels il réagit.)

Class bean = ...
BeanInfo bi = Introspector.getBeanInfo( bean, java.lang.Object.class);

La plupart des méthodes publiques d'un Bean sont "synchronized" (les Beans seront souvent utilisés dans un environement "multithreaded"). Il faut de plus mettre les Beans dans une boite de Beans : un fichier JAR (i.e., ZIP) qui contient le Bean et un fichier "manifeste", *.mf (avec le même nom), contenant quelque chose comme ça :

Manifest-Version: 1.0

Name: bangbean/BangBean.class
Java-Bean: True

On crée ensuite le Bean ainsi :

jar cfm BangBean.jar BangBean.mf bangbean

Généralement, les Beans sont Serializables, de manière à pouvoir être facilement utilisés par les "outils de programmation visuelle qui reconnaissent les Beans" -- c'est d'ailleurs la raison d'être des Beans.

Sérialisation

Lorsque des programmes Java veulent communiquer, ils peuvent se transmettre des objets (exacement comme avec Corba), qui doivent être préalablement sérialisés, i.e., transformés en un flux d'octets.

Cela permet aussi de rendre les objets persistants, en les stoquant dans des fichiers (exactement comme en Perl, avec le module Storable). Pour rendre un objet sérialisable, il suffit de dire qu'il implémente l'interface Serialisable (c'est une interface vide, il n'y a aucune méthode à définir, tout se fera tout seul, même si l'objet contient des références sur d'autres objets). Pour lire ou écrire des objets sérialisés, On utilise les méthodes readObject et writeObject des classes ObjectInputStream et ObjectOutputStream.

public class Worm implements Serializable {
  // Only define your data and your methods: the Serializable interface is empty
  ...
  public static void main(String[] args) 
  throws ClassNotFoundException, IOException {
    Worm w = new Worm(6, 'a');
    System.out.println("w = " + w);
    ObjectOutputStream out =
      new ObjectOutputStream(
        new FileOutputStream("worm.out"));
    out.writeObject("Worm storage");
    out.writeObject(w);
    out.close(); // Also flushes output
    ObjectInputStream in =
      new ObjectInputStream(
        new FileInputStream("worm.out"));
    String s = (String)in.readObject();
    Worm w2 = (Worm)in.readObject();
    System.out.println(s + ", w2 = " + w2);
    ByteArrayOutputStream bout =
      new ByteArrayOutputStream();
    ObjectOutputStream out2 =
      new ObjectOutputStream(bout);
    out2.writeObject("Worm storage");
    out2.writeObject(w);
    out2.flush();
    ObjectInputStream in2 =
      new ObjectInputStream(
        new ByteArrayInputStream(
          bout.toByteArray()));
    s = (String)in2.readObject();
    Worm w3 = (Worm)in2.readObject();
    System.out.println(s + ", w3 = " + w3);
  }
}

On peut aussi utiliser la sérialisation (en passant par un ByteArray -- c'est une non-classe, bien sûr) pour copier profondément un objet (il existe une méthode clone(), mais pour des objets profonds, il faut l'implémenter soi-même).

Autre exemple (qu'est-ce que le XML vient faire ici ???)

XMLEncoder encoder = new XMLEncoder(out);
encoder.writeObject(bean);
encoder.close();

XMLDecoder decoder = new XMLDecoder(in);
Object b = decoder.readObject();
decoder.close();
bean = (javax.swing.JFrame) b;
bean.setVisible(true);

On a parfois besoin de contrôler ce qui est sérialisé dans un objet : pour cela, l'objet n'implémente plus l'interface Serializable mais l'interface Externalizable, qui contient deux méthodes, writeExternal() et readExternal().

Comparaison de la vitesse de Java avec d'autres langages

J'écris le même programme en Java, en Perl et en C, et je compare les vitesses d'exécution.

Premier exemple

En Perl :

#! perl -w
use strict;
my $x;
for(my $i=0; $i<1_000_000; $i++){
  $x = sin($i);
}

En Java :

class Boucle {
  public static void main (String args[]) {
    double x;
    for( int i=0; i < 1000000; i++ ){
       x = Math.sin(i);
    }
  }
}

En C (il faut compiler avec l'option -lm pour accéder à la bibliothèque mathématique) :

#include <math.h>
#include <stdlib.h>

int main(void) {
  int i;
  double x;
  for(i=0; i<1000000; i++) {
    x = sin(i);
  }
  return EXIT_SUCCESS;
}

Voici la vitesse d'exécution.

pczoonek% time ./Boucle
./Boucle  0,13s user 0,00s system 101% cpu 0,128 total

pczoonek% time perl Boucle.pl
perl Boucle.pl  0,60s user 0,00s system 100% cpu 0,598 total

pczoonek% time java Boucle
java Boucle  0,99s user 0,04s system 95% cpu 1,083 total

Perl est 5 fois plus lent que le C, et Java est deux fois plus lent que Perl. C'est anormal, car d'une part, pour Perl, il y a aussi le temps de compilation, d'autre part, Perl ne sait pas de $i est et restera un entier, et passe donc à côté de certaines optimisations [de manière interne, $i est représenté par une structure qui contient la valeur entière de la variable, la valeur décimale, et la valeur en tant que chaine, et tant qu'on n'a pas besoin de ces deux dernières, elles ne sont pas calculées -- on se contente donc d'envelopper un entier dans une structure].

Deuxième exemple

Essayons maintenant avec une boucle vide, en retirant l'appel de la fonction sinus (j'ai mis dix fois plus d'itérations pour que le temps mis par le programme en C ne soit pas nul).

pczoonek% time perl BoucleVide.pl
perl BoucleVide.pl  3,28s user 0,01s system 100% cpu 3,284 total

pczoonek% time java BoucleVide
java BoucleVide  0,26s user 0,04s system 87% cpu 0,343 total

pczoonek% time ./BoucleVide
./BoucleVide  0,02s user 0,00s system 111% cpu 0,018 total

Sur ces deux exemples, on constate que Perl est incapable d'effectuer certaines optimisations car il ne connait pas le type des variables qu'il manipule (ça sera corrigé dans Perl 6 -- même dans Perl 5, on peut dire à Perl que les variables sont des entiers, en ajoutant "use integer;" au début du programme, mais cela ne produit pas d'amélioration notable -- on prévoit que sur cet exemple particulier, Perl 6 sera presque 100 fois plus rapide que Perl 5).

On constate aussi, et c'est beaucoup plus surprenant, que les fonctions mathématiques sont implémentées n'importe comment dans Java.

Troisième exemple

Comparons maintenant les accès aux fichiers, les manipulations de chaines de caractères et les tables de hachage, à l'aide du programme d'exemple présenté plus haut (le lecteur comprendra que je n'essaye pas de le programmer en C : il s'agit de manipuler des chaines de caractères, or le C est le langage le pire que je connaisse pour ce genre de chose).

perl FilesAndHashes.pl data.txt  0,55s user 0,02s system 82% cpu 0,692 total
java FilesAndHashes data.txt  0,79s user 0,05s system 82% cpu 1,023 total

Perl est gagnant, sur cet exemple, il est 50% pus rapide que Java.

Autres comparaisons

(Le java utilisé dans le test suivant est du java compilé, naturellement plus rapide que le Perl interprété par une machine virtuelle.)

http://www.bagley.org/~doug/shootout/craps.shtml

Voir aussi

http://directory.google.com/Top/Computers/Programming/Languages/Comparison_and_Review/?il=1

Conclusion

Perl et java sont cinq à 100 fois plus lents que le C -- il ne faut donc pas les utiliser pour des tâches qui exigent une certaine rapidité. Dans certains cas, Perl est plus rapide, dans d'autres, c'est Java, mais les deux se compensent et au final la différence n'est pas significative.

Par contre, on remarquera que le programme en Perl est toujours plus compact.

Comme je connais bien Perl et mal Java, je ne peux pas comparer objectivement le temps passé à écrire ces programmes -- quelques minutes en Perl, quelques heures en Java, passées à écumer la documentation en pestant contre le fait que les tableaux ne sont pas des classes (donc on a du mal à les trouver dans la documentation) et contre le fait qu'il y ait des classes différentes pour faire exactement la même chose, de la même manière, mais avec des interfaces différentes (Hashtable et HashMap).

Jython

Jython est un interpréteur Python en Java, qui peut servir à débugguer des classe Java, à les tester, ou à permettre à l'utilisateur du logiciel de le configurer.

C'est une très bonne chose (mais il s'agit en fait de remplacer Java par un autre langage, plus efficace -- tout en continuant à dire à son employeur, sans mentir, qu'on fait du Java).

(J'aurais dû mettre des exemples plus détaillés, mais quand je me suis apperçu qu'il me restait encore à rédiger cette partie, j'avais déjà effacé Java de mon disque et je n'avais pas envie de m'y remettre.)

http://www.oreilly.com/catalog/jythoness/chapter/ch01.html
http://www.onjava.com/pub/a/onjava/2002/03/27/jython.html
http://www.onlamp.com/pub/a/python/2002/04/11/jythontips.html
http://www.opikanoba.org/accueil/intro.php

Comment avoir un programme correct...

JUnit

Pour écrire des tests de régression (quand on écrit un programme, on est parfois amené à changer l'implémentation de certaines parties, mais comme ces parties sont utilisées par le reste du programme, il faut prendre garde à ne pas tout casser, la partie modifiée doit toujours offrir les mêmes services : pour cela, on décrit son comportement et on écrit des tests pour vérifier qu'elle se comporte bien ainsi).

Every programmer knows they should write tests for their code. Few do. The
universal response to "Why not?" is "I'm in too much of a hurry." This
quickly becomes a vicious cycle- the more pressure you feel, the fewer
tests you write. The fewer tests you write, the less productive you are and
the less stable your code becomes. The less productive and accurate you
are, the more pressure you feel.

Après avoir écrit son code (ou même avant, en fait), on écrit des tests de la manière suivante

public class MoneyTest extends TestCase {
  private Money f12CHF;
  private Money f14CHF;

  protected void setUp() {
    f12CHF= new Money(12, "CHF");
    f14CHF= new Money(14, "CHF");
  }

  public void testEquals() {
    Assert.assertTrue(!f12CHF.equals(null));
    Assert.assertEquals(f12CHF, f12CHF);
    Assert.assertEquals(f12CHF, new Money(12, "CHF"));
    Assert.assertTrue(!f12CHF.equals(f14CHF));
  }

  public void testSimpleAdd() {
    Money expected= new Money(26, "CHF");
    Money result= f12CHF.add(f14CHF);
    Assert.assertTrue(expected.equals(result));
  }

  // // Calls all the test* methods (it uses reflexion)
  // this is the default definition: we do not need it.
  // public static Test suite() {
  //   return new TestSuite(MoneyTest.class);
  // }

}

Pour lancer les tests :

java junit.swingui.TestRunner MoneyTest

ou

java junit.textui.TestRunner MoneyTest

(L'argument peut être n'importe quelle classe avec une méthode suite().)

Logger

Quand je débuggue un programme, j'ai tendance à mettre des "print" partout pour bien voir ce qui se passe. Mais si le programme est gros et distribué, ou s'il utilise plusieurs Threads, ce n'est pas très pratique. On peut remplacer l'écriture sur la sortie standard par l'écriture dans un fichier.

import java.util.logging.*;
...
private static Logger logger =
  Logger.getLogger("InfoLogging");
...
logger.info("Logging an INFO-level message");

Par défaut, les messages apparaissent à l'écran (là où on a lancé la machine virtuelle Java).

On peut demander que les messages soient mis dans un fichier.

logger.addHandler(new FileHandler("LogToFile.xml"));

On peut même demander qu'ils soient mis dans un fichier sous une forme humainement lisible (sinon, c'est du XML).

FileHandler logFile = 
  new FileHandler("LogToFile2.txt");
logFile.setFormatter(new SimpleFormatter());
logger.addHandler(logFile);

débuggueur

Si on compile avec l'option -d, on peut ensuite lancer le débuggueur jdb (mais il y a des débuggueurs graphiques dans la plupart des IDEs).

Exemples de programmes à lire

Cherchez sur freshmeat, sourceforge ou savannah.

http://freshmeat.net/
http://sourceforge.net/
http://savannah.gnu.org/

Optimisation

Avant de chercher à optimiser le code, il faut d'abord avoir du code qui marche. Une fois que le code marche, on peut se demander s'il marche suffisemment vite. Si ce n'est pas le cas, il faut chercher à l'améliorer.

On peut réaliser des tests et regarder où la machine virtuelle passe le plus de temps, i.e., quelles sont les morceaux de programme qu'il faut améliorer en priorité. Pour cela, on peut utiliser la méthode System.currentTimeMillis(), qui permet de mesurer finement le temps écoulé.

On peut aussi utiliser divers profileurs (par exemple les options -prof, -Xprof, -Xrunhprof, -Xaprof, -Xhprof, -Xrunjcov:type=M, -Xrunhprof:heap=sites,cpu=samples,depth=10,monitor=y,thread=y,doe=y de la commande "java"), ou regarder comment fonctionne le ramasse-miettes (-verbose:gc).

Pour trouver des goulots d'étranglement, on peut aussi donner plusieurs ensembles de données au programme, construits différemment, et regarder si les temps sont comparables. Des variations signalent peut-être un goulot d'étranglement qui n'apparait qu'avec certains types de données.

Une fois le goulot d'étranglement identifié, il faut avant tout chercher à améliorer les algorithmes utilisés (c'est généralement ce qui permet de gagner le plus). Si ça ne suffit pas, on peut souvent accélérer le programme en compliquant la structure des données.

On peut aussi utiliser diverses autres optimisations, de plus bas niveau, comme par exemple, éviter d'utiliser des exceptions en dehors des situations exceptionnnelles (car ça prend beaucoup de temps), préférer StringBuffer à String, implémenter des caches (si l'activité du ramasse-miette le suggère), etc.

Il faut prendre garde à ne pas optimiser trop tôt (car le code devient plus difficile à maintenir, les efforts se dispersent et on risque de se retrouver avec un programme qui ne fonctionne pas) et à ne pas tout optimiser : il faut rejeter les modifications qui nuisent à la lisibilité du code en apportant qu'un gain marginal.

http://www-106.ibm.com/developerworks/java/library/j-javaopt/?dwzone=java
http://www.onjava.com/pub/a/onjava/2002/03/20/optimization.html
http://JavaPerformanceTuning.com/

Exemples de Frameworks

Une bibliothèque, c'est un ensemble de classes que l'on peut agencer exactement comme on veut pour réaliser un logiciel. Un Framework, c'est un ensemble de classes déjà arrangées pour réaliser un certain type de programme ; il reste néanmoins certaines classes à écrire, qui permettent d'adapter le programme à ses besoins. c'est donc un programme presque terminé ; le programme inachevé est généraliste mais il y a plueisuers manières très différentes de le programmer.

Exemples : des frameworks pour réaliser des serveurs Web ou des services Web (Enhydra, JBoss, Jonas (concurrent de JBoss)).

Exemple : Netbeans (un IDE, dont j'ai parlé un peu plus haut) peut être modifié en des programmes complètement différents, qui utilisent néanmoins la même structure : on peut donc le voir comme un Framework.

AspectJ

http://aspectj.org/doc/dist/progguide.pdf

Ce n'est pas une classe Java, c'est juste un ajout au langage Java (un peu comme C++ est un ajout au langage C). Il provient du principe qu'il y a des choses qui sont intrinsèquement transversales, et qu'on ne parvient pas à isoler dans des classes.

crosscutting is inherent to complex systems

En Emacs lisp, on peut ajouter des "hooks" aux fonctions : ce sont d'autres fonctions qui seront appelées juste avant (ou juste après). Par exemple, si la fonction latex-mode ne nous plait pas, on peut la modifier, sans avoir à chercher où se trouve la fonction pour la modifier ni où elle est utilisée pour mettre un autre nom de fonction.

Les aspects permettent de faire exactement la même chose, mais, contrairement à EmacsLisp, on peut donner un ensemble de fonctions (par exemple, toutes les fonctions de la classe Toto, toutes les fonctions du paquetage Tutu qui commencent par set*), ou se limiter à certains des appels d'une fonction (par exemple, les appels de la méthode Foo.bar() à l'intérieur de la classe Baz).

Un peu de vocabulaire. Join Point : un point dans le flux d'excution du programme. Pointcut : un ensemble de Joinpoints. Advice: le code exécuté à un Pointcut (en EmacsLisp : le code que l'on met dans le Hook). Introduction : modification d'une ou plusieures classes (on rajoute des méthodes : par exemple, un champ observers[] et des méthodes addObserver(), removeObserver()). Aspect : On va regroupper toutes ces choses dans un "aspect" (on en aura un pour le logging, un pour le débugguage, un pour la sécurité, etc.), exactement comme on regroupe le code dans des classes.

Exemple : On peut rajouter des instructions de débugguage à chaque fois qu'on appelle un certaine fonction (ou plusieures fonctions, par exemple les fonctions set* d'une certaine classe). On peut aussi rajouter des conditions et n'appeler ces instructions de débugguage que dans certains cas, par exemple quand les méthodes en question sont appelées depuis une classe donnée.

Exemple : On peut aussi utiliser les aspects pour le logging.

Exemple : On peut aussi utiliser les aspects pour implémenter la "programmation par contrat", chère à Meyer, que l'on rencontre en Eiffel.

Exemple : On peut aussi imposer certaines pratiques de programmation, par exemple utiliser uniquement une usine (factory) pour construire certains objets (en levat une exception à chaque appel de constructeur qui ne vient pas de l'usine).

Quand on a du code transversal, i.e., commun à plusieures fonctions/classes mais qu'on ne parvient pas à factoriser à l'aide de la POO, on peut utiliser les aspects. Mais attention : on ne peut plus lire le code à l'oeil nu, il faut un IDE qui nous dise qu'à certains endroits il y a un aspect qui intervient ! (Emacs, JBuilder, convenablement étendus, conviennent.) C'est le seul point qui m'effaye (pout le débugguage et tout ce qui ne comporte aucun effet de bord, ça me semble très bien, mais personnellement j'éviterais la POA à effets de bord).

L'intérêt, c'est qu'on ajoute du code en dehors des classes en question.

Choses dont je n'ai pas parlé

Ant : c'est le remplacement des Makefiles pour les projets en Java. Les fichiers sont en XML (ils sont donc pénibles à taper, difficiles à lire pour les êtres humains, et offrent une puissance bien moindre que les Makefiles (qui peuvent utiliser le shell)). A éviter, sauf bien sûr, si on utilise un IDE ou un framework qui les crée pour nous.

J2EE : j'ai l'impression que c'est un framework pour écrire des applications distribuées (l'équivalent des Services Web). C'est dans ce cadre qu'on rencontre des Beans (les EJB : enterprise Java Beans) (sinon, les beans ne servent pas à grand-chose). Il y a différentes implémentations de ce framework, JBoss en est une.

Entity Bean : un bean qui représente des données dans une base de données (équivalent pour Enhydra : un objet dans la couche Données).

Session Bean : un bean qui fait quelque chose (équivalent pour Enhydra : un objet dans la couche Business).

JSP, Servlet : quand on fait de la programmation Web, on sépare la présentation (on la met dans dex fichiers JSP), le traitement des requètes (dans un servlet, i.e., un serveur, i.e., un programme qui attend des connections) et le reste (la couche Business, dans laquelle vivent les Beans) Un fichier JSP ressemble à :

<%@ page language="java" contentType="text/html"> %>

<jsp:useBean class="Foo.Bar">
  <jsp:setProperty name="userInfo" property="userName"/>
</jsp:useBean>

Comparaison avec C# : C# a été conçu pour être le concurrent de Java. Comme il bénéficie de l'expérience de Java, il ne doit pas en reproduire les erreurs. Néanmoins, développer en C# signifierait (du moins pour l'instant) se limiter à la seule plateforme Windows, sans aucun espoir de migration. A éviter tant que la portabilité n'est pas au moins au niveau de Java (et Java est loin d'être un exemple pour ce genre de chose !).

On trouve parfois le caractère $ dans les noms de fichiers : il indique les classes internes (la syntaxe est ClasseExterne$ClasseInterne.class si la classe interne a un nom, ou ClasseExterne$1, $2, etc., si elle est anonyme).

Vincent Zoonekynd
<zoonek@math.jussieu.fr>
latest modification on lun oct 28 09:25:47 CET 2002