Mémo Java
Michel Llibre - Août 2022
11.8 Accès aux fonctions membres de la classe java appelante |
Pour compiler et exécuter du java il faut disposer d'un ensemble de logiciels appelé JDK (Java Development Kit) qui est actuellement fourni par https://www.oracle.com, sous l'appellation Java SE Development Kit (SE signifie Standard Edition).
Une fois téléchargé, on installe le dernier JDK dans un répertoire de notre choix, par exemple le répertoire C:\Applis, puis il faut définir la variable système JAVA_HOME et path pour utiliser le kit :
set JAVA_HOME=C:\Applis\JavaJdk1.7.0_02
path=%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin;%path%
On pourra mettre ces deux lignes dans un fchier de commande ini_java.bat à appeler avant toute utilisation du kit.
Actuellement ce n'est plus JavaJdk1.7.0_02 mais jdk-18_windows-x64.
Les programmeurs java utilisent des centaines de fonctionalités (incluses dans des entités appelées classes) fournies avec le JDK. Pour pouvoir les utiliser, il faut avoir accès à la documentation des classes de l'API java (API = Application Programming Interface : ce sont les programmes qui permettent d'accéder aux services de la machine et de communiquer avec elle et ses périphériques). Au fur et à mesure qu'apparaissent de nouvelles versions de la Standard Edition du JDK (JDK SE 18 quand j'écris ce paragraphe) la documentation devient de plus en plus complexe à consulter. Heureusement, Oracle fournit toujous la documentation du JDK SE 7 à cette adresse https://docs.oracle.com/javase/7/docs/api/ qui à l'avantage d'offrir une liste alphabétique de toutes les classes du JDK SE 7. Les documentations pour le JDK SE 11 et JDK SE 18 se trouvent ici :
- https://docs.oracle.com/en/java/javase/11/docs/api/
- https://docs.oracle.com/en/java/javase/18/docs/api/index.html
mais elle sont moins pratique à utiliser.
Un fichier fichier source java, d'extension .java définit un ensemble de données et fonctions qu'on appelle une classe. Un fichier de ce type, Toto.java par exemple, définit une classe publique unique qui s'appelle du même nom Toto.
Un programme du JDK nommé "javac" (compilateur java) permet de traduire le fichier Toto.java en un fichier Toto.class qui contient ce qu'on appelle le bytecode qui est le volet interprétable de cette classe. Un deuxième programme du JDK nommé "java" permet d'exécuter les instructions contenues dans cette classe.
A la compilation, il y a autant de fichiers créés que de classes définies. Ces fichiers portent le nom des classes avec l'extension .class.
Exemple de fichier source un.java :
public class un
{
static int age = 11;
public static void main(String args[])
{
System.out.println("age + 3 = " + calcul.somme(age, 3));
}
}
class calcul
{
public static int somme(int a, int b) { return a+b;}
}
La syntaxe générale pour compiler est :
javac -d rep_destination fichier.java
soit pour compiller un.java dans son répertoire courant (qui est représenté par le caractère ".") :
javac -d . un.java
La compilation génère un fichier .class par classe, à savoir un.class et calcul.class, mais seule la classe un qui est publique est accessible de l'extérieur, en exécution par la commande suivante :
java un
Le résultat obtenu est :
age + 3 = 14
Dans cet exemple, on voit que notre fichier un.java contient deux classes (un et calcul) dont une seule est publique. La compilation génère autant de fichiers .class qu'il y a de classes, à savoir 2 dans notre cas : un.class et calcul.class.
Lorsqu'on exécute la commande "java un", c'est la fonction main de la classe un qui est appelée. Si elle n'est pas présente, la compilation javac -d . un.java se déroule normalement, mais on aura le message d'erreur suivant à 'exécution
Erreur : la méthode principale est introuvable dans la classe un, définissez la méthode principale comme suit :
public static void main(String[] args)
....
Remarque : Si l'écriture sur la console est simple, la lecture des caractères du clavier est plus délicate. Voir en 9.5.
- Les classes sont des entités qui contiennent des données et des fonctions et éventuellement des classes internes .
- Le nombre de classes disponibles étant devenu très important, on regroupe ces classes en paquets logiques appelés packages. Les classes appartenant à des packages ont deux noms, un nom court, par exemple Toto et un nom long, par exemple nompaq.Toto, où nompaq est le nom du package auquel appartient la classe Toto. Pour pouvoir utiliser le nom court, on met en début de fichier source :
import nompaq.toto;
On peut également accéder à toutes les classes du package nompacq par leur nom court en ajoutant l'instruction :
import nompaq.*;
Exception : Les classes du package java.lang sont accessibles par leur nom court sans qu'il soit nécessaire de le demander par l'instruction import.
Toutes les classes de java livrées avec le compilateur sont dans des packages dont les noms commencent par java.
Pour affecter la classe que l'on crée au package nompaq, on débute le fichier source de la classe par :
package nompaq;
Une classe qui n'est affectée à aucun package appartient au paquet sans nom (unnamed package).
Si la variable d'environnement classpath est vide, les classes usagers sont recherchées à partir du réperoire courant. Si elles se trouvent ailleurs, il faut le spécifier par la variable d'environnement classpath par :
set classpath=.;<chemin1>;<chemin2> sous Windows.
setenv classpath .:<chemin1>:<chemin2> sous Unix.
A leur tour, le nombre de packages étant devenu très important, depuis la version on peut les regrouper en Modules. Un module peut se présenter sous la forme d'un répertoire ou d'un fichier JAR. Un projet utilise généralement plusieurs modules. Ces Modules sont surtout utilisés pour les transports de logiciel ou lorsqu'on conçoit une application destinée à être mise sur le marché.
Instance, instancier. Une instance est une variable qui représente un élément qui peut être de deux types :
•primitif comme un nombre entier, un nombre réel,
•complexe comme une chaine de caractère, une personne...
Les types complexes correspondent à des classes préexistantes comme la classe String, ou créées par notre programmation, comme la classe Personne par exemple. Dans un programme on pourra créer plusieurs variables du type Personne, par exemple pierre, paul, jules... On dit que pierre, paul, jules sont des instances de la classe Personne. On le fera par exemple par l'instruction suivante :
Personne pierre, paul, jules;
On dit également que pierre, paul, jules instancient la classe Personne.
Exception : En informatique une exception est un interruption du déroulement normal du programme provoqué par une instruction inadéquate dans le contexte courant. Le programme ne peut donc plus se dérouler normalement. Heureusement, quand on se doute que ceci peut arriver au niveau de certaines instructions, il existe dans Java un mécanisme d'essai d'exécution qui consiste à mettre la séquence litigieuse dans un bloc précédé de l'instruction try() {... séquence d'instructions à essayer d'xécuter ..}. Et après ce bloc on met l'instruction catch(Exception e) {... instructions à exécuter si une erreur est survenue...}
Les noms de classe commencent par une majuscule : Vector.
Les noms de package commencent par une minuscule : java.util dans lequel on trouve la classe java.util.Vector.
Les noms de variables et de méthodes commencent par une minuscule : Point.x, Object.toString().
Les variables publiques finales (constantes) sont entiement écrites en majuscules.
Il y a 8 types primitifs : (valeur initiale utilisée à la création)
byte = entier signé 1 octet (0)
short = entier signé 2 octets (0)
int = entier signé 4 octets (0)
long = entier signé 8 octets (0)
float = réel 4 octets (0.0)
double = réel 8 octets (0.0)
char = caractère unicode 2 octets ('\u0000')
boolean = booléen au moins 1 bit (false)
Si la variable x est associé à un type primitif, l'expression x représente sa valeur, c'est-à-dire le contenu courant de la variable. Un type primitif est toujours accédé par valeur.
Si elle est associée à un objet de classe, l'expression x réprésente une référence à une instance de cette classe. Un objet est toujours accédé par référence. Contrairement au C++, une référence peut être modifiée (comme un pointeur C++) et pointer sur une nouvelle instance.
Si p et q sont deux références,
p = q ;
ne duplique l'objet référencé par q, mais produit un double référencement de cet objet par les références p et q (toute modification de l'objet référencé est accessible par p).
Pour effectuer la copie d'un objet de la classe Klass on utilise :
p = (Klass) q.clone();
La méthode clone existe pour tous les objets. Elle est définie une première fois dans la (super) classe Object. et peut être redéfinie dans la classe Klass.
L'expression :
p == q ;
n'est vraie que si p et q référencent rigoureusement le même objet (égalité des adresses).
L'égalité de deux objets est testée par la méthode :
p.equals(p) ;
définie une première fois dans la (super) classe Object.
Exemple :
int t1[] = {1,2,3};
int [] t2 = t2 ; // deuxième référence sur le tabeau {1,2,3}
t1[1] = 4 ; // Le tableau devient {1,4,3} ;
System.out.println(" " + t2[1]) ; // Renvoie 4 car t2 pointe sur le tableau qui a été modifié. !!
Les variables d'instances de type primitif sont automatiquement initialisées à zéro pour les numériques et false pour les booléennes.
Tous les éléments qui ne sont pas de type primitif sont des objets qui sont des instances d'une classe fournie par le JDK ou construite par le programmeur, par exemple la classe String qui représente des chaines de caractères comme "Bonjour tout le monde".
Si on veut des renseignements sur une classe du JDK, on peut les trouver à cette adresse :
https://docs.oracle.com/javase/7/docs/api/
Une instance s de String est simplement déclarée comme suit :
String s;
Cette instruction déclare simplement que la variable s sera utilisé pour stocker des chaines de caractères, mais en ce moment elle n'existe pas vraiment et on ne peut appeler aucune de ses méthodes. Il faut d'abord l'initialiser, par exemple par l'instruction suivante :
s = new String("toto");
Ou plus simplement, faire les deux opérations simultanément, à savoir :
String s = new String("toto");
C'est l'opérateur new qui crée véritablement l'objet en réservant sa place mémoire.
Un objet possède des données et des fonctionnalités qu'on appelle méthodes (fonctions, procédures...). Par exemple l'objet s de la classe String possède la chaine "toto" dans ses données, mais peut-être aussi l'entier 4 qui est le nombre de caractères de la chaine, et peut-être d'autres données ? et dans ses méthodes, il y a par exemple la méthode contains qui renvoie true ou false suivant que la chaine contient ou ne contient pas les caractères cherchés, par exemple s.contains("t") va renvoyer true et s.contains("a") va renvoyer false.
Toutes les classes dérivent de la super classe de base Object (java.lang.Object plus précisément).
La notion dedérivation est précisée plus loin, mais disons pour commencer que les méthodes de cette classe sont également disponibles dans TOUTES LES CLASSES.
•protected Object clone() : crée et renvoie une copie de l'objet.
•boolean equals(Object obj) : indique si un autre objet est le même que celui.ci
•protected void finalize() : appel du nettoyeur (garbage collector).
•Class<?> getClass() : renvoie le nom de la classe de l'objet (au runtime).
•int hashCode() : renvoie un hash code pour l'objet.
•void notify() : réveille un thread en attente de cet objet.
•void notifyAll() : réveille tous les threads en attente de cet objet.
•String toString() : renvoie une représentation en String de cet objet.
•void wait() : met le thread courant en pause jusqu'à ce qu'un autre thread émet un notify() ou notifyAll() sur cet objet.
•void wait(long timeout) : idem, mais se réveille de toute façon au bout du timeout (ms).
•void wait(long timeout, int nanos) : idem, mais se réveille de toute façon au bout de timeout*106 + nanos (10-9 s).
Cette classe n'a aucune donnée associée. Pour de la programmation de débutant seules les méthodes clone, equals et toString sont à considérer.
La classe String est en fait la classe java.lang.String.
Les éléments v de type primitif (char, int, long, float, double…) n'étant pas des objets n'ont pas de méthode toString disponible. Ils peuvent être convertis en String par la méthode String.valueOf(v).
Les classes Byte, Short, Integer, Long, Float, Double et Boolean (en fait java.lang.Byte, java.lang.Short...) encapsulent les 8 types primitifs byte, short, int, integer, long, float, double et boolean.
Ces classes (notées ci-après Zzz, Yyy,...) possèdent toutes une variable d'instance v du type primitif correspondant avec :
•un constructeur Zzz()
•une méthode yyyValue() qui permet d'obtenir la valeur v sous un autre format,
•une méthode toString() qui transforme v en String ,
•une méthode valueOf(String s) initialisant v à partir de l'expression de la valeur v sous forme d'un String.
Ces classes permettent d'encapsuler les types primitifs là où des instance de classe sont attendus. En particulier comme les types primitifs sont passés en argument de routine par copie, si on veut les faire modifier par la routine, on utilisera le passage par référence au moyen de la classe qui l'encapsule, ce qui permettra de modifier sa valeur et de récupérer la valeur modifiée.
En plus de la création traditionnelle d'une instance de ces types primitifs, comme suit :
Integer unEntier = new Integer(12345);
avec new suivi du constructeur Integer(int v), il existe pour ces classes le raccourci suivant :
Integer unEntier = 12345;
La classe Vector (java.util.Vector en vérité) ne permet que de mémoriser des instances de classe. Si on veut y mémoriser un type primitif, on le fera par l'intermédiaire de la classe qui l'encapsule. Exemple pour mettre des entiers dans un vecteur :
Vector w = new Vector();
ne permet de mémoriser qu'une collection d'objets. On y ajoute la valeur i d'un int en le convertissant en objet Integer :
w.add(new Integer(i));
Remarque :
Pour convertir un String en Double, Float, ... on dispose de 2 méthodes statiques, par exemple pour Double :
d = Double.parseDouble(s);
ou
d = Double.valueOf(s);
Pour info, les sources de ces 2 méthodes sont :
public static double parseDouble(String s) throws NumberFormatException {
return FloatingDecimal.readJavaFormatString(s).doubleValue();
}
public static Double valueOf(String s) throws NumberFormatException {
return new Double(FloatingDecimal.readJavaFormatString(s).doubleValue());
}
La première parseDouble renvoi un type primitif double, alors que la deuxième valueOf l'encapsule dans un objet Double. Si on n'en a pas besoin, la première est plus économique.
Remarque : Pour remplacer la virgule par un point dans un string que l'on va convertir en nombre, on peut faire :
str = str.replace(",", ".");
au préalable.
Attention ! Ce paragraphe concerne java sous Android, mais peut être inexact pour le java standard.
L'écriture et la lecture des nombres décimaux pose un problème à cause de l'interprétation du carctère décimal (".") ou (",") selon le cas, géré de façon très complexe par la classe Locale.
Les routines de conversion String vers Double ou Float de base, citées au paragraphe précédent supposent que le séparateur décimal est un point.
Pour la conversion inverse Double ou Float vers String le plus simple est de faire la même chose. Pour cela on utilisera la classe DecimalFormat qui permet de faire ces conversions en spécifiant le formatage (nb de chiffres après la virgule) et le caractère décimal. Exemple :
DecimalFormatSymbols sb = new DecimalFormatSymbols();
sb.setDecimalSeparator('.');
DecimalFormat df = new DecimalFormat("0.0", sb);
où le premier argument signifie que l'on veut un seul chiffre après la virgule (le point avec le zéro est insuffisant pour spécifier le séparateur décimal) et le deuxième argument sb spécifie le caractère qui sera utilisé comme séparateur décimal. On utilise ensuite ce DecimalFormat df comme ceci :
String str = df.format(nombre);
string que l'on peut utiliser à sa convenance pour afficher le nombre et qui pourra ensuite être re-converti en nombre sans erreur par la méthode Double.parseDouble(str) ou Double.valueOf(str).
Pour la conversion string -> nombre on peut utilisera une instance nf de la classe NumberFormat, classe mère de DecimalFormat, qui offre les méthodes nf.parse(str).doubleValue(), nf.parse(str).floatValue(), etc...
Les tableaux ne sont pas des classes, mais ont pratiquement les mêmes propriétés.
Deux références sur des tableaux monodimensionnels tab1 et tab2 de type Tipe (type primitif ou classe) sont créées indifféremment par :
Tipe tab1[], tab2[] ;
ou par :
Tipe[] tab1, tab2 ;
Ces références sont créées, mais pointent sur null. Un tableau de n éléments (initialisation par défaut de ses éléments à zéro) est effectivement créé par :
tab1 = new Tipe[n] ;
Raccourci :
Tipe[] tab1 = new Tipe[n] ;
Remarque : on accède au cardinal de tab1 par :
m = tab1.length ;
où length est une variable d'instance finale (constante) des classes tableaux.
Les exceptions suivantes peuvent survenir :
IndexOutBoundsException si l'indice est hors limite,
OutOfMemoryError si l'allocation (par le new) échoue.
Déclarations équivalentes de la référence à un tableau multi de type Tipe :
Tipe mat[][] ;
Tipe[] mat[];
Tipe [][] mat;
Allocation des tableaux. Indifféremment :
mat = new Tipe[n][m];
ou bien :
mat = new Tipe[n]; for(int i=0;i<n;i++) mat[i]=new Tipe[m];
Cette deuxième méthode permettrait de définir un tableau dont les "lignes" ont des nombres de "colonnes" différentes.
Dans tous les cas :
mat référence la matrice,
mat[i] référence la iième "ligne" de la matrice
mat[i][j] valeur du (si type primitif) ou référence sur le jième élément de la iiéme ligne.
Chaînes de caractères
Les instances de la classe String sont des chaînes invariables qui ne peuvent plus être modifiées après leur création.
Quelques méthodes de la classe String :
int length(); // donne la longueur
char charAt(int i); // Accès au iiéme caractère
boolean equals(String s); // Comparaison
boolean equalsIgnoreCase(String s); // Evident!
boolean compareTo(String s); // Encore ..
Pas de méthode clone() car recopie inutile puisque invariables. Il suffit de dupliquer les références.
La méthode format permet de créer des chaines dynamiques. Exemple :
String.format("Bonjour %1$s tu as %2$d ans", "Michel", 10);
Les arguments sont annoncés par le prototype %n$x où n est le numéro de l'argument (débute à 1) et x est son type, avec s pour string, d pour entier, ...
Toutes les classes ont une méthode
String toString() ;
définie la première fois dans la classe Object, qui permet d'associer un String à l'objet. Ainsi, les 2 instructions suivantes sont équivalentes :
System.out.println(x);
System.out.println(x.toString());
L'opérateur "+" permet de concatèner les chaînes. Exemple :
float x; ......
String s = "distance : " + x + " Km";
équivalent à :
String s = "distance : " + x.toString() + " Km";
car conversion automatique en String, dès qu'un des opérandes du + en est un.
Les instances de la classe StringBuffer sont des tableaux de caractères qui pourront être modifiés.
Quelques méthodes de la classe StringBuffer :
StringBuffer append(Tipe x); // Conversion x en chaîne puis concaténation
int length() ; // donne la longueur
void setLength(int n);
StringBuffer insert (int pos, Tipe x) ; // Insertion
StringBuffer delete (int debut, int fin) ; // Suppression
Dans une méthode du genre maFonction (int a, Klass b, int []c), les arguments sont passés :
•par copie pour les types primitifs comme pour int a
•par adresse de la référence pour les types classe et tableau, comme b et c.
Lors de la sortie du langage Java ses concepteurs ont mis en avant l'aspect sécurité que pouvait offrir Java (voir le paragraphe sur la programmation objet et l'encapsulation) par rapport aux langages précédents. En vérité, les méthodes sont de véritables passoires qui permettent de modifier tous les arguments à l'exception de ceux de type primitif.
Ainsi, effectivement, la valeur de la variable primitive a, qui peut être modifiée dans maFonction, ne le sera pas à l'extérieur lors du retour chez l'appelant. Par contre si, dans maFonction, on modifie les éléments de la classe b et le contenu du tableau c , on les modifie également à l'extérieur de maFonction et ils seront différents au retour, chez l'appelant.
Par contre si dans on fait b = new Klass(...) et qu'on triture cet objet b, au retour chez l'appelant c'est l'ancien b qui sera toujours en place.
En effet b pointe un emplacement de la mémoire dans lequel on a mis, en appelant maFonction ,une copie des adresses des emplacements des éléments de l'objet original. Lorqu'on modifie un champ b.truc, on modifie vraiment le champ de l'objet original. Par contre quand on fait b = new Klass(...), on remplace la copie des adresses des emplacements des éléments de l'objet original par les adresses des emplacements des éléments du nouvel objet créé. L'objet original n'est plus atteignable.
L'ellipse dont le prototype de la fonction se présente comme ceci : maFonction(KlassA a, KlassB... b), c'est-à-dire avec 3 points qui suivent le nom de la classe du dernier argument, a un fonctionnement particulier. Dans le prototype elle doit toujours se situer en dernier argument (ou unique).
L'ellipse indique que pour cet argument la fonction appelante va mettre 0, 1, 2, .., n arguments, là où la fonction appelée va recevoir un tableau de KlassB contenant 0, 1,2,..,n éléments.
Dans le cas du prototype précédent, les différents appels suivants sont possibles :
maFonction(a); // Pas d'argument b
maFonction(a,b1); // un argument b
maFonction(a,b1,b2,b3); // plusieurs arguments b
Lorsque qu'un argument b (ou plusieurs) est (sont) présent(s), on y accède comme si c'était des éléments d'un tableau b[] : à savoir par b[0] pour b1, b[1] pour b2 et b[2] pour b3.
Une classe est une structure réunissant des données parfois appelées des attributs (il peut ne pas y en avoir comme dans la classe Object) et des fonctions appelées des méthodes (il peut ne pas y en avoir).
Exemple :
class Personne {
String nom; int age;
Personne(){}
Personne(String _nom, int _age) {nom = _nom; age = _age;}
void info(){System.out.println("Je m'appelle " + nom + ". J'ai "+age+" ans");}
}
Ici la classe personne a 2 attributs : le String nom et l'int age , et une méthode la méthode info qui ne prend aucun argument () et qui renvoie void, c'est-à-dire rien. Elle affiche le nom et l'age sur la console.
Remarquons des méthodes particulières qui ne renvoient rien, même pas void, et qui portent le nom de la classe. Ces méthodes Personne(..) sont appelés des constructeurs. Il en faut au moins un si on désire créer des objets instances de cette classe.
Voici une classe d'essai qui se sert de la classe Personne :
public class Essai {
public static void main(String args[])
{
Personne p = new Personne("Thomas", 11);
p.info();
Personne q = new Personne();
q.info();
}
}
et voici ce que l'exécution affiche sur la console :
Je m'appelle Thomas. J'ai 11 ans
Je m'appelle null. J'ai 0 ans
On voit que le constructeur sans argument a quand même créé un objet. Il a initialisé l'attibut primitif age à 0 et mais la référence nom n'est pas initialisée : (q.nom == null) renverra true.
La simple déclaration d'un objet (non primitif) ne réserve la mémoire que pour la référence à l'objet. Par exemple l'instruction Personne p; ne fait que déclarer le nom de variable p qui permettra ensuite de faire référence à l'objet que cette variable représentera, c'est pour cela qu'on appelle ce nom une référence. Mais l'instruction précédente n'alloue, à ce stade, aucune mémoire pour p. Ce nom ne peut donc pas être utilisé sauf pour créer l'objet, comme ci-après.
L'allocation de la mémoire nécessaire se fait par l'appel d'un constructeur :
p = new Personne("Thomas", 11);
Lorsque l'objet n'est plus référencé par aucune instruction, par exemple lorsque l'exécution sort du champ d'une méthode pour un objet local crée dans cette méthode, le ramasse-miettes de java peut le détruire l'objet (à son gré) et s'il le fait il appelle la méthode finalize() de la super classe Object. Remarque : Il est possible de redéfinir cette méthode.
A l'intérieur d'une classe, this représente l'objet instance de cette classe. Ainsi à l'intérieur de la classe Personne, this.nom et nom sont synonymes ainsi que this.age et age. Cela est utile, par exemple dans le constructeur si on a utilisé les mêmes noms pour les arguments et les attributs :
Personne(String nom, int age) {this.nom = nom; this.age = age;}
A l'intérieur de la fonction les nom et age passés en argument ont priorité sur nom et age attributs de la classe. Pour faire référence à ces attributs il faut utiliser leur appellation sous la forme this.nom et this.age.
Ce qualificatif permet de différencier les membres en deux types : les membres de classe et les membres d'instance.
Un membre ordinaire, qui n'est pas précédé du qualificatif static est un membre d'instance. Chaque instance de la classe possède ses propres variables d'instance. Les méthodes d'instance ne sont appelables qu'à travers une instance de la classe. Dans le corps d'une méthode d'instance l'objet à travers lequel elle est appelée est accessible à travers la constante d'instance this.
Un membre précédé du qualificatif static est appelé membre de classe. Un membre de classe est commun pour toutes les instances de la classe.Il est donc unique pour cette classe.
On peut accéder à sa valeur en faisant précéder son nom du nom d'une instance de la classe ou plus simplement par le nom de la classe elle-même comme Klass.x (si c'est un attribut) ou Klass.truc() (si c'est une méthode) et par son nom seul dans le champ de la classe, sans le mot clé this.
Une méthode déclarée static ne peut faire référence qu'à des variables et méthodes déclarées static. Elle ne peut donc pas faire référence à this qui représente une instance de la classe.
Une méthode déclarée static ne peut être redéfinie dans les classes dérivées.
Un bloc de code peut être déclaré static. Il ne sera exécuté qu'une fois lors du chargement de la classe par le ClassLoader. On peut utiliser ces blocs, par exemple, pour initialiser des variables statiques complexes.
On peut qualifier une classe interne de static. Elle n'est plus liée à l'instance de la classe conteneur. Elle peut déclarer des contextes statiques contrairement à une classe interne ordinaire. Mais elle ne pourra plus utiliser les membres d'instance de la classe conteneur (sauf si elle a récupéré une référence sur celle-ci pour y accéder), mais seulement les membres de classe (cad les membres statiques), alors qu'une classe interne ordinaire y accéde de manière transparente.
Une classe interne static ne contient que des variables et méthodes static (il est alors inutile de le préciser pour celles-ci) qui peuvent être directement appelées en les préfixant du nom de la classe.
Pour que l'initialisation ne soit faite qu'une seule fois et pas à chaque création d'une instance, on a intérêt à effectuer l'initialisation des variables static dans un bloc précédé du mot static, situé n'importe où dans la définition de la classe, mais après la déclaration des variables static à initialiser (sinon leur portée serait réduite au bloc). Exemple :
public class Maclass
{
...
static final Truc LETRUC;
static Bidon lebidon ;
...
static
{
LETRUC = new Truc(...) ; /* initialisé par son constructeur par ex */
lebidon = new Bidon(...) ;
...
}
...
}
En C++, le constructeur de la classe Pixel (à trois membres x,y,couleur), dérivé de la classe Point (à deux membres x,y) s'écrit :
Pixel(int a, int b, Color c) : Point(a, b) {couleur = c;} // Cest du C++
Cela devient en java :
Pixel(int a, int b, Color c) {super(a,b); couleur = c;} // C'est du java
L'instruction super() si elle est présente doit être la première du constructeur.
Normalement, on ne s'en occupe pas en Java. C'est le système qui gère cela automatiquement.
On peut modifier le comportement standard lors de la destruction de l'objet par le garbage collector en surchargeant la méthode finalize dont le prototype est :
protected void finalize() throws Throwable.
Le complement throws Throwable signifie qu'il faut faire cette surcharge dans un bloc try car l'exécution de finalize peut générer une exception.
Si on veut récupérer la place mémoire à tout prix, avant le réveil automatique du garbage collector, on peut assigner l'objet inutilisé à null et forcer l'appel au garbage collector comme suit :
toto = null;
System.gc();
Nous avons déjà vu qu'un attribut static était un membre unique, le même pour tous les objets de la classe.
Nous avaons également qu'une méthode static était exécutable sans qu'il y ait besoin de créer un objet instance de cette classe, en appelant la méthode en la préfixant du nom de la classe.
Remarquons que la méthode main appelé par l'interpréteur java est déclarée static.
Un membre d'une classe qui est déclaré public est accessible depuis toutes les classes (à condition bien sur que la classe y soit accessible).
Un membre d'une classe déclaré private n'est accessible qu'à l'intérieur des méthodes de la classe.
Un membre sans mention de visibilité est accessible depuis l'intérieur de toutes les classes qui sont dans le même package que sa classe.
Remarque : Une classe qui n'appartient à aucun package, appartient au package sans nom, et ainsi toute classe sans indication peut accéder à toutes les classes dans le même cas.
Un membre d'une classe déclaré protected est accessible depuis l'intérieur des méthodes de la classe, des classes qui sont dans le même package que la classe, et des classe qui étendent la classe, même dans d'autres packages (ce qui est plus large que les membres sans aucun qualificatif de visibilité).
Ce qualificatif ne s'applique pas aux attributs. Il s'applique à plusieurs méthodes d'une même classe. Des méthodes d'un même objet peuvent lorsqu'elle sont exécutées dans des threads différents s’interrompre et s'exécuter ainsi sur une configuration de l'objet inadéquate (certains attributs calculés et d'autre pas). Pour éviter cela, on déclare ces méthodes synchronized et elles ne pourront plus s’interrompre mutuellement.
Mais si une méthode synchronized, appelle une autre méthode de la classe qui ne l'est pas, une interruption par un appel depuis un autre thread au moyen d'une méthode (synchronized ou non) de l'objet peut passer, d'où l'intérêt de déclarer toutes les méthodes de la classe synchronized en déclarant la classe synchronized.
Bloc synchronized
Une autre possibilité pour faire de l'exclusion mutuelle, c'est-à-dire faire qu'une portion de code exécutée par un thread ne puisse pas être interrompue pour être exécutée par un autre, c'est de déclarer cette portion synchronized comme ceci :
synchronyzed (verrou) {bloc d'instructions}
L'objet verrou utilisé peut être un attribut quelconque de la classe ou un attribut spécialement déclaré pour cette utilisation comme ceci :
private final Object verrou = new Object();
Le bloc d'instructions ne sera exécuté par un thread que si l'objet a pu être verrouillé. Pendant la durée de l'exécution du bloc d'instructions l'objet est inaccessible aux autres threads du programme.
Une primitive (variable ordinaire) déclarée final ne peut plus être modifié (c'est une constante).
Un handle d'objet déclaré final ne peut plus être modifié (ne plus plus être affecté à une autre instance), alors que l'objet lui, peut être modifié.
Exemple :
final int a[] = {1,2,3};
int []b = {4,5};
a[1]=8; // autorisé car a pointe toujours le tableau
a = b; // interdit car a ne pointerait plus le tableau initial
b = a; // autorisé, a n'est pas altéré
Une méthode déclarée final ne peut pas être redéfinie dans les classes dérivées. Le fait de la déclarer final permet un accès plus rapide à la méthode lors de l'éxécution (pas de recherche dynamique).
Une méthode native est écrite dans un autre langage, généralement C ou C++.
La sérialisation est une opération d'écriture des valeurs de tous attributs d'une classe. Les attributs transient (transitoires) ne sont pas transférés lors des serialisations.
Les variables volatile ne doivent pas faire l'objet d'une optimisation (pour garantir l'accès à l'unique et bonne représentation d'une variable utilisée par plusieurs processus).
Une méthode abstract n'est pas définie. Elle le sera dans une sous-classe.
Une classe qui contient une méthode abstract doit être déclarée abstract, mais une classe peut être déclarée abstract, même si elle ne contient pas de méthode abstract.
Une classe peut être interne ou externe. Une classe interne est une classe déclarée à l'intérieur d'une autre classe. Elle possède les mêmes possibilités qu'une autre classe, mais elle est toujours dépendante de sa classe conteneur et ne peut être utilisé que par la classe conteneur. Elle peut accéder de manière transparente aux membres de l'instance de la classe dont elle fait partie (sa classe conteneur). Une classe interne peut être qualifiée de private.
Une et une seule classe publique par fichier. Le fichier porte forcément le nom de cette classe avec l'extension .java.
Une classe publique peut être accédée de n'importe quelle autre classe. Une classe non publique (classe auxilliaire locale) ne peut être accédée que des classes du même package. Les membres (variables ou méthodes) des méthodes publiques sont accessibles partout ou la classe est accessible.
Les classes, interfaces, variables et méthodes public peuvent être utilisés sans restriction. La classe contenant la méthode main doit être public.
Une classe qui n'est pas déclarée publique ne sera accessible qu'aux classes du même paquetage.
Remarque : Une classe qui n'appartient à aucun package, appartient au package sans nom, et ainsi toute classe sans indication peut accéder à toutes les classes dans le même cas.
Seule une classe interne peut-être déclarée static.
De cette classe, on ne peut accéder qu'aux membres statiques de la classe externe.
On peut accéder à ses membres sans créer d'objet instance de cette classe.
Intérêt ?
Dans une classe déclarée synchronized toutes les méthodes sont synchronized.
Une classe déclarée final ne peut être ni étendue ni clonée.
Une classe déclarée abstract est une classe qui va servir de prototype pour des classes dérivées. On ne peut pas déclarer d'objets instance de cette classe. Elle ne sert qu'à déclarer les membres que devront comporter les classes qui héritent d'elle.
Une classe qui contient une méthode abstract doit être déclarée abstract, mais une classe peut être abstract, même si elle ne contient pas de méthode abstract (elle ne définira que la liste des attributs imposés à ses héritières).
En programmation à l'ancienne, les attributs des classes sont soit sans qualificatif, soient déclarées avec le qualificatif public, ce qui permet d'accéder à leur valeur et de la modifier à partir de leur nom, comme dans l'exemple suivant :
package getset;
public class Personne {
public String nom;
public String prenom;
public int age;
}
Tous les attributs de cette classe sont déclarés public. La classe Main suivante peut accéder à ses attibuts et les modifier.
package getset;
public class Main {
public static void main(String[] args) {
Personne pers = new Personne();
pers.nom = "Joseph";
pers.prenom = "Joe";
pers.age = 30;
System.out.println("Je suis " + pers.prenom + " " + pers.nom + " et j'ai " + pers.age + " ans");
}
}
Cette facilité est combattue par les adeptes de la programmation objet orthodoxe qui préconisent qu'on ne puisse ni modifier, ni lire un attribut directement à l'aide de son nom, en dehors des méthodes de sa classe. Ils préconisent donc de les déclarer private comme ceci :
package getset;
public class Personne {
private String nom;
private String prenom;
private int age;
}
Dans ce cas, le programme Main précédent ne marchera pas car les 3 attributs sont invisibles à l'extérieur de la classe personne. Pour permettre d'accéder à ces attributs en lecture, on ajoute des méthodes publiques appelées des getter (du verbe to get = obtenir) qui renvoient une copie de l'attribut, et pour permettre de les modifier, on ajoute des méthodes publiques appelées de setter (du verbe to set = positionner, établir...).
package getset;
public class Personne {
private String nom;
private String prenom;
private int age;
public String getNom() {return nom;}
public String getPrenom() {return prenom;}
public int getAge() {return age;}
public void setNom(String nom) {this.nom = nom;}
public void setPrenom(String prenom) {this.prenom = prenom;}
public void setAge(int age) {this.age = age;}
}
Maintenant la méthode Main qui permet de faire la même chose que la précédente s'écrit :
package getset;
public class Main
{
public static void main(String[] args)
{
Personne pers = new Personne();
pers.setNom("Joseph");
pers.setPrenom("Joe");
pers.setAge(30);
System.out.println("Je suis " + pers.getNom() + " "
+ pers.getPrenom() + " et j'ai " + pers.getAge() + " ans");
}
}
Les partisans de l'orthodoxie sont essentiellemnt les programmeurs qui ont appris à programmer après les années 1980, alors que les programmeurs qui ont débuté avant, avec des langages non ou peu sécurisés comme l'assembleur, le fortran, le basic, le C... continuent avec leurs habitudes qui permettent d'accéder directement à tout et partout, avec un code, certes moins sécurisé en apparence, mais beaucoup plus simple, plus lisible, et générant beaucoup moins d'erreurs incopréhensibles de non respect des règles d'encapsulation.
Les langages continuent d'évoluer et on voit se développer d'un coté des ajouts de contraintes pour augmenter (en apparence) la sécurité et de l'autre coté des ajouts pour simplifier la programmation (dans Kotlin par exemple) qui permettent de s'affranchir des contraintes précédentes.
Par définition une interface est une classe spéciale qui ne peut contenir que des variables static ou final. Ces classes spéciales sont principalement utilisées dans deux buts :
•Créer une classe qui regroupe un ensemble de paramètres constants qui seraient communs à plusieurs autres classes. Pour accéder à ces paramètres ces classes n'auront qu'à implémenter cette interface. Cela crée une zone de constantes communes.
•Définir un ensemble de méthodes qui devront être disponibles dans certaines classes.
Pour les méthodes, une interface est une classe qui a les mêmes propriétés qu'une classe abstract, avec toutes ses méthodes abstract, sans qu'il y ait à les déclarer abstract (et bien sûr sans qu'il y ait à les définir). Généralement, une interface sert à définir un prototype de classe pour lui imposer une certaine structure.
On peut définir une nouvelle classe B en ajoutant des attributs et/ou des méthodes à une classe A. On utilise la syntaxe suivante :
class B extends A {... attributs et méthodes additionnelles... }
On dit que :
•la classe B étend la classe A
•la classe B hérite de la classe A.
•la classe B dérive de la classe A
Une classe ne peut hériter que d'une seule autre classe.
Il ne faut pas confondre l'héritage qui utilise la syntaxe extends A où A est une classe avec l'implémentation d'une interface qui utilise la syntaxe implements C où C est une interface. Alors qu'une classe ne peut deriver que d'une classe, elle peut implémenter plusieurs interfaces, par exemple :
class B extends A implements C, D {...attributs et méthodes additionnelles... }
A FAIRE
Parfois on veut utiliser une méthode modifiée d'une classe, une seule fois, ce qui fait que créer une classe modifiée, puis créer une instance et l'utilser parait bien lourd. Examinons l'example suivant :
import javax.swing.JFrame;
import java.awt.event.*;
public class Jmdd48
{
public static void main(String[] args)
{
MaFenetre f = new MaFenetre();
f.afficher();
}
}
class MaFenetre
{
JFrame mainFrame = null;
public MaFenetre()
{
mainFrame = new JFrame();
mainFrame.setTitle("Mon application");
WlClose wlClose = new WlClose();
mainFrame.addWindowListener(wlClose);
mainFrame.setSize(320, 240);
}
public void afficher()
{
mainFrame.setVisible(true);
}
}
class WlClose extends WindowAdapter {
public void windowClosing(WindowEvent ev)
{
System.exit(0);
}
}
La classe WlClose est créée uniquement pour modifier le comportement après un clic sur la croix de la fenêtre mainFrame : on veut fermer l'application. Pour cela, on crée cette classe, on crée une instance de cette classe : WlClose wlClose = new WlClose(); et on la passe en argument par l'instruction mainFrame.addWindowListener(wlClose);
Ouvrons une parenthèse : Pourquoi WlClose étend WindowAdapter alors qu'on a besoin que d'une classe qui implémente l'interface WindowListener. Parce que quand on définit une classe qui implémente l'interface WindowListener il faut redéfinir toutes les méthodes de cette interface ce qui représente beaucoup de code, alors que laclasse WindowAdapter qui l'implémente les a déjà toutes définies. En l'étendant on n'a donc qu'a modifier celle que l'on veut changer. Parenthèse fermée.
La programmation moderne est plus concise avec l'utilisation de classe anonyme. On ne définit pas de nouvelle classe et donc pas d'instance de cette et à l'emplacement où on utilise cette instance, on met le code de définition de la classe, entre accolades {}, sans lui donner de nom, comme ci-après.
import javax.swing.JFrame;
import java.awt.event.*;
public class Jmdd48
{
public static void main(String[] args)
{
MaFenetre f = new MaFenetre();
f.afficher();
}
}
class MaFenetre
{
JFrame mainFrame = null;
public MaFenetre()
{
mainFrame = new JFrame();
mainFrame.setTitle("Mon application");
mainFrame.addWindowListener(new WindowAdapter()
{
public void windowClosing(WindowEvent ev)
{
System.exit(0);
}
}
);
mainFrame.setSize(320, 240);
}
public void afficher()
{
mainFrame.setVisible(true);
}
}
En fait, en plus de MaFenetre.class, le compilateur, crée une classe supplémentaire qu'il va nommer MaFenetre$1.class.
Certaines opérations déclenchent des exceptions que l'on gère à l'aide de blocs
try {/* bloc des instructions problématiques */ }
catch(Exceptione e) {/* bloc exécuté si erreur détectée */ }
finally {/* bloc facultatif exécuté ensuite, si erreur ou non /*}
Les instructions du bloc try sont exécutées intégralement s'il n'y a pas d'exception et sont interrompues s'il y en a une.
S'il n'y a pas eu d'exception les instructions du bloc catch sont ignorées, sinon elles sont exécutées.
Le bloc finally est optionnel. On peut ne pas le mettre mais s'il est présent, ses instructions sont systématiquement exécutées (après les autres blocs), même si dans les autres blocs il y a un return ou toute autre instruction de déroutement.
Généralement dans le bloc catch il y a au moins les instructions suivantes :
e.printStackTrace(); qui produit l'impression du string e.toString().
Exemples de d'exception :
ArithmeticException si division par zéro,
NullPointerException si accès à un membre non initialisé,
OutOfMemoryError si echec allocation mémoire.
On peut définir une nous même un classe d'exception qui dérive de la classe Exception, par exemple :
class ErrAge extends Exception {
public int age;
ErrAge(int age) {this.age = age;}
}
Cette classe, ne fait strictement rien à part mémoriser l'argument reçu, mais elle permet de bénéficier du mécanisme de gestion des exceptions de Java.
Maintenant que cette classe est écrite, on peut déclencher une erreur si on crée une personne avec un age anormal, en modifiant la classe Personne comme ceci :
public class Personne
{
public String nom;
public String prenom;
public int age;
public Personne(String _prenom, String _nom, int _age) throws ErrAge
{
if (_age <= 0 || _age >= 150) throw new ErrAge(_age);
nom = _nom; prenom = _prenom; age = _age;
}
public static void main(String[] args)
{
Personne p;
try {
p = new Personne("Thomas", "Paulhet", 11);
System.out.println("Je suis " + p.prenom + " "
+ p.nom + " et j'ai " + p.age + " ans");
}
catch(ErrAge e)
{
System.out.println("Erreur age = " + e.age + "ans impossible");
}
try {
p = new Personne("Michel", "de Saint-Germain", 250);
System.out.println("Je suis " + p.prenom + " "
+ p.nom + " et j'ai " + p.age + " ans");
}
catch(ErrAge e)
{
System.out.println("Erreur age = " + e.age + " ans impossible");
}
}
}
L'exécution donne :
Je suis Thomas Paulhet et j'ai 11 ans
Erreur age = 250 ans impossible
Ici, c'est le programmeur qui, dans le constructeur public Personne(String _prenom, String _nom, int _age) va tester la valeur reçue _age et va déclencher une exception si l'age est anormal. Il doit prévenir du fait que cette méthode risque de déclencher une exception en ajoutant à sa déclaration le suffixe throws ErrAge qui indique le nom de l'exception qui peut être émise. Remarquer le s final dans throws. Le prototype complet de la méthode s'écrit :
public Personne(String _prenom, String _nom, int _age) throws ErrAge
Ensuite dans le corps de cette méthode on choisit la condition d'émission de l'exception, ici nous avons choisi :
if (_age <= 0 || _age >= 150)
et si cette condition est réunie nous lançons l'exception par l'instruction suivante :
throw new ErrAge(_age);
qui crée un objet new ErrAge(_age) et l'envoie (throw sans s à la fin) au gestionnaire d'exceptions.
Dans le programme de test, si l'exception est reçue, on se trouve dans un bloc catch(ErrAge e). Dans ce bloc on imprime un message ou on précise la valeur de l'age erroné mémorisé dans e.age.
Remarque : La classe Exception possède un constructeur Exception(String mes) et le message qu'il reçoit peut être accédé par e.getMessage() et généralement on l'imprime dans le catch. En créant dans notre classe ErrAge le constructeur ErrAge(String mes) {super(mes); } on pourra bénéficier de e.getMessage(), par exemple en remplaçant
if (_age <= 0 || _age >= 150) throw new ErrAge(_age);
par
if (_age<=0 || _age>=150) throw new ErrAge("Erreur age = " + _age + " ans impossible");
Le catch se simplifie alors en :
catch(ErrAge e) {System.out.println(e.getMessage());}
On peut décider dans le corps d'une méthode de ne pas s'occuper d'une exception TelleException potentiellement émise lors de l'exécution d'une instruction à condition que la méthode déclare qu'elle ne traitera pas cette exception et qu'elle la transmettra à son appelant. Ceci est simplement réalisé en ajoutant au prototype de déclaration de la méthode le suffixe throws TelleException avec le s à throws.
Bien faire la différence entre :
•throw qui précède la création et l'envoi d'une exception (à la suite d'un test qui a détecté une erreur).
•throws qui précède la liste les exceptions qui ne seront pas traitées localement dans la méthode et qui sont renvoyées à son appelant.
En fait Exception est dérivé de la classe Throwable qui descend directement de Object. C'est cette classe qui est à la base de toute les exceptions. Elle a 2 constructeurs Throwable() et Throwable(String mes), et 3 méthodes getMessage() qui renvoie le string mes, printStackTrace( ) qui affiche l'exception et l'état de la pile d'exécution au moment de son appel et printStackTrace(PrintStream s) idem mais envoie le résultat dans un flux s.
La classe Error dérive de Trowable et est consacré aux erreurs graves dans la machine virtuelle Java ou dans un sous système Java. L'application Java s'arrête instantanément dès l'apparition d'une exception de la classe Error.
Exception dérive de Throwable et RunTimeException dérive d'Exception.
Pour arrêter l'exécution du programme pour une raison quelconque, on peut appeler la méthode System.exit(int n).
a instanceof Toto : renvoi true si a est une instance de la classe Toto.
round() arrondi à l'entier le plus proche (classe java.lang.Math)
a.equals(b) retourne true si les handles a et b pointent sur le même objet (classe java.lang.Objet).
La classe Arrays fournit des utilitaires de tri comme Arrays.sort(..) pour tous les types de tableau.
En java les entrées/sorties se font par l'intermédiaires d'objets appelés stream (flux ou flot en français). Les flux permettent d'échanger l'information entre l'application et des entités quelconques : mémoire, fichier, etc..
La gestion bas niveau des flots d'octets se fait octet par octet sans aucune aide, à l'aide des classes suivantes :
inputStream et outputStream qui sont les classes abstraites de base,
FileInputStream et FileOutputStream pour les accès aux fichiers classiques en mémoire secondaire,
ByteArrayInputStream et ByteArrayOutputStream pour les accès dans des tableaux d'octets,
PipedInputStream et PipedOutputStream pour des accès entres applications,
Les classes suivantes (dont les constructeurs prennent en argument une des précédentes ou une des collègues) apportent des possibilités plus évoluées de lecture/écritures plus globales :
BufferedInputStream et BufferdOutputStream apporte l'accès aux lignes,
DataInputStream et DataOutPutStream apporte la manipulation des objets élémentaires,
ObjectInputStream et ObjectOutPutStream apporte la manipulation des objets complexes,
Pour traiter les flots de caractères, caractère par caractère (avec gestion des caractères spéciaux new_line, etc…) on a les classes spécifiques suivantes :
Reader et Writer qui sont les classes abstraites de base,
FileReader et FileWriter pour les accès aux fichiers classiques en mémoire secondaire,
CharArrayReader et CharArrayWriter pour les accès aux tableaux de char,
StringReader et StringWriter pour les accès à l'intérieur des strings,
PipedReader et PipedWriter pour des accès entres applications,
Les classes suivantes (dont les constructeurs prennent en argument une des précédentes ou une des collègues) apportent des possibilités plus évoluées de lecture/écritures plus globales :
BufferedReader et BufferWriter apporte l'accès aux lignes,
LineNumberReader permet de gérer les numéros des lignes
// classe fournissant des fonctions de lecture au clavier -
import java.io.* ;
public class Clavier
{ public static String lireString () // lecture d'une chaine
{ String ligne_lue = null ;
try
{ InputStreamReader lecteur = new InputStreamReader (System.in) ;
BufferedReader entree = new BufferedReader (lecteur) ;
ligne_lue = entree.readLine() ;
}
catch (IOException err)
{ System.exit(0) ;
}
return ligne_lue ;
}
public static float lireFloat () // lecture d'un float
{ float x=0 ; // valeur a lire
try
{ String ligne_lue = lireString() ;
x = Float.parseFloat(ligne_lue) ;
}
catch (NumberFormatException err)
{ System.out.println ("*** Erreur de donnee ***") ;
System.exit(0) ;
}
return x ;
}
public static double lireDouble () // lecture d'un double
{ double x=0 ; // valeur a lire
try
{ String ligne_lue = lireString() ;
x = Double.parseDouble(ligne_lue) ;
}
catch (NumberFormatException err)
{ System.out.println ("*** Erreur de donnee ***") ;
System.exit(0) ;
}
return x ;
}
public static int lireInt () // lecture d'un int
{ int n=0 ; // valeur a lire
try
{ String ligne_lue = lireString() ;
n = Integer.parseInt(ligne_lue) ;
}
catch (NumberFormatException err)
{ System.out.println ("*** Erreur de donnee ***") ;
System.exit(0) ;
}
return n ;
}
// programme de test de la classe Clavier
public static void main (String[] args)
{ System.out.println ("donnez un flottant") ;
float x ;
x = Clavier.lireFloat() ;
System.out.println ("merci pour " + x) ;
System.out.println ("donnez un entier") ;
int n ;
n = Clavier.lireInt() ;
System.out.println ("merci pour " + n) ;
}
}
Les fichiers d'octets ou de caractères sont ainsi accessibles à travers les classes FileInputStream, FileOutputStream, FileReader, FileWriter. Les constructeurs de ces classes prennent en argument soit un string qui est le nom du fichier, sur un File ou un FileDescriptor.
La classe File offre une représentation des fichiers et répertoires qui permet de les manipuler au niveau système.
La classe FileDescriptor offre une représentation opaque des fichiers, des sockets et d'autres sources de données.
Un thread (fil d'exécution en français) est une partie de programme qui s'exécute de manière indépendante des autres parties, comme si cette partie était la seule à s'exécuter sur l'ordinateur. Pour les programmes simples, il n'y a qu'un thread, appelé le thread principal dans lequel s'exécute la méthode main qui lance le déroulement du programme.
La classe Thread est une classe qui possède une méthode run dont les instructions s'exécuteront dans un nouveau thread, indépendant de celui qui a créé l'instance de ce thread. L'appel du constructeur se faisant dans le thread de l'appelant, l'instance appartient au thread appelant. Si on appelait la méthode run, elle s'exécuterait aussi dans ce thread, c'est pour cela qu'il faut appeler une méthode spéciale du thread : sa méthode start() qui va créer le nouveau fil d'exécution et y placer l'exécution du programme de la méthode run.
Voici par exemple une classe Ecrit1 qui étend la classe Thread, à laquelle on confie la tache d'écrire un certain texte, puis d'attendre le nombre attente de secondes, et ceci nb fois. Après cela il aura terminé sa tâche et disparaitra.
Le constructeur récupère ces 3 paramètres de l'appelant et les mémorise.
La méthode run, fait l'impression du texte dans une boucle limitée à nb fois, et se met le thread en sommeil au moyen de la méthode sleep(attente) qui est une méthode qui peut déclencer une exception car elle utilise l'horloge système qui est peut-être inaccessible. Il faut donc mettre ces instruction dans un bloc try.
class Ecrit1 extends Thread {
private String texte;
private int nb;
private long attente;
public Ecrit1(String texte, int nb, long attente) {
this.texte = texte;
this.nb = nb;
this.attente = attente;
}
public void run() {
try {
for (int i = 0; i < nb; i++) {
System.out.print(texte);
sleep(attente);
}
} catch (InterruptedException e) {} // impose par sleep
}
}
Et voici la méthode main qui lance 3 fils d'exécutions en concurrence chacun écrivant un texte différent avec une attente différente et un nombre de fois différent.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class TstThr1 {
public static void main(String args[]) throws IOException {
System.out.println("Le 1er thread écrit AAAA et attend 5 secondes, 20 fois");
System.out.println("Le 2nd thread écrit BBBB et attend 10 secondes, 20 fois");
System.out.println("Le 3me thread fait un retour ligne et attend 15 secondes, 5 fois");
System.out.println("Quand tout est fini, taper un retour ligne.");
Ecrit1 e1 = new Ecrit1("AAAA ", 20, 5);
Ecrit1 e2 = new Ecrit1("BBBB ", 20, 10);
Ecrit1 e3 = new Ecrit1("\n", 5, 15);
e1.start();
e2.start();
e3.start();
(new BufferedReader (new InputStreamReader (System.in))).readLine() ;
}
}
Et voici le résultat sur la console :
Le 1er thread écrit AAAA et attend 5 secondes, 20 fois
Le 2nd thread écrit BBBB et attend 10 secondes, 20 fois
Le 3me thread fait un retour ligne et attend 15 secondes, 5 fois
Quand tout est fini, taper un retour ligne.
AAAA BBBB
AAAA AAAA BBBB AAAA
AAAA BBBB AAAA AAAA BBBB
AAAA AAAA BBBB AAAA
AAAA BBBB AAAA BBBB AAAA
AAAA AAAA AAAA BBBB AAAA AAAA BBBB AAAA AAAA BBBB BBBB BBBB BBBB BBBB BBBB BBBB BBBB BBBB BBBB BBBB
Remarque : Le constructeur Thread(String nom) permet de donner un nom au thread.
Utilisation de Thread anonyme :
Lorsqu,il n'y a qu'une méthode run à lancer et qu'elle n'utilise que des variables sures (pas modifiées par d'autres thread), on peut lancer le thread à l'aide d'une classe anonyme. Par exemple, on pourrait remplacer les instructions Ecrit1 e2 = new Ecrit1("BBBB ", 20, 10); e2.start(); par la séquence suivante :
String txt = "BBBB "; int nb = 20; long att = 10;
pour définir les variables utilisées par le thread anonyme, puis on crée la thread anonyme avec sa méthode run :
(new Thread() { public void run() {
try { for (int i = 0; i < nb; i++) { System.out.print(txt); sleep(att);}}
catch (InterruptedException e) {}
}}
).start();
Cet exemple n'est pas bien approprié car ici on a 3 threads qui utilisent la même méthode run, d'où l'intérêt de fabriquer une classe qui dérive de Thread.
Rappelons que le constructeur de Thread ne le lance pas. Il le met dans l'état NEW. C'est la méthode start() qui le met dans l'état RUNNABLE (c'est-à-dire activable et il les différents threads activables sont activés à tour de rôle par le système) et au premier return rencontré dans la méthode run(), le thread est mis dans l'état TERMINATED.
Nous avons vu également la méthode static void sleep(long ms) qui passe le thread à l'état TIMED_WAITING. Il repassera à l'état RUNNABLE après ms millisecondes. ATTENTION cette méthode est STATIC. On l'appelle normalement par Thread.sleep(v). Elle peut lever l'interruption InterruptedException. (=> try/catch ou throws)
Comme une classe ne peut hériter que d'une seule classe, une classe qui étend la classe Thread ne peut dériver d'aucune autre classe, ce qui est très contraignant ! Mais heureusement, une classe peut implémenter de nombreuses interfaces et il existe une interface appelée Runnable qui n'a aucun attribut et dont la seule méthode à redéfinir est la méthode run.
En résumé l'interface Runnable ne fait qu'imposer la définition de la méthode public void run().
Et pour créer l'instance de Thread, on utilise un constructeur de cette classe qui prend en paramètre une interface Runnable : public Thread(Runnable target), et on passe à ce contructeur notre classe qui implémente l'interface Runnable.
Mais il reste un problème à résoudre, celui du lancement du thread.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class TstThr2 {
public static void main(String args[]) throws IOException {
Ecrit2 e1 = new Ecrit2("AAAA ", 20, 5);
Ecrit2 e2 = new Ecrit2("BBBB ", 20, 10);
Ecrit2 e3 = new Ecrit2("\n", 5, 15);
e1.lance();
e2.lance();
e3.lance();
(new BufferedReader (new InputStreamReader (System.in))).readLine() ;
}
}
class Ecrit2 implements Runnable {
private String texte;
private int nb;
private long attente;
public Ecrit2(String texte, int nb, long attente) {
this.texte = texte;
this.nb = nb;
this.attente = attente;
}
public void lance() {
Thread t = new Thread(this);
t.start();
}
public void run() {
try {
for (int i = 0; i < nb; i++) {
System.out.print(texte);
Thread.sleep(attente);
}
} catch (InterruptedException e) {
} // impose par sleep
}
}
Dans l'exemple ci-dessus, on crée dans l'instance du nouveau thread Thread t = new Thread(this); est crée dans classe Ecrit2, donc sur this, dans sa méthode public void lance(). et le thread est démarré par sa méthode t.start().
Dans l'exemple ci-après,
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class TstThr3
{ public static void main (String args[]) throws IOException
{ Ecrit3 e1 = new Ecrit3 ("bonjour ", 10, 5) ;
Ecrit3 e2 = new Ecrit3 ("bonsoir ", 12, 10) ;
Ecrit3 e3 = new Ecrit3 ("\n", 5, 15) ;
Thread t1 = new Thread (e1) ; t1.start() ;
Thread t2 = new Thread (e2) ; t2.start() ;
Thread t3 = new Thread (e3) ; t3.start() ;
(new BufferedReader (new InputStreamReader (System.in))).readLine() ;
}
}
class Ecrit3 implements Runnable
{ public Ecrit3 (String texte, int nb, long attente)
{ this.texte = texte ;
this.nb = nb ;
this.attente = attente ;
}
public void run ()
{ try
{ for (int i=0 ; i<nb ; i++)
{ System.out.print (texte) ;
Thread.sleep (attente) ; // attention Thread.sleep
}
}
catch (InterruptedException e) {} // impose par sleep
}
private String texte ;
private int nb ;
private long attente ;
}
ce n'est pas dans la classe interne Ecrit3 qu'est créée l'instance de la classe Thread, mais dans la méthode main de classe publique : Thread t1 = new Thread (e1) . On lui passe en argument l'instance de la classe Ecrit3 e1 = new Ecrit3 (...); et ensuite on démarre le thread t1.start().
La méthode précédente me parait plus simple car plus proche du fonctionnement par héritage de la classe Thread, en particulier si la méthode lance est appélée start. On a exactement le même comportement avec class Ecrit2 implements Runnable qu'avec class Ecrit1 extends Thread en tout simplement en ajoutant à la classe Ecrit2 la méthode :
public void start() {Thread t = new Thread(this); t.start();}
Les constructeurs de Thread à partir d'un Runnable sont Thread(Runnable cible) ou Thread(Runnable cible, String nom) qui permet de nommer le Thread.
Runnable anonyme :
Comme indiqué précédemment, si la méthode run n'utilise que des variables sures (pas modifiées par d'autres thread), on peut utiliser une cible runnable anonyme. Dans le deuxième cas, la cible est une classe qui étend l'interface Runnable, définissant ainsi le code à exécuter dans le thread. Mais on peut également utiliser une classe anonyme qui implente l'interface Runnable en mettant directement comme argument le code suivant :
new Runnable() { public void run() {/* Ici le code a exécuter */} };
Un thread quelconque peut demander l'arrêt d'un thread A. Pour cela il utilise la méthode void interrupt() de ce thread qui va positionner un booléen pour lui indiquer qu'un autre thread a demandé son arrêt. En résumé les threads B, C,... peuvent demander l'arrêt du thread A en mettant dans leur fil d'exécution l'instruction :
A.interrupt().
Pour que le thread A satisfasse cette demande, il faut qu'il examine le booléen mémorisant ces demande d'arrêt, ce qu'il peut faire dans sa méthode run à l'aide de la méthode static bool interrupted() qui renvoie true si une demande d'arrêt a été faite, false sinon. Attention cette méthode est STATIC (son appel est équivalent à appeler par Thread.interrupted()). Si la réponse est true on programme normalement un return pour sortir de la méthode run, mais rien ne nous y oblige. Quoiqu'on fasse, l'exécution de interrupted() remet l'indicateur à false.
Exemple de demande d'arrêt des threads d'écriture des exemples précédents lorsque l'utilisateur envoie une ligne au clavier :
public class TstInter {
public static void main(String args[]) {
Ecrit4 e1 = new Ecrit4("AAAA ", 5);
Ecrit4 e2 = new Ecrit4("BBBB ", 10);
Ecrit4 e3 = new Ecrit4("\n", 35);
e1.start();
e2.start();
e3.start();
Clavier.lireString();
e1.interrupt(); // interruption premier thread
System.out.println("\n*** Arret premier thread ***");
Clavier.lireString();
e2.interrupt(); // interruption deuxième thread
e3.interrupt(); // interruption troisième thread
System.out.println("\n*** Arret deux derniers threads ***");
}
}
class Ecrit4 extends Thread {
public Ecrit4(String texte, long attente) {
this.texte = texte;
this.attente = attente;
}
public void run() {
try {
while (true) // boucle infinie
{
if (interrupted()) // Appelle la méthode statique Thread.interrupted()
return;
System.out.print(texte);
Sleep(attente); // Appelle la méthode statique Thread.sleep()
}
} catch (InterruptedException e) {return; // on peut omettre return ici}
}
private String texte;
private long attente;
}
Dans cet exemple Ecrit4 hérite de Thread. C'est donc un Thread et l'utilisation des méthodes interrupt() , interrupted() et Sleep(attente) est facilitée.
Ci-après Ecrit5 implémente Runnable. L'accès aux méthodes précédentes est fait par l'intermédiaire d'un thread attribut à l'intérieurce de Ecrit5.
public class TstInter5 {
public static void main(String args[]) {
Ecrit5 e1 = new Ecrit5("AAAA ", 500);
Ecrit5 e2 = new Ecrit5("BBBB ", 1000);
Ecrit5 e3 = new Ecrit5("\n", 3500);
e1.start();
e2.start();
e3.start();
Clavier.lireString();
e1.t.interrupt(); // interruption premier thread
System.out.println("\n*** Arret premier thread ***");
Clavier.lireString();
e2.t.interrupt(); // interruption deuxième thread
e3.t.interrupt(); // interruption troisième thread
System.out.println("\n*** Arret deux derniers threads ***");
}
}
class Ecrit5 implements Runnable {
public Ecrit5(String texte, long attente) {
this.texte = texte;
this.attente = attente;
}
public void run() {
try {
while (true) // boucle infinie
{
if (Thread.interrupted())
return;
System.out.print(texte);
Thread.sleep(attente);
}
} catch (InterruptedException e) {return; }
}
public void start() { t = new Thread(this); t.start(); }
public Thread t;
private String texte;
private long attente;
}
Si dans un main, comment dans la plupart des exemples précédents, on lance un ou des threads, puis que le main se termine, l'exécution du programme continue tant qu'il reste un thread inachevé. La terminaison du main n'arrête pas les threads. Si on veut que la terminaison du main arrête un thread t, il faut déclarer ce thread comme appartenant à la catégorie spéciale appelée Daemon par l'instruction t.setDaemon(bool b) avec b=true qui doit être placée avant le start(), et qui doit n'être exécutée qu'une seule fois.
Dans ce cas, dès qu'on sort du main, tous les thread Daemon dont brutalement achevés.
En plus des run(), start(), sleep(long ms), interrupt() et interrupted() la classe Thread a également la méthode :
•void yield(). Le thread laisse son tour au thread suivant tout en restant dans l'état RUNNABLE (cad activable).
•static Thread currentThread() renvoie une référence sur le thread en cours.
•bool isInterrupted(). Cette méthode d'instance permet de savoir si un certain thread t est actif ou non : t.isInterrupted() renvoie true ou false suivant le cas.
Méthode de la classe Object mère de toutes les classe :
void object.wait() ou object.wait(long timeout). Le thread se met dans l'état WAITING jusqu'à l'appel par un autre thread d'une des méthodes suivantes,
•void object.notify(). Le premier thread en wait sur object passe à l'état RUNNABLE.
•void object.notifyAll(). Tous les threads en wait sur object passent à l'état RUNNABLE.
Les classes qui étendent la classe TimerTask sont utilisées pour effectuer des tâches lourdes dans un thread d'arrière plan (afin de ne pas bloquer l'interface avec l'usager (l'UI). La méthode run() de ces tâches peut être lancée ou cadencée par une instance de la classe Timer au moyen des méthodes schedule(...) et scheduleAtFixedRate(...). Si une périodicité est fixée, avec schedule la période est relative à la fin de l'éxécution précédente, et avec scheduleAtFixedRate les périodes sont relative au début de la 1ère exécution.
Dans l'exemple ci-dessous la tâche MaTask1 est exécutée immédiatement toutes les 1000 millisecondes, et la tâche Matask2 arrête ce cadencement en cancelant les timers qui ont cadencés ces 2 tâches ;
Le 2eme arg des méthodes schedule ou scheduleAtFixedRate est un delai avant la première exécution et le 3ème argument est la périodicité.
import java.util.Timer;
import java.util.Date;
import java.util.TimerTask;
public class TestTimer {
private static Timer timerLoop, timerFin;
public static void main(final String[] args) {
timerLoop = new Timer();
timerLoop.scheduleAtFixedRate(new MaTask1(), 0, 1000);
timerLoop.schedule(new MaTask2(), 12000);
timerFin = new Timer();
}
private static class MaTask1 extends TimerTask {
@Override
public void run() {
System.out.println(new Date() + " Execution de ma tache");
}
}
private static class MaTask2 extends TimerTask {
@Override
public void run() {
timerLoop.cancel();
timerLoop.purge();
timerFin.cancel();
}
}
}
Les méthodes schedule et scheduleAtFixedRate de Timer lancent l'exécution de la méthode run de la classe passée en 1er argument dans un nouveau thread. On peut le vérifier en mettant les impressions suivantes :
System.out.println("Nom Thread dans XXX : "+Thread.currentThread().getName());
dans la routine main et dans la routine run (remplacer XXX par main puis run).
Dans l'exemple suivant, on tue le timer après un certain delai au moyen d'un sleep :
public class TestTimer {
public static void main(final String[] args) {
Timer timer = new Timer();
timer.schedule(new MaTask1(), 0, 1000);
try { Thread.sleep(12000);}
catch (InterruptedException e) {e.printStackTrace();}
timer.cancel();
}
private static class MaTask1 extends TimerTask {
@Override
public void run() {
System.out.println(new Date() + " Execution de ma tache");
}
}
}
Pour synchroniser des threads on peut utiliser des méthodes de type synchronized utilisant wait() et notify(). Ci-après un exemple en java standard construit à partir d'exemples du chapitre 11 de "Programmer en java" de Claude Delannoy.
// Classe contenant la routine de traitement a exécuter. Ici impression d'un nombre
class Traitement
{ // Mémorisation des paramètres du traitement
int n;
// La routine de traitement débute par wait() pour attente déblocage
public synchronized void execute() throws InterruptedException
{ wait(); System.out.println ("merci pour " + n) ;}
// Reception des paramètres du traitement puis déblocage
public synchronized void debloque(int n) throws InterruptedException
{ this.n = n; notifyAll() ;}
}
// Thread faisant un appel en boucle infini du l'execution du traitement
class MyThread extends Thread {
Traitement c; // Memo classe contenant le traitement a effectuer
public MyThread(Traitement c) {this.c = c;}
// Appel en boucle infini du l'execution du traitement
public void run()
{ try {while(!isInterrupted()) c.execute() ;} catch (InterruptedException e) {} }
}
// Exemple d'utilisation
public class MyTHreadDemo {
public static void main (String[] args)
{
int n = 0;
Traitement work = new Traitement();
MyThread myThread = new MyThread(work);
myThread.start();
// Tant que le nombre lu est > 0 on le traite.
while(true)
{
System.out.println ("donnez un entier") ;
n = Clavier.lireInt() ;
if (n <= 0) break;
try { work.debloque(n); } catch (InterruptedException e) {e.printStackTrace();}
}
myThread.interrupt(); // Demande de terminaison au thread
}
}
Les méthodes wait(), notify() et notifyAll() sont appellées à l'intérieur d'une méthode de type
synchronized, d'un objet d'une certaine classe qui joue le rôle de verrou (ci-dessus l'objet work de la classe Traitement) :
•L'appel work.wait() met le thread en cours en attente.
•L'appel work.notifyAll() (ou work.notify() ) va débloquer un (ou tous les) thread(s) qui a(ont) été mis en attente par un work.wait().
Une méthode synchronized d'un objet x ne peut être interrompu par une autre méthode synchronized du même objet x. Il y a exclusion mutuelle de l'accès à x pour ces méthodes, même si wait() et/ou notify() ne sont pas utilisés.
On peut limiter le blocage à une petite portion du code d'une méthode. Au lieu de la déclarer synchronized, on utilise un bloc du type :
synchronized(this) {instructions en exclusin mutuelle}
Le verrou porte ici sur l'objet this, mais on peut le faire porter sur un objet quelconque.
L'utilisation d'une classe étendant la classe AsyncTask est un moyen efficace de synchroniser un thread d'arrière plan avec l'UI_thread. Voici le schéma de mise en oeuvre :
class MyAsyncTask extends AsyncTask<T1, T2, T3>
{
@Override
protected void onPreExecute() // Méthode faculative
{/*Exécuté dans l'UI_thread avant l'exécution du back_thread */}
@Override
protected T3 doInBackground(T1... t1) // Méthode obligatoire renvoie type T3
{ /*Partie exécutée ds le background_thread. Travaille sur t1[0], t1[1], ..*/
boucle { ...; publishProgress(t2); ...} // publishProgress(T2)
return(t3); // Valeur renvoyée du type T3
}
@Override
protected void onProgressUpdate(T2... t2) // Méthode faculative
{/*Exécuté dans l'UI_thread après chaque appel de publisProcess(t2)*/}
@Override
protected void onPostExecute(T3 t3) // Méthode faculative
{/*Exécuté dans l' UI_thread après l'execution du background_thread*/}
}
La déclaration AsyncTask<T1, T2, T3> définit les 3 types suivants :
•T1 : Types pris en argument par la méthode doInBackground(T1... t1) (... : ellipse, cad avec un nombre d'arguments de type T1 variable, éventuellement aucun) exécutée dans le thread en arrière plan. Cette méthode est grossièrement la méthode run() du thread en arrière plan.
•T2 : La méthode doInBackground peut utiliser la méthode publishProgress(t2a,t2b,..) pour signaler son avancement à l'UI_Thread. Ces arguments sont reçus dans l'UI_thread par la méthode onProgressUpdate(T2... t2).
•T3 : Type de l'objet renvoyé par la méthode T3 doInBackground. Il est pris en argument par la méthode onPostExecute(T3 t3) qui est exécutée dans l'UI_thread à la fin de l'éxécution du background_thread.
Des quatre méthodes onPreExecute,doInBackground, onProgressUpdate et onPostExecute seule la définition de la méthode doInBackground est obligatoire.
L'ensemble est mis en oeuvre par l'appel de la méthode execute() sur l'AsyncTask :
new MyAsyncTask().execute();
Le déroulement des tâches est le suivant :
1.La méthode void onPreExecute() si elle est définie sera exécutée en premier au niveau de l'UI_Thread.
2.Le background_thread déroule la méthode T3 doInBackground(T1... t1).
3.La méthode void onProgressUpdate(T2... t2) si elle est définie sera exécutée au niveau de l'UI_thread en parallèle au background_thread, synchronisée par les d'appels de la méthode void publishProgress(T2... t2) dans la méthode doInBackground.
4.La méthode void onPostExecute(T3 t3) si elle est définie sera exécutée en dernier au niveau de l'UI_Thread après la terminaison de la méthode T3 doInBackground(T1... t1). Elle prend en argument la valeur renvoyée par cette méthode.
Exemple d'applel du C/C++ depuis Java, sous MinGW (ou Cygwin), tiré du cours de JMDoudoux
Soit un fichier java contenant une classe java TestJNI1 qui appelle une fonction C int ajoute(int a, int b).
class TestJNI1
{
public native int ajoute(int a, int b);
static {System.loadLibrary("mabibjni");}
public static void main(String[] args)
{
TestJNI1 t = new TestJNI1();
System.out.println("2 + 3 = " + t.ajoute(2,3));
}
}
Danc ce fichier :
•Le patron de la fonction C appelée doit être déclarée native :
public native int ajoute(int a, int b);
•Cette fonction va être fournie par une librairie obligatoirement partagée (nommée dans notre cas libmabibjni.so sous linux ou mabibjni.dll sous windows) qui doit être préalablement chargée par l'instruction :
static {System.loadLibrary("mabibjni");}
•elle est est ensuite appelée comme une méthode de la classe.
Cette classe java est compilée normalement par la commande :
javac TestJNI1.java
qui génère TestJNI1.class à partir de TestJNI1.java.
Par ailleurs il faut génér les deux fichiers suivants :
1.Le fichier entête TestJNI1.h qui contient les prototypes pour le langage C des
fonctions correspondant aux méthodes déclarées native dans le code source Java. Ce fichier est généré par la commande :
javah -jni TestJNI1
écrire le fichier TestJNI.def suivant :
EXPORTS
Java_TestJNI1_ajoute
qui déclare la routine C qui sera appelée par le code java. Ce fichier sert au linker pour la génération de la librairie partagée.
Par exemple le fichier original C suivant :
#include <stdio.h>
int ajoute(int a, int b)
{
return (a+b);
}
doit être ré_écrit sous la forme suivante :
#include <jni.h>
#include <stdio.h>
#include "TestJNI1.h"
JNIEXPORT jint JNICALL Java_TestJNI1_ajoute(JNIEnv *env, jobject thisObj, jint a, jint b)
{
return (a+b);
}
Les modifications suivantes ont été apportées :
•ajout des include de TestJNI1.h et jni.h,
•ajout des attributs JNIEXPORT et JNICALL avant et après le type renvoyé par la fonction,
•modification du nom de la fonction appelée en la préfixant par Java_NomDeLaClasseQuiLutilise_
•ajout de 2 arguments en tête des arguments : JNIEnv *env et jobject thisObj,
•modification du type de retour et du type des arguments.
La structure JNIEnv *env passée en premier argument à pour membre des pointeurs sur toutes les fonctions (api) utilisées pour faire le lien entre le monde java et le monde C/C++.
Le deuxième argument jobject thisObj permet d'accéder à l'instance de la classe java qui a appelé la routine C (qui du coté java est une méthode de cette classe).
Compiler avec gcc sous Mingw ou Cygwin, par exemple sous Mingw (sur une seule ligne) :
gcc -c -I"C:\Program Files\Java\jdk1.6.0_25\include" -I"C:\Program Files\Java\jdk1.6.0_25\include\win32" -o TestJNI.o TestJNI.c
Recommencer cette étape si plusieurs fichiers C.
La génération la dll est réalisée par la commande suivante sous Mingw :
gcc -shared -o mabibjni.dll TestJNI.o TestJNI.def
(mettre tous les fichiers *.o s'il y en a plusieurs).
Ces deux étapes peuvent être faites en une seule fois sans générer TestJNI.o par la commande (sur une ligne) :
gcc -shared -o mabibjni.dll -I"C:\Program Files\Java\jdk1.6.0_25\include" -I"C:\Program Files\Java\jdk1.6.0_25\include\win32" TestJNI.c TestJNI.def
(mettre tous les fichiers *.c s'il y en a plusieurs).
Remarque : le linker répond le message suivant qui ne pose pas de problème :
Warning: resolving _Java_TestJNI1_ajoute by linking to _Java_TestJNI1_ajoute@8
Use --enable-stdcall-fixup to disable these warnings
Use --disable-stdcall-fixup to disable these fixups
Remarque 1 : Si sous Cygwin __int64 n'est pas défini, le définir dans la ligne de compilation de gcc en ajoutant -D __int64="long long".
Remarque 2 : Sous Eclipse, on commence par un projet Java pour la classe Java, puis on transforme ce projet java en projet mixte java/C/C++ par un clic-droit sur le nom du projet, puis Java project ⇒ New ⇒ Other... ⇒ C/C++ / Convert to a C/C++ Project (Adds C/C++ Nature) ⇒ Next.
Dans le dialogue "Convert to a C/C++ Project" , dans "Project type", sélectionner "Makefile Project" ⇒ et dans "Toolchains", sélectionner "MinGW GCC" ⇒ Finish.
Puis dans Properties⇒ C/C++ General / Path and Symbols / Includes,, ligne GNU C, ajouter les deux répertoires C:\Program Files\Java\jdk1.6.0_25\include et C:\Program Files\Java\jdk1.6.0_25\include\win32.
Par ailleurs j'ai du changer le jre java 64 bits par le jre java 32 bits car gcc génère du 32 bits et ne peut donc pas être compatible avec le jre 64 bits. Pour faire cela : Properties ⇒Java Build Path ⇒Libraries : Supprimer le jre qui pointe sur celui en 64 bits, puis Add Library ⇒JRE system Library/next ⇒ Alternate Jre/Installed JREs... et aller chercher le répertoire de la jre 32 bits.
Ensuit on peut exécuter le projet comme un projet java ou comme un projet C.
Sous Eclipse, il faut, dans la boite de dialogue "Run configurations", dans java application, onglet Main Arguments, mettre dans la fenêtre VM Arguments : -Djava.library.path=jni
et l'exécution se fait normalement par la commande run.
L'exécution du code se fait normalement par la commande :
java TestJNI1
S'il manque java.dll il faut ajouter java/bin dans le PATH, par exemple sous Mingw :
PATH="/c/Program Files (x86)/java/jre6/bin":$PATH
Remarque : Avec PATH="/c/Program Files/java/jre6/bin":$PATH il y a un problème de plateforme 64 bits.
Exception in thread "main" java.lang.UnsatisfiedLinkError: D:\llibre\javaprogs\TestJni1\mabibjni.dll: Can't load IA 32-bit .dll on a AMD 64-bit platform
at java.lang.ClassLoader$NativeLibrary.load(Native Method)
at java.lang.ClassLoader.loadLibrary0(Unknown Source)
at java.lang.ClassLoader.loadLibrary(Unknown Source)
at java.lang.Runtime.loadLibrary0(Unknown Source)
at java.lang.System.loadLibrary(Unknown Source)
at TestJNI1.<clinit>(TestJNI1.java:5)
Could not find the main class: TestJNI1. Program will exit.
car dans "Program Files" c'est du 64 bits et comme gcc génère une dll 32 bits, il faut utiliser la dll java 32 bits qui est dans dans Program Files (x86)/java/jre6/bin.
Pour que l'exécution marche, il faut comme ci-dessus modifier le path pour accéder au fichier java.dll 32 bits :
PATH="/c/Program Files (x86)/java/jre6/bin":$PATH
Le makefile suivant suppose que l'existence sous le répertoire projet des 3 répertoires suivants :
src : où se trouve le fichier $(MYCLASS).java
bin : où sera placé le fichier $(MYCLASS).class
jni : où se trouve le fichier makefile et le fichier C native.c. Il y aura ensuite en plus les fichiers native.h, native.o et myjni.dll.
La cible all du fichier makefile génère tout ce qui permet l'exécution.
La cible execute du fichier makefile génère tout et exécute.
Voici le fichier makefile :
# Nom de la classe java principale
MYCLASS = JNICallBackMethod
# Definition de la variable CLASSPATH
CLASS_PATH = ../bin
# Répertoires où make va vérifier les dépendances par type de fichier
vpath %.class $(CLASS_PATH)
vpath %.java ../src
all : myjni.dll
# $@ = cible, $< = 1ere dépendance, $* = cible sans extension
myjni.dll : native.o
gcc -Wl,--add-stdcall-alias -shared -o $@ $<
native.o : native.c native.h
gcc -I"C:\Program Files\Java\jdk1.6.0_25\include" -I"C:\Program Files\Java\jdk1.6.0_25\include\win32" -c $< -o $@
native.h : $(MYCLASS).class
javah -o $@ -classpath $(CLASS_PATH) $(MYCLASS)
$(MYCLASS).class : $(MYCLASS).java
javac -d $(CLASS_PATH) ../src/$(MYCLASS).java
clean :
rm native.h native.o myjni.dll
# la cible effective $(MYCLASS).class est trouvée dans le rép $(CLASS_PATH).
execute : $(MYCLASS).class myjni.dll
java $(MYCLASS)
Dans le code java il y a la déclaration de méthodes de la classe et leurs appels. Ces méthodes prennent des arguments et fournissent un retour qui sont des soit des types primitifs java, soit des objets java. Il en est de même pour la méthode déclarée native. Dans le code C les types java n'existant pas, ils sont remplacés par de nouveaux types C que nous appelons NativeType. Pour les types primitifs java, c'est simple, ils sont représentés par des équivalents de types primitifs C déclarés par des typedef. Pour les objets c'est plus compliqué, ils sont représentés par un NativeType qui est un pointeur sur une structure opaque, ce qui ne pose pas de pb, car en fait les instances de ces objets ne sont utilisés qu'en argument ou en retour de fonctions de l'api JNI.
Le traitement des types primitifs est très simple. Ils simplement sont remplacés, dans les arguments ou le type de retour par le même nom précédé d'un j (à l'exception de void). La table suivante donne la correspondance entre les types primitifs java et le type permettant de les manipuler en C, qui sont appelés ici NativeType :
Type Java |
NativeType |
boolean |
jboolean |
byte |
jbyte |
char |
jchar |
double |
jdouble |
int |
jint |
float |
jfloat |
long |
jlong |
short |
jshort |
void |
void |
Les types C sont définis dans les fichiers headers suivants :
// Dans "win2\jni_mh.h" - header machine dependant
#define JNIEXPORT __declspec(dllexport)
#define JNIIMPORT __declspec(dllimport)
#define JNICALL __stdcall
typedef long jint;
typedef __int64 jlong;
typedef signed char jbyte;
Remarquer que jint qui correspont au int java correspond à un long en C et que jlong équivalent du long java correspond au __int64 parfois noté long long en C
// Dans "jni.h"
typedef unsigned char jboolean;
typedef unsigned short jchar;
typedef short jshort;
typedef float jfloat;
typedef double jdouble;
typedef jint jsize;
Les objets (types non primitifs dérivant tous de la classe Object) sont passés par référence en utilisant une variable de type jobject. Plusieurs autres types sont prédéfinis par JNI pour des objets fréquemment utilisés :
type_Java |
NativeType |
java.lang.object |
jobject |
java.lang.String |
jstring |
java.lang.Class |
jclass |
java.lang.Throwable |
jthrowable |
int[] |
jintArray |
long[] |
jlongArray |
float[] |
jfloatArray |
double[] |
jdoubleArray |
Object[] |
jobjectArray |
boolean[] |
jbooleanArray |
byte[] |
jbyteArray |
char[] |
jcharArray |
short[] |
jshortArray |
Dans la partie codée en java figure un objet java de la 1ère colonne, par exemple un String. Dans la partie codée en C il est remplacé par un objet défini en C, d'un nouveau type prédéfini, ici un jstring, qui figure dans la 2ème colonne.
En fait tous les NativeType sont définis en C de la même manière : pointeurs sur une structure opaque. Ils sont convertis en objet C plus traditionnel à l'aide d'une fonction du JNI, par exemple un jstring est converti en un tableau de char, à l'aide de la fonction const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy). La nature exacte de ces types est donc sans importance car ils ne servent que dans les fonctions du JNI.
Toutes les fonctions du JNI sont des membres de la structure JNIEnv dont le pointeur *env est le premier argument de la procédure C.
A titre d'exemple, en C, la construction (avec allocation) du tableau de char sera réalisé par un appel de la forme :
const char *inCStr = (*env)->GetStringUTFChars(env, inJNIStr, NULL);
où l'env JNIEnv figure en tant que structure porteuse de la méthode membre et en tant que premier argument.
En C++, la forme se simplifie en :
const char *inCStr = env->GetStringUTFChars(inJNIStr, NULL);
car ici l'env JNIEnv figure en tant qu'instance de classe, ce qui fait qu'on l'économise en tant que premier argument.
Supposons que la classe java ClasseJava utilise la méhode C suivante :
private native String fonction_qcq(String msg);
Dans le code en C les arguments String sont remplacés par des NativeType jstring :
JNIEXPORT jstring JNICALL Java_TestJNIString_sayHello(JNIEnv *, jobject, jstring msg);
const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);
const jchar * GetStringChars(JNIEnv *env, jstring string, jboolean *isCopy);
Si la mémoire ne peut être allouée on obtient un pointeur NULL.
Par le pointeur *isCopy on peut demander de recevoir un pointeur sur la chaine originale plutôt que sur une copie. En retour *isCopy permet de savoir si la demande a été exaucée. C'est dangereux et il est conseillé de passer un pointeur NULL.
Une fois que ces tableaux alloués ne sont plus utiles, il faut les désallouer avec les fonctions suivantes :
void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf);
void ReleaseStringChars(JNIEnv *env, jstring string, const jchar *jchs);
Par ailleurs on peut se limiter à récupérer une sous-chaine de nb caractères à partir de start, avec les fonctions :
void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize nb, char *buf);
void GetStringRegion(JNIEnv *env, jstring str, jsize start, jsize nb, jchar *buf);
Le tableau buf[] doit être assez grand pour recevoir la chaine. On peut obtenir le nombre de caractères des chaines nécessaires à allouer pour la chaine entière par :
jsize GetStringUTFLength(JNIEnv *env, jstring string);
jsize GetStringLength(JNIEnv *env, jstring string);
Pour retourner un jstring depuis le C vers le java, il faut le fabriquer à partir de tableaux de char ou de jchar (cad d'unsigned short), ce que l'on fait avec les fonctions suivantes :
jstring NewStringUTF(JNIEnv *env, const char *chs); // jusqu'au caractère null
jstring NewString(JNIEnv *env, const jchar *chs, jsize length);
Remarque : Toutes ces fonctions sont des membres de la structure JNIEenv *env passée en argument qui sont accédées sous la forme (*env)->fonction(env, ...) en C et sous la forme env->fonction(...) en C++, sans le premier argument env.
Les tableaux java des 8 types primitifs java int, byte, short, long, float, double, char, boolean sont remplacés au niveau du C par les 8 NativeTypes jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray, jbooleanArray.
Pour résumer, disons que le type primitif java []xxx est remplacé au niveau du C par le NativeType jxxxArray.
La taille du tableau est obtenue par :
jsize length = (*env)->GetArrayLength(env, inArray);
Procédure pour accéder à un argument, 2 possibilés :
1) Allocation par le JNI et remplissage d'un tableau jxxx[] buf, pour loger le jxxxArray inArray reçu :
jxxx *buf = (*env)->GetXxxArrayElements(env, inArray, NULL);
ou NULL est le même argument jboolean *iscopy que pour les String.
Ensuite, tester si l'allocation a réussi : if(buf==NULL) ...
2) Allocation classique et remplissage partiel ou total par le JNI :
jxxx *buf = malloc(...)
(*env)->GetXxxArrayRegion(env, inArray, start, length, jxxx buf);
Lorsque le tableau buf n'est plus nécessaire il y a intérêt à le désallouer par :
(*env)->ReleaseXxxArrayElements(env, inArray, buf, 0);
Procédure pour générer un jxxxArray outArray pour un retour à partir d'un jxxx[] buf.
On procède en deux temps :
- 1) Allocation :
jxxxArray outArray = (*env)->NewXxxArray(env, length); // Tester si échec à NULL
- 2) Copie :
(*env)->SetXxxArrayRegion(env, outArray, start, length, buf);
Récapitulation des prototypes :
jxxx *GetXxxArrayElements(JNIEnv *env, jxxxArray array, jboolean *isCopy);
void ReleaseXxxArrayElements(JNIEnv *env, jxxxArray array, jxxx *elems, jint mode);
void GetXxxArrayRegion(JNIEnv *env, jxxxArray array, jsize start, jsize length, jxxx *buffer);
jxxxArray NewXxxArray(JNIEnv *env, jsize length);
void SetXxxArrayRegion(JNIEnv *env, jxxxArray array, jsize start, jsize length, const jxxx *buffer);
void *GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy);
void ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode);
Ces deux dernières routines ne permettent pas les entrées d'appel bloquant entre get et release.
Si la routine C doit renvoyer un type primitif du genre jint, jchar, ... on le crée tout simplement par sa déclaration.
Pour renvoyer un String, on le crée à l'aide des fonctions précédemment vues NewString ou NewStringUTF.
Pour renvoter un tableau ded'élémnts de type primitifs, on le crée à l'aide des fonctions précédemment vues NewXxxArray.
Pour tous les autres objets, leur création est un peu plus complexe et se fait en suivant la procédure suivante :
1) Obtention d'un équivalent-C de la classe java de l'objet à créer :
jclass FindClass(JNIEnv *env, const char *class_name);
où class_name est le nom complet de la classe java. Cette fonction renvoie l'élément cls de type jclass qui est un pointeur sur une structure opaque..
Remarque : D'une manière générale, en dehors des types déjà cités, les types renvoyés par les apis du JNI sont comme jclass un pointeur sur une structure opaque sans importance car l'élémént renvoyé n'est utilisé qu'en argument d'autres fonctions du JNI.
2) Obtention de l'id d'une méthode de cet objet : Pour construire un objet de la classe class_name, on utilise la fonction :
jmethodID GetMethodID(JNIEnv *env, jclass cls, const char *fct_name, const char *sig);
qui renvoie par exemple idconstruct : un id de la méthode de construction de cet objet. Examinons les arguments :
•const char *fct_name = "<init>" est le nom spécial utilisé pour tous les constructeurs. Pour les autres méthodes, on utilise simplement leur nom, par exemple "toString",...
•const char *sig est une chaine synthétique spécifiant le prototype de la fonction : les types de ses arguments et son type de retour.
Dans le cas d'un constructeur sans argument la chaine sig est simplement "()V", c'est-à-dire une paire de parenthèses sans rien à l'intérieur (car pas d'arguments) suivi d'un V (pour void) car le constructeur ne renvoie rien.
Pour spécifier les types des arguments ou du retour on utilise les conventions suivantes :
•pour les types primitifs : I pour int, B pour byte, S pour short, J pour long, F pour float, D pour double, C pour char, et Z pour boolean,
•pour les autres types, on utilise le nom complet de la classe précédée d'un L et suivie d'un ;, en remplaçant les points par des /, par exemple "java.lang.String" devient "Ljava/lang/String;"
Exemples d'arguments sig :
•void fonction_qcq() => "()V"
•double fonction_qcq(int,int) => "(II)D"
•String fonction_qcq() => "()Ljava/lang/String;"
•String fonction_qcq(String) => "(Ljava/lang/String;)Ljava/lang/String;"
•String fonction_qcq(String,String) => "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"
On peut obtenir les prototypes sig des méthodes de la class MyClass avec l'utilitaire javap appliqué au fichier MyClass.class (dont le répertoire est éventuellement accessible via la variable CLASSPATH) :
javap -s -p MyClass
3) Construction de l' instance de l'objet : La méthode
jobject NewObject(JNIEnv *env, jclass cls, jmethodID idconstruct, ...);
va générer l'équivalent C de l'instance de l'objet, par exemple myObj. Les ... sont à remplacer par les arguments du constructeur. C'est ce jobject myObj que la procédure C va renvoyer dans le cas où l'objet a été créé pour être renvoyé.
On peut également créer (allouer) un objet sans appeler de constructeur à l'aide de la méthode suivante :
jobject AllocObject(JNIEnv *env, jclass cls);
4) Appel d'une méthode de l'objet. Pour appeler une méthode de l'objet, il faut d'abord obtenir une id de la méthode en question par la fonction décrite en 2), que nous noterons idMethod. Avec cette valleur, on appelle la méthode à l'aide d'une des fonctions suivantes :
NativeType Call<type>Method(JNIEnv *env, jobject myObj, jmethodID idMethod, ...);
NativeType Call<type>Method(JNIEnv *env, jobject myObj, jmethodID idMethod, ...);
NativeType Call<type>MethodA(JNIEnv *env, jobject myObj, jmethodID idMethod, const jvalue *args);
NativeType Call<type>MethodV(JNIEnv *env, jobject myObj, jmethodID idMethod, va_list args);
ou dans le cas d'une méthode static :
NativeType CallStatic<type>Method(JNIEnv *env, jobject myObj, jmethodID idMethod, ...);
NativeType CallStatic<type>MethodA(JNIEnv *env, jobject myObj, jmethodID idMethod, const jvalue *args);
NativeType CallStatic<type>MethodV(JNIEnv *env, jobject myObj, jmethodID idMethod, va_list args);
ou pour appeler la super.methode) :
NativeType CallNonvirtual<type>Method(JNIEnv *env, jobject myObj, jclass cls, jmethodID idMethod, ...);
NativeType CallNonvirtual<type>MethodA(JNIEnv *env, jobject myObj, jclass cls, jmethodID idMethod, const jvalue *args);
NativeType CallNonvirtual<type>MethodV(JNIEnv *env, jobject myObj, jclass cls, jmethodID idMethod, va_list args);
•Dans le nom de la fonction <type> est à remplacer par le type de retour, c'est-à-dire Xxx pour un type primitif (Int, Char, ...) ou Object pour tout les autres cas.
•Pour les fonctions zzzMethod les ... sont à remplacer par la succession des arguments de la méthode à appeler.
•Pour les fonctions zzzMethodA, const jvalue *args est un tableau où sont mis tous les arguments de la méthode à appeler, jvalue étant une union de tous les types java : typedef union jvalue {jboolean z; jbyte b; jchar c; jshort s; jint i; jlong j; jfloat f; jdouble d; jobject l;} jvalue; Exemple : jvalue zargs[2]; zargs[0].z=JNI_FALSE;zargs[1].i=123;
•Pour les fonctions zzzMethodV les argumenst proviennent d'une va_list. C'est a priori dans le cas où la routine native C à dû être appelée avec un dernier argument que nous nommerons bidon suivi de .... L'appel de zzzMethodV se fait alors selon le protocole suivant :
va_list args;
va_start(args, bidon); // dernier argument précédent les ...
result = Call<type>MethodV(env, myObj, idMethod, args);
va_end(args).
Les deux premières étapes sont celles de la création d'un objet classique :
1) Obtention d'un équivalent-C de la classe java de l'objet à créer :
jclass FindClass(JNIEnv *env, const char *class_name);
où class_name est la classe dont on veut créer un tableau.
XXXXXXXXXXXXXXXXXXXXXXXX
2) Obtention de l'id d'une méthode de cet objet :
jmethodID GetMethodID(JNIEnv *env, jclass cls, const char *fct_name, const char *sig);
avec const char *fct_name = "<init>" dans le cas du constructeur.
3) Construction de l' instance de l'objet :
jobject NewObjectArray(JNIEnv *env, jsize length, jclass cls, jobject initialElement);
Supposons que la classe java appelante possède les membres suivants :
private int number = 88;
private String message = "Hello from Java";
Procédure pour accéder à la valeur d'une variable :
1) Obtention d'un équivalent-C de la classe java appelante (fonction plus simple que FindClass) :
jclass thisClass = (*env)->GetObjectClass(env, thisObj);
2) On obtient l'ID du champ de la variable int number par :
jfieldID fidNumber = (*env)->GetFieldID(env, thisClass, "number", "I");
avec "I" pour type int.
3) On obtient la valeur par (auparavant tester si fidnumber != 0 ) :
jint number = (*env)->GetIntField(env, thisObj, fidNumber);
Procédure pour modifier la valeur d'une variable :
Inversement pour renvoyer une autre valeur, on fera par exemple :
number = 99; (*env)->SetIntField(env, thisObj, fidNumber, number);
Accès : On accède à l'ID d'un type primitif par la fonction :
jfieldID GetFieldID(JNIEnv *env, jclass thisClass, const char *name, const char *sig);
où le dernier argument sig est "I" pour int, "B" pour byte, "S" pour short, "J" pour long, "F" pour float, "D" pour double, "C" pour char, and "Z" pour boolean.
Valeur : On obtient la valeur de la variable par la fonction (auparavant tester fieldID!=NULL) :
NativeType GetXxxField(JNIEnv *env, jobject thisObj, jfieldID fieldID);
où Xxx est à remplacer par Int, Byte, Short, Long, Float Double, Boolean.
Remplacement : On modifie la valeur avec la fonction :
void SetXxxField(JNIEnv *env, jobject thisObj, jfieldID fieldID, NativeType value);
Accès : On obtient l'ID du champ d'une variable String message par la même fonction que pour le type primitif, mais ici, l'argument sig vaudra "Ljava/lang/String;".
jfieldID fidMessage = (*env)->GetFieldID(env, thisClass, "message", "Ljava/lang/String;");
Valeur : On obtient un jString par la fonction (auparavant tester si fidMessage!=NULL) :
jstring message = (*env)->GetObjectField(env, thisObj, fidMessage);
et on obtient les caractères par :
const char *cStr = (*env)->GetStringUTFChars(env, message, NULL);
Modification : Inversement pour renvoyer une autre chaine, on fera par exemple :
message = (*env)->NewStringUTF(env, "Hello from C");
if (NULL == message) return;
(*env)->SetObjectField(env, thisObj, fidMessage, message);
En dehors des cas particulier des types primitifs et des String, on accède aux autres types en tant que type dérivant de la super classe Object.
Accès : On obtient l'ID du champ d'un objet quelconque par la même fonction que pour le type primitif, mais ici, l'argument sig aura la forme suivante "L<nom_java_complet>;" en remplaçant dans le nom java complet les points par des slash (/). Par exemple le java.lang.String devient "Ljava/lang/String;".
Pour les tableaux on inclut le préfixe [, ce qui donne, "[Ljava/lang/Object;" dans le cas d'un tableau d'Object; Pour un tableau d'éléments de type primitif, c'est plus simple, par exemple pour un tableau d'int ce sera tout simplement "[I".
Valeur : A partir du Field ID, on récupère l'équivalent en NativeType de l'Object à l'aide de la fonction :
NativeType GetObjectField(JNIEnv *env, jobject thisObj, jfieldID fieldID);
(idem ci-dessus pour les types primitifs, mais en remplaçant le type primitif Xxx par Object).
Modification : Pour modifier la variable d'instance, on utilise la fonction :
void SetObjectField(JNIEnv *env, jobject thisObj, jfieldID fieldID, NativeType value);
(idem ci-dessus pour les types primitifs, mais en remplaçant le type primitif Xxx par Object).
Les fonctions précédentes sont valables pour des variables dynamiques de la classe java. Pour les variables déclarées static il faut utiliser des variantes de ces fonctions ou le mot Static suit le préfixe Get et Set :
jfieldID GetStaticFieldID(JNIEnv *env, jclass cls, const char *name, const char *sig);
NativeType GetStatic<type>Field(JNIEnv *env, jobject thisObj, jfieldID fieldID);
void SetStatic<type>Field(JNIEnv *env, jobject thisObj, jfieldID fieldID, NativeType value);
On peut appeler depuis le C une autre fonction membre de la classe java donc le fichier C est une méthode native.
Considérons par exempe la méthode String change(String). Pour l'appeler depuis le C, on obtient directement une référence sur la classe java appelante par la fonction :
jclass thisClass = (*env)->GetObjectClass(env, thisObj);
Ensuite on procède comme on l'a déjà vu pour une classe quelconque. Obtention de l'id de la méthode que l'on vet utiliser :
jmethodID id = (*env)->GetMethodID(env, thisClass, "change", "(Ljava/lang/String;)Ljava/lang/String;");
if (id == NULL) return;
On prépare les arguments. Par exemple ici un jstring pour mettre à la place du String :
const char *texte = "Bonjour Michel";
jstring oldmes = (*env)->NewStringUTF(env, texte);
On appelle la méthode qui va retourner un jstring :
jstring newmes = (*env)->CallObjectMethod(env, thisObj, id, oldmes);
Suite faculative. Ici, pour utiliser le jstring résultat, on le convertit en char[] :
const char *newtexte = (*env)->GetStringUTFChars(env, newmes, NULL);
if (NULL == newtexte) return; /* ... */
Et on libère les allocations :
(*env)->ReleaseStringUTFChars(env, newmes, newtexte);
On peut utiliser la classe Clavier de Claude Delannoy qui fournit lireString(), lireInt(), lireFloat(), et lireDouble().
import java.io.* ;
public class Clavier
{
public static String lireString () // lecture d'une chaine
{ String ligne_lue = null ;
try
{ InputStreamReader lecteur = new InputStreamReader (System.in) ;
BufferedReader entree = new BufferedReader (lecteur) ;
ligne_lue = entree.readLine() ;
}
catch (IOException err)
{ System.exit(0) ;
}
return ligne_lue ;
}
public static float lireFloat () // lecture d'un float
{ float x=0 ; // valeur a lire
try
{ String ligne_lue = lireString() ;
x = Float.parseFloat(ligne_lue) ;
}
catch (NumberFormatException err)
{ System.out.println ("*** Erreur de donnee ***") ;
System.exit(0) ;
}
return x ;
}
public static double lireDouble () // lecture d'un double
{ double x=0 ; // valeur a lire
try
{ String ligne_lue = lireString() ;
x = Double.parseDouble(ligne_lue) ;
}
catch (NumberFormatException err)
{ System.out.println ("*** Erreur de donnee ***") ;
System.exit(0) ;
}
return x ;
}
public static int lireInt () // lecture d'un int
{ int n=0 ; // valeur a lire
try
{ String ligne_lue = lireString() ;
n = Integer.parseInt(ligne_lue) ;
}
catch (NumberFormatException err)
{ System.out.println ("*** Erreur de donnee ***") ;
System.exit(0) ;
}
return n ;
}
// programme de test de la classe Clavier
public static void main (String[] args)
{ System.out.println ("donnez un flottant") ;
float x ;
x = Clavier.lireFloat() ;
System.out.println ("merci pour " + x) ;
System.out.println ("donnez un entier") ;
int n ;
n = Clavier.lireInt() ;
System.out.println ("merci pour " + n) ;
}
}
Crée une fenêtre avec comme principales méthodes setSize, setTitle et setVisible. On peut créer une JFrame comme suit :
import javax.swing.* ;
public class Premfen0
{ public static void main (String args[])
{ JFrame fen = new JFrame() ;
fen.setSize (300, 150) ;
fen.setTitle ("Ma premiere fenetre") ;
fen.setVisible (true) ;
}
}
Résultat :
Mais de manière plus classique on crée une classe qui étend JFrame, comme ceci :
import javax.swing.* ;
class MaFenetre extends JFrame
{ public MaFenetre () // constructeur
{ setTitle ("Ma premiere fenetre") ;
setSize (300, 150) ;
}
}
public class Premfen1
{ public static void main (String args[])
{ JFrame fen = new MaFenetre() ;
fen.setVisible (true) ;
}
}
Remarque :
Au lieu de setSize, on peut utiliser setBounds(DecalX, DecalY, largeur, hauteur).
On obtient les dimensions de l'écran par :
Toolkit tk = Toolkit.getDefaultToolkit();
Dimension dimEcran = tk.getScreenSize();
int larg = dimEcran.width, haut = dimEcran.height;
Une fenêtre est constituée de plusieurs parties dont en particulier la barre de titre avec ou sans menu et le panneau principal qui contiendra du texte des images, des dessins, des boutons...
Le panneau principal appartient à la classe Container. On une instance par l'instruction :
Container panneau = this.getContentPane();
Il existe de nombreuses manières pour mettre des composants dans ce panneau. Il existe en particulier des méthodologies de disposition ou gestionnaire de mise en forme qu'on appelle des Layouts et qui sont mise en action par l'instruction :
panneau.setLayout(myLayout);
dont les plus courants sont présentées dans les paragraphes suivants. Mais on peut également se passer de ces gestionnaire en faisant panneau.setLayout(null) mais dans ce cas il faudra définir soi-même la place et la taille des composants à l'aide de leur méthode setBounds(x,y,dx,dy).
Le plus simple à utiliser. Mise des composants les uns à la suite des autres, comme les lettres d'un texte, avec retour en début de ligne suivante quand il n'y a plus de place sur la ligne en cours. La taille des composants est respectée. Si on modifie la taille de la fenêtre et qu'ils ne peuvent pas tous contenir, les derniers sont coupés.
On définit le layout et on l'affecte au panneau :
FlowLayout disposition = new FlowLayout();
panneau.setLayout(disposition);
ou, plus simplement, lorsque l'argument disposition n'est utilisé nulle part ailleurs :
panneau.setLayout(new FlowLayout());
Ensuite, les composants sont ajoutés à la queue leu leu dans le panneau par sa méthode add :
panneau.add(composant);
Remarquer que la méthode add est une méthode du Container et non pas du Layout.
Pour disposer de la place libre on peut centrer, cadrer à droite ou à gauche en spécifiant FlowLayout.CENTER, FlowLayout.RIGHT ou FlowLayout.LEFT en premier paramètre de la définition du FlowLayout :
contenu.setLayout (new FlowLayout(FlowLayout.RIGHT, 10, 5)) ;
10 et 5 sont les marges horizontales et verticales entre les composants.
Mise de composants le long des bords haut, bas, droite, gauche ou au centre. C'est ce layout qui est utilisé par défaut. On précise la position par une des valeurs BorderLayout.NORTH ou "north", BorderLayout.SOUTH ou "south", BorderLayout.EAST ou "east", BorderLayout..WEST ou "west", BorderLayout.CENTER ou "center" (par défaut) mise en 2ème argument de la méthode add.
Si on met plusieurs composant dans la même zone, seul le dernier est visible.
Mise les composants les uns devant les autres (comme dans un paquet de cartes). Le dernier au-dessus du paquet, cache les précédents.
Etand donné un panneau
JPanel panCard = new JPanel() ;
contenu.add (panCard) ;
L'instruction CardLayout pile = new CardLayout (10,5) ; crée un CardLayout avec 10 pixels de marges à droite et à gauche et 5 pixels de marge en haut et en bas.
On affecte ce Layout au panneau par l'instruction panCard.setLayout (pile) ;
puis on ajoute des compoasants au panneau panCard.add(composant, "Nom du composant") ...
et ensuite on peut changer celui qui est affiché au moyen des méthodes :
pile.previous (panCard) ; // affiche le précédent
pile.next (panCard) ; // affiche le suivant
pile.first (panCard) ; // affiche le premier
pile.last (panCard) ; // affiche le dernier
L'instruction contenu.setLayout (new GridLayout(4, 3, 10, 5)) ; définit une grille de 4 lignes de 3 colonnes, avec des marges de 10 et 5 en horizontal et vertical.
Les composantes sonts ajoutés à la suite, ligne par ligne. Si on diminue la taille de la fenêtre la taille des cellules, toutes de la même taille, est diminuée.
Les Box ne sont pas des layouts mais des sous-containers qui jouent le rôle de layout horizontal pour disposer des composants en ligne ou vertical pour disposer des composants en colonne. Ils sont définis par les instructions :
Box hBox = Box.createHorizontalBox() ;
ou
Box vBox = Box.createVerticalBox() ;
et ajoutés au container :
contenu.add(hBox) ; ou contenu.add(vBox) ;
Ensuite on peut ajouter des composants dans ces Box : hBox.add (comp); ou vBox.add (comp);
La classe Box possède 2 méthodes qui créent des composants que l'on peut ajouter avec la méthode add du Box pour créer des espaces vierges. Par exemple :
hBox.add (Box.createVerticalStrut(10)) ;
ajoute un espace vierge de 10 pixels au niveau de ce pseudo-composant, et l'instruction :
bVert.add (Box.createGlue()) ;
va créer un composant invisible qui prendra le maximum d'espace disponible.
Le plus souple, mais assez complexe à mettre en oeuvre. Voir Claude Delannoy, chapitre 17.6, page 492.
La JFrame (ou un composant) peut recevoir les clics de la souris ajoutant un écouteur des clics souris qu'on appelle un MouseListener au moyen de l'instruction addMouseListener(EcouteurSouris); où EcouteurSouris est une classe qui implémente l'interface MouseListener. La classe en cours elle-même peut implémenter cette interface, et à ce moment l'EcouteurSouris c'est-elle-même, et l'instruction d'ajout deviendra addMouseListener(this) ;
Quand on implante l'interface MouseListener il faut définir les méthodes mouseClicked, mousePressed, mouseReleased, mouseEntered et mouseExited.
Dans l'exemple suivant, comme on ne réagit qu'à mouseClicked , c'est la seule méthode qui contient des instructions.
import javax.swing.* ;
import java.awt.event.* ; // pour MouseEvent et MouseListener
class MaFenetre extends JFrame implements MouseListener
{
public MaFenetre() // constructeur
{
setTitle ("Gestion de clics") ;
setBounds (10, 20, 300, 200) ;
addMouseListener(this) ; // ajout du listener
}
// On définit toutes les méthodes de l'interface
public void mouseClicked(MouseEvent ev)
{ System.out.println ("clic dans fenetre") ;}
public void mousePressed (MouseEvent ev) {}
public void mouseReleased(MouseEvent ev) {}
public void mouseEntered (MouseEvent ev) {}
public void mouseExited (MouseEvent ev) {}
}
public class Clic1
{
public static void main (String args[])
{
MaFenetre fen = new MaFenetre() ;
fen.setVisible(true) ;
}
}
Remarque : Dans les méthodes de l'interface
int x = ev.getX() ;
int y = ev.getY() ;
fournissent les coordonnées de la souris.
Implanter une interface c'est fastidieux car il faut définir toutes ses méthodes. Heureusement la package java.awt.event fournit la classe MouseAdapter qui implémente l'interface MouseListener et définit toutes les méthodes (avec aucune action prédéfinie dans ces définitions). Ainsi, en faisant hériter une classe EcouteurSouris de MouseAdapter, cela revient à implémenter MouseListener, sans avoir besoin de définir les méthodes. Exemple :
import javax.swing.*;
import java.awt.event.*;
class MaFenetre extends JFrame {
MaFenetre() // constructeur
{
setTitle("Gestion de clics");
setBounds(10, 20, 300, 200);
addMouseListener(EcouteurSouris);
}
}
// EcouteurSouris ne définit que mouseClicked
class EcouteurSouris extends MouseAdapter {
public void mouseClicked(MouseEvent ev) {
int x = ev.getX();
int y = ev.getY();
System.out.println("clic au point de coordonnees " + x + ", " + y);
}
}
public class Clic3bis {
public static void main(String args[]) {
MaFenetre fen = new MaFenetre();
fen.setVisible(true);
}
}
Ci-dessus, on crée une classe EcouteurSouris qui hérite de MouseAdapter, et notre classe MaFenetre l'ajoute au moyen de l'instruction addMouseListener(EcouteurSouris);.
En programmation moderne, une classe comme EcouteurSouris qui ne sert qu'à un seul endroit est généralement définie directement à l'endroit où elle est sert, sans lui donner de nom, et comme on se passe de lui donner un nom (qui ne sert à rien), on l'appelle classe anonyme, comme dans l'exemple suivant :
import javax.swing.*;
import java.awt.event.*;
class MaFenetre extends JFrame {
MaFenetre() // constructeur
{
setTitle("Gestion de clics");
setBounds(10, 20, 300, 200);
addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent ev) {
int x = ev.getX();
int y = ev.getY();
System.out.println("clic au point de coordonnees " + x + ", " + y);
}
});
}
}
public class Clic3 {
public static void main(String args[]) {
MaFenetre fen = new MaFenetre();
fen.setVisible(true);
}
}
Le JPanel est une sous-fenêtre qui peut contenir d'autres composants ou d'autre sous-fenêtres. On peut l'utiliser pour découper la fenêtre principale en sous-fenêtres.
On peut changer la plupart de ses caractéristiques, par exempe la couleur du fond :
pan.setBackground(Color.yellow) ; // couleur de fond = jaune
De plus on peut dessiner dans un JPanel, ou plus précisément dans une classe qui étend la classe Jpanel. Pour cela, il suffit de redéfinir la méthode public void paintComponent(Graphics g) de cette classe. En effet c'est cette méthode qui est appelé chaque fois qu'il faut dessiner le JPanel. On mettra donc à l'intérieur de la méthode qu'on redéfinit nos ordres de tracé en utilisant pour cela le contexte graphique g.
import javax.swing.* ;
import java.awt.* ;
class MaFenetreD extends JFrame
{ MaFenetreD ()
{ setTitle ("Essai dessins") ;
setSize (300, 150) ;
pan = new Panneau() ;
getContentPane().add(pan) ;
pan.setBackground(Color.yellow) ; // couleur de fond = jaune
}
private JPanel pan ;
}
class Panneau extends JPanel
{ public void paintComponent(Graphics g)
{ super.paintComponent(g) ;
g.drawLine (15, 10, 100, 50) ;
}
}
public class PremDes
{ public static void main (String args[])
{ MaFenetreD fen = new MaFenetreD() ;
fen.setVisible(true) ;
}
}
Remarquer lors de la redéfinition de paintComponent la première instruction qui appelle la méthode de son constructeur : super.paintComponent(g) ;.
On peut dessiner des lignes, des rectangles, des ovales, ...
g.drawLine (15, 10, 100, 50) ;
g.drawOval (80, 20, 120, 60) ;
g.drawRect (80, 20, 120, 60) ;
Il est généralement préférable de regrouper les ordres de tracé de dessin dans la méthode paintComponent, mais on peut aussi dessiner ailleurs à l'aide du contexte graphique du JPanel pan que l'on obtient par l'instruction Graphics g = pan.getGraphics() mais qu'il faut restituer dès la fin du tracé par l'instruction g.dispose().Exemple de dessin de petits rectangles à l'emplacement des clics souris effectués dans l'écouteur de ces clics :
import javax.swing.* ;
import java.awt.* ;
import java.awt.event.* ;
class MaFenetreT extends JFrame
{ MaFenetreT ()
{ setTitle ("Traces de clics") ;
setSize (300, 150) ;
pan = new JPanel() ;
getContentPane().add(pan) ;
pan.addMouseListener (new EcouteClic(pan)) ;
}
private JPanel pan ;
}
class EcouteClic extends MouseAdapter
{ public EcouteClic (JPanel pan)
{ this.pan = pan ;
}
public void mouseClicked (MouseEvent e)
{ int x = e.getX(), y = e.getY() ;
Graphics g = pan.getGraphics() ;
g.drawRect (x, y, 5, 5) ;
g.dispose();
}
private JPanel pan ;
}
public class TrClics1
{ public static void main (String args[])
{ MaFenetreT fen = new MaFenetreT() ;
fen.setVisible(true) ;
}
}
Remarque : Le dessin effectué par cette méthode est perdu si on redimensionne la fenêtre, car la méthode paintComponent du paneau sera appelé avec ien à tracer. Voici comment il faut procéder pour conserver ce tracé : il faut mémoriser les emplacements des clics :
import javax.swing.* ;
import java.awt.* ;
import java.awt.event.* ;
class MaFenetreT2 extends JFrame
{ MaFenetreT2 ()
{ setTitle ("Traces de clics") ;
setSize (300, 150) ;
pan = new PaneauT2() ;
getContentPane().add(pan) ;
}
private PaneauT2 pan ;
}
class PaneauT2 extends JPanel
{ final int MAX = 100 ;
public PaneauT2 ()
{ abs = new int[MAX] ; ord = new int[MAX] ;
nbclics = 0 ;
addMouseListener (new MouseAdapter()
{ public void mouseClicked (MouseEvent e)
{ if (nbclics < MAX)
{ abs[nbclics] = e.getX() ;
ord[nbclics] = e.getY() ;
nbclics++ ;
repaint() ;
}
}
}) ;
}
public void paintComponent (Graphics g)
{ super.paintComponent(g) ;
for (int i = 0 ; i < nbclics ; i++)
g.drawRect (abs[i], ord[i], 5, 5) ;
}
private int abs[], ord[] ;
private int nbclics ;
}
public class TrClics2
{ public static void main (String args[])
{ MaFenetreT2 fen = new MaFenetreT2() ;
fen.setVisible(true) ;
}
}
Dans paintComponent on obtient la taille du panneau par Dimension dim = getSize();
Quand on modifie le contenu d'un JPanel au niveau des composants on peut demander qu'il actualise son apparence en appelant sa méthode validate().
Le composant le plus important est le JPanel qui est une sous-fenêtre qui peut contenir d'autres composants ou d'autre sous-fenêtres. De plus on peut dessiner dans un JPanel.
Parmi les autres composants que l'on affiche dans un panneau,les plus utilisés sont :
JButton : bouton à activer
JCheckBox : case à cocher
JRadioButton : cases à cocher mutuellement exclusives lorsqu'elles sont ajoutées à un ButtonGroup en plus d'être ajoutées au Layout.
JLabel : Ligne de texte, sans cadre
JtextField : Ligne de texte éditable dans un cadre
JList : liste de texte à sélectionner
JComboBox : liste déroulante
...
Tous ces composants peuvent être supprimés, désactivés, réactivés... et possèdent tous les méthodes suivantes :
revalidate() pour forcer le réaffichage après une modif (ou appeler le validate() du conteneur).
setEnabled(true/false); pour rendre actif/inactif
isEnabled(); pour demander si actif ou inactif.
On peut essayer de définir la taille d'un composant par
composant.setPreferredSize(New Dimension(l,h));
Les boutons sont de la classe JButton. On crée un bouton, généralement en donnant le texte qu'il contient, comme suit :
monBouton1 = new JButton("Bouton A");
et on l'ajoute dans le panneau par : panneau.add(monBouton1);
Un clic sur un bouton n'est pas considéré comme un événement clic souris mais comme ce qu'on appelle une Action. Ce n'est donc pas un MouseListener qui écoute cette Action mais un ActionListener qui n'impose qu'une seule méthode à définir, qui se nomme actionPerformed(ActionEvent ev). Pour l'écouter, on ajoute un écouteur de cette Action au bouton considéré (alors que le MouseListener était ajouté à la fenêtre) par l'instruction :
monBouton1.addActionListener(this);
Ici l'écouteur est this, c'est-à-dire la fenêtre car on a choisi de l'utiliser pour implémenter l'ActionListener.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
class Fen2Boutons extends JFrame implements ActionListener {
public Fen2Boutons() {
setTitle("Avec deux boutons");
setSize(300, 200);
JButton monBouton1 = new JButton("Bouton A");
JButton monBouton2 = new JButton("Sortir");
Container panneau = getContentPane();
panneau.setLayout(new FlowLayout());
panneau.add(monBouton1);
panneau.add(monBouton2);
monBouton1.addActionListener(this);
monBouton2.addActionListener(this);
}
public void actionPerformed(ActionEvent ev) {
String nom = ev.getActionCommand();
if (nom.compareToIgnoreCase("Sortir") == 0)
System.exit(0);
else
System.out.println("Action sur bouton " + nom);
}
}
public class Boutons3 {
public static void main(String args[]) {
Fen2Boutons fen = new Fen2Boutons();
fen.setVisible(true);
}
}
Dans cet exemple l'écouteur est le même pour deux boutons. C'est this, c'est-à-dire la fenêtre elle-même qui définit les actions à entreprendre. Comme il y a plusieurs boutons qui déclenchent l'action, pour savoir quelle action entreprendre, on récupère le texte du bouton au moyen de l'instruction
String nom = ev.getActionCommand();
et on écrit l'action à réaliser en conséquence (écrire le nom du bouton ou sortir).
On peut également associer son ActionListener à chaque bouton, par exemple comme ceci :
monBouton1.addActionListener(new ActionListener() {public void actionPerformed (ActionEvent ev)
{ System.out.println ("action sur bouton 1") ;
}} );
On peut également créer une classe d'écoute des boutons prenant pour argument un numéro de bouton, par exemple :
class EcouteBouton implements ActionListener
{
private int n ;
public EcouteBouton (int n){ this.n = n ;}
public void actionPerformed (ActionEvent ev)
{
if(n == 2) System.exit(0);
else System.out.println ("action sur bouton " + n) ;
}
}
et associer cet écouteur à chaque bouton :
monBouton1.addActionListener(new EcouteBouton(1));
monBouton2.addActionListener(new EcouteBouton(2));
1Le code unicode java est limité à 65536 caractères chacun codé sur 2 octets
2Le String java est une classe complexe avec de nombreux paramètres. Seuls les caractères sont récupérés en C.
3En UTF-8 les caractères ASCII sont codés sur 1 octet, et les autres caractères du code unicode java sont codés sur 1 à 3 octets.