Outils pour utilisateurs

Outils du site


developpement:java:avance

Programmation Java avancée

Cette page est surtout question de la mise en oeuvre de la qualité d'une application en démontrant de nouveaux concepts à l'aide du langage Java.

La gestion des exceptions

Avant de débuter, il faut faire un rappel sur les types d'erreurs en programmation :

  • Lexico-syntaxique
  • Logique
  • Exécution
  • Situations d'exceptions
    • Exemple → peut-être dû à un problème de la machine-virtuelle
    • Exemple → dû au mauvais comportement des utilisateurs

Le mécanisme de la gestion des erreurs

Auparavant, sans le mécanisme de gestion des exceptions, la programmation se faisait de style :

if (situation_normale) {
  traitement(normal);
}
else {
  traitement(exception);
}


Buts :

  • Séparer le code de déroulement normal du code de gestion des erreurs
  • Remédier à la situation où on ne peut pas gérer le problème à l'endroit où il se pose


Principe :

  • Une erreur/exception est modélisée sous forme d'un objet1) qui encapsule toutes les informations sur l'exception qui s'est produite.
  • Le déroulement du programme est interrompu et on se branche vers un gestionnaire d'exception.
class Cercle implements Comparable
..
 
public int compareTo(object c) {
  if (c instanceof Cercle) {
    derourlement(normal);
  }
  else {
    throw new ClassCastException("ne peut comparer un cercle avec autre objet");
  }
}

Il est rare qu'un programmeur doive créer de nouvelles classes d'exceptions. L'API contient beaucoup de cas :

  • Fichier non-trouvé → FileNotFoundException
  • Division par zéro → DividedByZeroException
  • Etc

Hiérarchie d'exceptions

Hiérarchie des exceptionsLes exceptions sont des objets de type Throwable et il y a ensuite deux grandes catégories : Error et Exception. Les erreurs n'ont rien a voir avec la programmation. C'est plutôt lié à l'environnement de développement.

Exception dérive plusieurs classes : RuntimeException et les autres. Dans RuntimeException il y a par exemple:

  • ArithmeticException
  • ClassCastException
  • IndexOutOfBoundException
  • NullPointerException
  • IllegalFormatException
  • et autres…

Dans l'autre partie il y a :

  • InterruptedException
  • CloneNotSupportedException
  • IOException
    • FileNotFoundException

Les excepions sont divisées en deux grandes catégories :

  • Exceptions vérifiées (checked exceptions) → Elles peuvent faire l'objet d'un try.. catch
  • Exceptions non-vérifiées (hors contrôle, unchecked exceptions)

Gestion des exceptions vérifiées

Le code original peut poser des problèmes :

public class Util {
  public static Double getSurface(double rayon) {
    return Math.PI * Math.pow(rayon, 2);
  }
  public double getRayon(double surface) {
    return Math.sqrt(surface/Math.PI);
  }
}

Le même code avec la gestion des exceptions :

public class Util {
  public static Double getSurface(double rayon) {
    return Math.PI * Math.pow(rayon, 2);
  }
  public double getRayon(double surface) throws Exception {
    if (surface >= 0) {
      return Math.sqrt(surface/Math.PI);
    }
    else {
      throw new Exception("La surface ne peut être négative");
    }
  }
}
pubic class UnProgramme {
  public static void main (String args[]) {  // ne pas mettre de 'throws Exception'
    double s;
    saisie(s);
 
    try {
      double rayon = Utils.getRayon(s);
    }
    catch {  // gestionnaire d'exception
      JOptionPane.showMessageDialog(null, "Entrez a nouveau une surface positive");
      // S'arranger pour boucler tant que c'est positif
    }
  }
}

Un try..catch peut gérer plusieurs types d'exceptions :

try {
  ...
  ...
}
catch (IOException x) {
 
}
catch (InterruptedException y) {
 
 
 
 
 
}

Si une classe Exception CE2 est fille d'une autre classe Exception CE1 il faut faire attention :

try {
  ..exception CE1
  ..exception CE2
}
catch (CE1 x) {
  ..
}
catch (CE2 y) {
  ..
}

Dans le cas du code ci-dessus, si une exception de type CE2 est levée, le code qui gère l'exception (catch (CE2 y)) ne sera jamais exécutée, puisque la classe CE2 est un CE1, alors ce sera le code du catch (CE1 x) qui sera exécuté.

La clause Finally

Cette partie du code est toujours exécutée, qu'il y ait une exception ou non. Sert habituellement à libérer des ressources, comme fermer une connexion à une base de données ou fermer un fichier.

try {
 
}
catch (Exception) {
 
}
finally {
 
}

Les entrées / sorties

Lorsqu'on a un programme qui s'exécute sur un poste, a des relations avec l'extérieur, il a des entrées (lien provenant de l'extérieur) et des sorties (lien vers l'extérieur). Que le flux soit vers un réseau ou une écriture vers une disquette, la méthode reste la même.

Un flux est un « tuyeau » donnant l'accès vers l'extérieur. Il y a un flux pour l'entrée et un flux pour la sortie, donc les flux ont un seul sens. On ne peut pas naviguer (avancer/reculer) dans un flux, contrairement aux fichiers à accès direct.

Les flux

Flux d'octets

Flux d'entrée

Pour les flux d'octets, il y a les classes abstraites InputStream et OutputStream.
Dans InputStream il y a essentiellement deux méthodes :

  1. int read() qui lit un octet
  2. int read(t[]) qui lit un tableau d'octets
  3. int read(t[], int start, int length) qui lit un tableau d'octets avec le début et la longueur spécifiées


Il existe plusieurs classes qui descendent de InputStream :

  1. FileInputStream
  2. ByteArrayInputStream
  3. StringInputStream
  4. …InputStream

Leur rôle est d'apdapter (ou d'implémenter) la méthode abstraite read() en fonction de la source des données.

Flux de sortie

Empilement de flux (flux filtré)

Hiérarchie des flux Il existe une classe qui descend de InputStream qui s'appelle FilterInputStream

// lire un fichier octet par octet
FileInputStream f = new FileInputStream("C:\fichier.dat");
while (...) {
  int x = f.read();
}
 
// ajout d'un buffer
BufferedInputStream bf = new BufferedInputStream(f);
while (..) {
  int x = bf.read();
}
 
//lire des entiers :
DataInputStream = df = new DataInputStream(bf);
while (...) {
  int x = df.readInt();
}

Le code ci-haut est équivalent à :

DataInputStream df3 = new DataInputStream(new BufferedInputStream(new FileInputStream("c:\\donnes.dat")));

Flux d'ojets (sérialisation)

Ancienne méthode

Sans la sérialisation, on peut écrire une donnée primitive, exemple un int :

DataOutputStream d = new DataOutputStream(new FileOutputStream("C:\\Fichier.dat");
int x=35;
d.writeInt(x);

Écrire un objet, exemple un Point :

public class Point {
  private double abscisse, ordonnee;
 
  public void ecrire(OutputStream o) {
    DataOutputStream = new DataOutputStream(o);
    d.writeDouble(abscisse);
    d.writeDouble(ordonnee);
  }
}

Exemple avec un cercle qui contient un autre objet (Point) :

public class Cercle {
  private Point centre;
  private double rayon;
  public void ecrire(OutputStream o) {
    centre.ecrire(o);
    DataOutputStream = new DataOutputStream(o);
    d.writeDouble(rayon);
  }
}

Le problème avec cette méthode → Imaginons deux cercles ayant le même centre, c'est-à-dire que les cercles référencent le même objet Point. Lorsque vient le temps de lire, chaque cercle aura son propre centre qui auront les mêmes valeurs. Donc deux Point sont recréés au lieu d'un seul comme originalement.

Nouvelle méthode

La solution consiste à utiliser la sérialisation. Il faut que l'objet implémente l'interface de marquage Serializable qui vient de java.io.Serializable.

Si un objet sérialisable contient d'autres objets, ces objets doivent être aussi sérialisables. Les types de données primitives sont toutes sérialisables, mais des objets d'interface comme le JButton ne le sont pas. D'où l'importance d'utiliser le pattern MVC.

ObjectOutputStream
writeObject
readObject

Dans la déclaration de la classe, il faut implémenter Serializable, qui se trouve dans java.io.

import java.io.Serializable;
 
public class Point implements Serializable {

Flux de caractères

Comme les applications Java utilisent le système de caractères Unicode et que le système d'exploitation peut en utiliser un autre, exemple ASCII ou UTF-8, les flux de caractères interviennent pour faire la conversion.

Pour lire un fichier texte :

  • FileReader lit un caractère à la fois.
  • BufferedReader comme FileReader peut lire un caractère, mais il offre aussi d'autres méthodes comme readLine().

Pour écrire un fichier texte :

  • FileWriter peut écrire un tableau de caractères, ou une chaîne, etc, en utilisant la méthode write().
  • PrintWriter est une classe utilitaire qui a des méthodes comme print() et println().

Fichiers à accès direct

Fichiers d'accès direct et ses dépendances

Contrairement aux fluex d'entrées/sorties dans un fichier à accès direct :

  1. On peut l'ouvrir en lecture, écriture ou les deux
  2. On peut naviguer (avancer/reculer) à travers son contenu

Constructeurs

  1. RandomAccessFile(String nomFichier, String modeOuverture)
  2. RandomAccessFile(File fichier, String modeOuverture)

Les modes d'ouverture sont r et rw.

Principales méthodes

  1. Méthodes de DataInput : readInt(), readDouble()
  2. Méthodes de DataOutput : writeInt(), writeDouble()
  3. void seek(long position) → déplace la tête de lecture/écriture à la position spécifié.
  4. long getFilePointer() → retourne la position de la tête de lecture.
  5. long length() → retourne la taille du fichier.

Pour les méthodes seek(), getFilePointer() et length() l'unité de mesure c'est l'octet.

En ouvrant un fichier en mode lecture/écriture (rw), le fichier visé n'est pas écrasé, mais la tête de lecture se trouve au début. Donc, pour ajouter des données, il faut déplacer la position.

Collections

[Graphique décrivant les liens entres les différentes classes de collections] Java fournit plusieurs interfaces et classes (abstraites et concrètes) pour les collections. Cela permet de :

  • Séparer l’interfaced’une collection deson implémentation.
  • Fournir un cadre communà toutes les collections.
  • Faciliter la définition des types de données abstraits.

Une file (queue) peut être implémentée de différentes façons (liste chaînée ou tableau) mais on peut imposer une interface indépendante de toute implémentation. L’interface fournira par exemple trois méthodes : ajouter(), supprimer() et taille().

Interface Collection

Constitue la première interface servant de base à toutes les collections.

Fournit plusieurs méthodes : add(), iterator(), addAll(), size(), isEmpty(), etc.

Plusieurs méthodes de Collection peuvent être implémentées (les algorithmes sont standards). Par exemple :

public static boolean addAll(Collection a, Collection b) { 
  Iterator i = b.iterator(); 
  boolean modifie = false; 
  while (i.hasNext()) 
    if (a.add(i.next())) modifie = true; 
  return modifie; 
} 

Ces méthodes (sauf add() et iterator()) sont implémentées par la classe abstraite AbstractCollection.

public bolean isEmpty() {
  return (size()==0);
}

La méthode suivante, addAll, peut retourner false dans deux cas :

  1. La collection b est vide
  2. La collection a est un ensemble et qu'un même élément se trouve dans la collection b.
public static bolean addAll(Collection a, Collection b) {
 
 
}

Interface Iterator

C'est la deuxième interface de base. Elle sert à créer et utiliser des itérateurs sur une collection.

Fournit 3 méthodes : next(), hasNext() et remove().

Un itérateur sert de « tête de lecture/écriture » sur une collection (une sorte de curseur).

L'interface Iterator n'a pas de méthode add() car ajouter à une position spécifique n'a de sens que si la position des éléments est importante. Ce qui est le cas des collections ordonnées (liste chaînée, par exemple), mais pas des collections non ordonnées (ensemble, par exemple).

Pour accéder à tous les éléments d'une liste chaînée, il est préférable de le faire avec l'itérateur qui offre une méthode next().

Avant Java 5 :

LinkedList l = new LinkedList();
 
// code d'ajout de cercles dans l
int i = 27;
Cercle c = (Cercle) l.get(i);  // retourne un object

Avec Java 5 et plus :

LinkedList<Cercle> l = new LinkedList<Cercle>();
 
// code d'ajout de cercles dans l
int i = 27;
Cercle c = l.get(i);  // retourne un objet Cercle, pas de transtypage

Les listes chaînées

Les tableaux et les vecteurs ont des inconvénients : les opérations d'ajout et de suppression sont très gourmandes (il faut décaler une partie des éléments).

En Java les listes sont doublement chaînées. Une liste est une collection dans laquelle la position des éléments est importante.

Java propose la classe concrète LinkedList qui dérive de AbstractCollection et implémente l'interface List qui dérive de Collection.

La méthode LinkedList.add() permet d’ajouter un élément à la fin de la liste.

Pour ajouter à une position quelconque, il faut un itérateur (qui stocke la position courante).

Comme Iterator n’a pas de méthode add(), Java en fait dériver une interface ListIterator qui ajoute une méthode add() et trois méthodes hasPrevious(), previous() et set() qui permet de modifier la valeur de l'élément qui vient d'être parcouru par next() ou previous().

Autres types de collections

Ensembles

C'est une collection dont les éléments sont uniques. Ils sont représentés par l’interface Set :

public interface Set extends Collection{
  int size(); 
  boolean isEmpty(); 
  boolean contains(Object element); 
  boolean add(Object element); 
  boolean remove(Object element); 
  Iterator iterator(); 
  boolean containsAll(Collection c); 
  boolean addAll(Collection c); 
  boolean removeAll(Collection c); 
  boolean retainAll(Collection c); 
  void clear(); 
  Object[] toArray(); 
  Object[] toArray(Object a[]); 
}

Cartes

C’est une collection pour stocker des paires clé/valeur (dictionnaires). Elle est représenté par l’interface Map :

public interface Map { 
  Object put(Object key, Object value); 
  Object get(Object key); 
  Object remove(Object key); 
  boolean containsKey(Object key); 
  boolean containsValue(Object value); 
  int size(); 
  boolean isEmpty(); 
  void putAll(Map t); 
  void clear(); 
  public Set keySet(); 
  public Collection values(); 
  public Set entrySet(); 
}

L’interface Map est implémentée par deux classes :

  • HashSet : préférable pour la suppression et l'ajout d'éléments
  • TreeSet : préférable pour la recherche d'éléments

Le multithreading

  1. Parallelisme vrai → multiprocesseur
  2. Pseudo-parallelisme → 1 processeur dont le temps est partagé par plusieurs processus

Processus : un programme en exécution, ils ont leurs propres données et traitements

Processeur : dispositif capable d'exécuter des instructions

Chaque processus possède ses propres données et son traitement

Deux types de pseudo-parrallelisme

  1. Le mutitâche coopératif : basé sur la ccopération entre les processus : un processus qui détient le processeur le libère volontairement
  2. Le multitâche préemptif : le temps est divisé en intervales et le système d'exploitation alloue ces intervales aux processus à tour de rôle.

Thread

Traduction : fil d'exécution

Processus qui s'exécute au sein d'une application. Les threads d'une même application se partagent les mêmes données.

États

  1. Nouveau : instancié, mais pas encore démarré
  2. Actif : après l'appel de la méthode start() il est actif (Alive)
    1. Exécutable
    2. Bloqué : avec sleep()
  3. Mort : fin du run()

Priorités

En Java il y a dix niveaux de priorité.

Thread.MIN_Priority = 1
...
Thread.NORM_PRIORITY = 5
...
Thread.MAX_PRIORITY = 10

Un Thread hérite par défaut de la priorité de son Thread parent. On peut modifier la priorité avec setPriority(int).

Les systèmes d'exploitations ont d'autres niveaux de priorité :

  1. Basse
  2. Normal
  3. Élevée

Comme les systèmes d'exploitation peuvent gérer différemment les priorités, il peut être préférable de gérer soi même les priorités avec sleep() ayant une valeur plus élevée pour les Threads ayant une plus basse priorité.

Création d'un thread

Il y a deux façons:

  1. Étendre la classe Thread
  2. Implémenter l'interface Runnable

Étendre la classe Thread

Créer une classe qui dérive de Thread et qui a une méthode à surdéfinir run().

public class MonThread1 extends Thread {
  public void run() {
    //traitment du thread
  }
}
 
//...
// Démarrer le thread
Monhread1 t1 = new MonThread1();
t1.start();  // ne pas appeler la méthode run() directement

Implémenter l'interface Runnable

public class MonThread2 implements Runnable {
  pubic void run() {
    //traitment du thread
  }
}
 
//...
// Démarrer le thread
 
Monhread2 t2 = new MonThread2();
Thread t = new Thread(t2);
t1.start();  // ne pas appeler la méthode run() directement

Arrêt d'un Thread

Même si une méthode stop() existe, il est préférable de ne pas l'utiliser puisqu'elle est dépréciée. La méthode stop() arrête brusquement le thread même en plein milieu. Alors, imaginons qu'il s'agit d'une transaction…

Pour bien se faire, il faut retenir trois éléments :

  1. Interrompre le fil de traitement par interrupt()
  2. Exécuter le traitement dans la méthode run() tant qu'il n'y a pas d'interruption
  3. Prévoir une exception
  t1.interrupt();
public void run() {
  while (!interrupted()) {
    //traitement
  }
  try {
    Thread.sleep(10);
  }
  catch (InterruptedException) {
    break;
  }
}

Suspension

Pseudo-suspension

La pseudo-suspension est l'idée de bloquer l'exécution d'un thread avec une condition qui l'empêche de rouler. Par exemple, dans la méthode run(), une condition qui dit :

if (!dormir) { // exécution des instructions }

Vraie suspension

La vraie suspension est de mettre le thread en mode bloqué en utilisant sleep().

Synchronication et communication

Méthode synchronisée

public class MaClasse {
 
  public synchronized ms(...) {
    //...
  }
}
main (...) {
 
  MaClasse x = new MaClasse();
 
  Thread t1 = ...
  Thread t2 = ...
 
  x.ms();

Un thread ne peut pas exécuter un méthode qui est en court d'exécution par un autre thread. Le premier thread à exécuter une méthode synchronisée, il la verrouille jusqu'à temps que la méthode soit terminée. Un autre thread (t2) va attendre (se bloquer) que la méthode se libère pour l'exécuter.

Bloc synchronisé

synchronized (objet nonnull) {
 
 
}

Interblocage

Dès qu'un thread t1 bloque une méthode x, plus tard un thread t2 bloque la méthode y, le thread t2 se bloque aussi, et si le thread t1 demande la méthode y alors qu'il est bloqué, t1 attend que t2 se débloque et t2 attend que t1 se débloque.

Il n'y a pas de solution à ce genre de problème. On peut les éviter en minimisant les méthodes synchronisées, les rendre courtes.

Le modèle producteur-consommateur

Dans le modèle producteur-consommateur, les deux classes restent tel quel, mais les deux méthodes ajouter() et enlever() de la classe File ont une condition :

while (estPleine())
  wait;  

qui met le thread en attente dès que la file est pleine. Même principe du côté Consommateur qui va vérifier plutôt si la file est vide. Une fois cela fait, l'ajout ou l'enlèvement se fait, et ensuite un notify() est appelé.

Les méthodes ajouter(), enlever(), estPleine() et estVide() sont synchronisées.

1)
L'API offre des dizaines de classes d'exceptions.
developpement/java/avance.txt · Dernière modification : 2022/02/02 00:42 de 127.0.0.1