Programmation orientée objet, collections et I/O

Se connecter pour s'inscrire

Votre progression

Continuez pour débloquer la suite !
0 / 0 étapes complétées 0
%

Syllabus

  1. Description

  2. Objectifs

  3. Validations de compétences

  4. Public cible

    Aucun public cible spécifié pour ce module.

  5. Besoins techniques

  6. Prérequis

    • Aucun prérequis
  7. Niveau

  8. Historique du module

    Aucune action enregistrée
  • Programmation orientée objet, collections et I/O

    1. Concepts fondamentaux de la POO

      La programmation orientée objet (POO) repose sur une idée simple : on regroupe des données et les traitements qui les concernent dans une même entité, l'objet. Java est un langage à classes : tout y est un objet, à l'exception des types primitifs.

      Classe, objet, référence

      La classe est le moule (le plan). L'objet est une instance concrète créée à partir de ce plan. On parle de référence pour la variable qui pointe vers l'objet.

      class Voiture {
          private String marque;
          private int vitesse;
      
          public void accelerer(int kmh) {
              this.vitesse += kmh;
          }
      }
      
      Voiture maVoiture = new Voiture();  // création de l'objet
      maVoiture.accelerer(50);             // appel de méthode
      // IMPORTANT : la copie de référence, pas de l'objet
      Voiture copie = maVoiture;           // copie ET ALIAS sur le même objet
      copie.accelerer(50);
      System.out.println(maVoiture.vitesse); // 100 : les deux noms voient le même objet

      L'erreur la plus fréquente des débutants est de croire que copie a créé un second objet. En Java, une variable de type objet contient une référence : affecter une référence en recopie simplement le pointeur. Pour un véritable duplicata, il faut new ou une méthode de copie.

      Les 4 piliers

      1. Abstraction : on ne retient que ce qui est utile (l'interface d'un objet), pas son fonctionnement interne.
      2. Encapsulation : les données sont privées, on y accède par des méthodes qui contrôlent ce qui entre et sort.
      3. Héritage : une classe spécialisée réutilise et spécialise une classe existante.
      4. Polymorphisme : un même appel se comporte différemment selon le type réel de l'objet.

      Encapsulation et bonnes pratiques

      L'encapsulation consiste à rendre les attributs private et à contrôler l'accès par des méthodes. C'est ce qui garantit qu'un objet reste toujours dans un état valide.

      class CompteBancaire {
          // invariant : le solde ne peut pas être négatif
          private double solde;
      
          public CompteBancaire(double soldeInitial) {
              this.solde = soldeInitial;
          }
      
          public boolean deposer(double montant) {
              if (montant <= 0) {
                  return false;   // refus : l'état reste valide
              }
              this.solde += montant;
              return true;
          }
      
          public double getSolde() { return this.solde; }
      }

      Notez que deposer refuse l'opération au lieu de créer un solde négatif : c'est le rôle de l'encapsulation. Un attribut exposé (public double solde) aurait laissé n'importe qui casser cet invariant.

      • this distingue l'attribut de l'instance du paramètre de même nom.
      • Un constructeur initialise, il ne travaille jamais (pas d'affichage, pas d'appel réseau).
      • Getter et setter ne sont pas obligatoires : un objet immuable (final sur les attributs) est souvent préférable.

      Héritage, interfaces et polymorphisme

      extends : réutiliser et spécialiser

      class Animal {
          protected String nom;
          public Animal(String nom) { this.nom = nom; }
          public String presenter() { return "Je suis " + nom; }
      }
      
      class Chien extends Animal {
          public Chien(String nom) { super(nom); }  // appel du constructeur parent
          @Override
          public String presenter() { return super.presenter() + " et j'aboie"; }
      }
      
      Animal a = new Chien("Rex");
      System.out.println(a.presenter());
      // « Je suis Rex et j'aboie » : le type réel (Chien) décide

      La dernière ligne est le polymorphisme : la variable est déclarée Animal mais l'objet est un Chien. C'est ce mécanisme qui rend les programmes extensibles sans modifier le code existant.

      implements : le contrat

      Une interface décrit ce qu'une classe sait faire, sans dire comment. Une classe peut en implémenter plusieurs, l'héritage de classes étant unique en Java.

      interface Deplacable {
          void avancer(int distance);
          default void reculer(int distance) { avancer(-distance); }  // méthode par défaut
      }
      
      class Vo implements Deplacable {
          private int position = 0;
          public void avancer(int distance) { this.position += distance; }
      }
      
      class Robot implements Deplacable {
          private int batterie = 100;
          public void avancer(int distance) { batterie -= Math.abs(distance); }
      }

      Avec cette interface, on peut écrire un code qui manipule les deux sans connaître leur type réel :

      static void deplacerTout(Deplacable[] elements) {
          for (Deplacable e : elements) {
              e.avancer(10);   // chaque objet fait ce qu'il sait faire
          }
      }
      deplacerTout(new Deplacable[]{ new Vo(), new Robot() });

      abstract : le contrat partiel

      Une classe abstraite peut imposer des méthodes sans les implémenter. On ne peut pas l'instancier : elle sert de base commune.

      abstract class Forme {
          public abstract double aire();          // chaque figure calcule la sienne
          public String decrire() {                // méthode commune
              return getClass().getSimpleName() + " : " + aire();
          }
      }
      
      class Cercle extends Forme {
          private final double rayon;
          public Cercle(double rayon) { this.rayon = rayon; }
          public double aire() { return Math.PI * rayon * rayon; }
      }
      
      class Carre extends Forme {
          private final double cote;
          public Carre(double cote) { this.cote = cote; }
          public double aire() { return cote * cote; }
      }

      À retenir : « préferez des interfaces à des classes parentes », principe KISS de Joshua Bloch. Une interface décrit un rôle ; l'héritage de classes décrit « est un ».

      Classes internes et lambdas

      Une classe déclarée dans une autre est une classe interne. Utile pour regrouper une classe utilitaire avec la seule classe qui s'en sert, ou pour créer un objet anonyme sans nommer sa classe.

      class Gestionnaire {
          private int compteur = 0;
      
          // classe interne non statique : accès aux membres de l'instance
          class Compteur {
              void incrementer() { compteur++; }   // accès direct au champ externe
          }
      }

      Depuis Java 8, la forme la plus courante est la classe anonyme, qui n'est qu'une implémentation inline d'une interface ou d'une classe abstraite — typiquement pour un listener :

      bouton.addActionListener(new ActionListener() {   // avant Java 8
          public void actionPerformed(ActionEvent e) {
              System.out.println("clic");
          }
      });
      
      bouton.addActionListener(e -> System.out.println("clic"));  // depuis Java 8

      La seconde forme est une lambda : elle implémente l'interface fonctionnelle ActionListener (une seule méthode abstraite) sans déclarer de classe. C'est le mécanisme utilisé par le module suivant, sur les Stream et la méthode forEach.

      Packages et organisation du code

      Un package regroupe des classes dans un espace de noms. La convention standard est le domaine inversé : com.entreprise.projet.

      // fichier src/com/entreprise/bank/Compte.java
      package com.entreprise.bank;
      
      import java.util.List;
      import java.time.LocalDate;      // import explicite
      
      public class Compte { ... }

      Points de vigilance :

      • le chemin du dossier doit correspondre exactement au package (com/entreprise/bank pour com.entreprise.bank) ;
      • une classe public doit être dans un fichier portant son nom ;
      • on importe les classes, jamais les packages entiers — un import java.util.* masque l'origine des symboles et rend le code illisible.

      Exercices pratiques

      1. Compte bancaire : écrivez une classe Compte avec un solde privé, un dépôt, un retrait qui refuse de passer sous zéro, et une méthode historique() renvoyant la liste des opérations.
      2. Formes géométriques : définissez une interface Deplacable et des classes Cercle, Carre, Rectangle. Écrivez une méthode qui déplace toutes les formes d'un coup, sans connaître leur type.
      3. Correctif : le code suivant perd-il des données ? Corrigez-le.
        class Panier {
            List<Article> articles = new ArrayList<>();
            void copierDans(Panier autre) { autre = this; }   // que fait cette ligne ?
        }
    2. Le Framework de Collections

      Le Collections Framework (java.util) fournit des structures de données prêtes à l'emploi. Avant de l'utiliser, on choisissait entre un tableau ([]) et un ArrayList ; le tableau reste adapté quand la taille est fixe et connue.

      Les trois grandes familles

      1. List — ArrayList (accès par indice, insertion en fin rapide) et LinkedList (insertion rapide en milieu). L'interface expose l'ordre et l'accès par indice.
      2. Set — HashSet sans doublon et sans ordre garanti, TreeSet trié selon l'ordre naturel des éléments.
      3. Map — HashMap associe une clé à une valeur. Une map n'est pas une collection : c'est une structure clé/valeur.
      List<String> villes = new ArrayList<>();
      villes.add("Lyon");
      villes.add("Paris");
      villes.add(1, "Marseille");   // insertion à l'indice 1
      System.out.println(villes.get(0));        // Lyon
      
      Set<String> emails = new HashSet<>();
      emails.add("a@test.fr");
      System.out.println(emails.add("a@test.fr"));  // false : déjà présent
      
      Map<String, Integer> stock = new HashMap<>();
      stock.put("clavier", 12);
      stock.put("souris", 40);
      stock.put("clavier", 8);      // écrase la valeur
      System.out.println(stock.get("clavier"));  // 8
      
      for (Map.Entry<String, Integer> e : stock.entrySet()) {
          System.out.println(e.getKey() + " : " + e.getValue());
      }

      Point d'attention : une HashMap ne garantit pas l'ordre d'itération. Si l'ordre compte, utilisez LinkedHashMap (insertion) ou TreeMap (trié).

      Génériques et sécurité de type

      Les génériques substituent le type à l'intérieur d'une classe ou d'une méthode, et le contrôle est fait à la compilation, pas à l'exécution.

      // AVANT les génériques : on stocke n'importe quoi, plantage à l'exécution
      List liste = new ArrayList();
      liste.add("texte");
      liste.add(42);
      String s = (String) liste.get(1);   // ClassCastException à l'exécution
      
      // AVEC les génériques : erreur détectée par le compilateur
      List<String> liste = new ArrayList<>();
      liste.add(42);        // erreur de compilation

      Le losange <> permet d'omettre le type à droite lorsque le compilateur peut le déduire — il ne faut jamais l'écrire deux fois, ce qui produirait deux types différents.

      On peut aussi paramétrer ses propres méthodes :

      public static <T> T premier(List<T> liste) {
          if (liste.isEmpty()) {
              throw new NoSuchElementException("liste vide");
          }
          return liste.get(0);
      }
      
      String nom = premier(List.of("Alice", "Bob"));

      Itération, foreach et Streams

      La boucle foreach est la façon naturelle de parcourir une collection :

      for (String ville : villes) {
          System.out.println(ville);
      }

      L'itérateur explicite reste utile quand on supprime pendant la boucle :

      Iterator<String> it = villes.iterator();
      while (it.hasNext()) {
          String v = it.next();
          if (v.startsWith("L")) {
              it.remove();   // suppression sûre : pas de ConcurrentModificationException
          }
      }

      Supprimer dans un for (String v : villes) provoque une ConcurrentModificationException : l'itérateur détecte que la collection a changé pendant le parcours.

      Les Streams (Java 8+)

      Un Stream traite une séquence de valeurs sans la modifier. Les opérations se chaînent : source → opérations intermédiaires → opération terminale.

      List<String> villes = List.of("lyon", "paris", "marseille", "lille");
      
      List<String> resultat = villes.stream()
          .filter(v -> v.length() >= 5)          // intermédiaire : garde Marseille
          .map(String::toUpperCase)                   // intermédiaire : transforme
          .sorted()                                   // intermédiaire : trie
          .toList();                                  // terminal : collecte
      
      // équivalent avec Collectors
      String villesLongues = villes.stream()
          .filter(v -> v.length() >= 5)
          .collect(Collectors.joining(", "));
      
      long combien = villes.stream()
          .filter(v -> v.startsWith("l"))
          .count();

      Un Stream est à usage unique : on ne peut pas le parcourir deux fois, et les opérations sont paresseuses — rien n'est calculé avant l'opération terminale (count(), forEach(), toList()…). Les streams parallèles (parallelStream()) ne sont utiles que sur de gros volumes et des traitements coûteux.

      Collections avancées et performance

      Choisir la bonne collection, c'est surtout choisir la bonne complexité :

      • ArrayList : accès par indice en O(1), insertion en fin O(1) amorti, insertion en milieu O(n).
      • LinkedList : insertion/suppression O(1) si la position est connue, mais accès par indice O(n) — à n'utiliser que si l'insertion en milieu est fréquente.
      • HashMap : recherche en O(1) en moyenne, sans ordre.
      • ArrayDeque : file d'attente et pile, plus efficace que Stack ou LinkedList pour cet usage.

      Enfin, Collections.unmodifiableList() renvoie une vue non modifiable : pratique pour protéger une collection exposée par une méthode.

      public List<String> getNoms() {
          return Collections.unmodifiableList(interne);  // l'appelant ne peut plus modifier
      }

      Exercices pratiques

      1. Annuaire : une Map<String, String> associe un nom à un numéro. Ajoutez ajouter, supprimer, chercher, et l'affichage trié par nom.
      2. Déduplication : à partir d'une liste de 200 mots, affichez les 10 plus fréquents, triés par occurrences décroissantes, sans utiliser de HashMap à l'implémentation manuelle.
      3. Refactoriser : réécrivez cette boucle avec un Stream.
        List<String> resultat = new ArrayList<>();
        for (String s : liste) {
            if (s != null && !s.isBlank() && s.length() > 3) {
                resultat.add(s.toUpperCase().trim());
            }
        }
    3. Les flux d'E/S (Input/Output)

      Toute entrée-sortie passe par un flux (Stream). Un flux est orienté : on lit d'un flux, on écrit dans un flux, mais pas les deux. L'ensemble des hiérarchies est simplifié par deux classes abstraites :

      • InputStream : flux d'octets en entrée (read()).
      • OutputStream : flux d'octets en sortie (write()).

      Pour du texte, on préfère les flux de caractères — ils gèrent l'encodage :

      // flux d'octets : pour une image, un ZIP, tout format binaire
      InputStream octets = Files.newInputStream(Path.of("image.png"));
      
      // flux de caractères : pour du texte
      Reader texte = Files.newBufferedReader(Path.of("data.txt"));

      Les flux doivent toujours être fermés, même en cas d'exception. C'est exactement ce que garantit le try-with-resources :

      // à éviter : si read() lève une exception, le flux reste ouvert
      FileReader fr = new FileReader("config.txt");
      try {
          // ...
      } finally {
          fr.close();
      }
      
      // forme correcte et obligatoire
      try (BufferedReader br = Files.newBufferedReader(Path.of("config.txt"))) {
          String ligne = br.readLine();
          while (ligne != null) {
              System.out.println(ligne);
              ligne = br.readLine();
          }
      }   // fermeture automatique, même en cas d'exception

      Lecture et écriture de fichiers

      Écrire un fichier

      try (BufferedWriter writer = Files.newBufferedWriter(Path.of("notes.txt"))) {
          writer.write("Première ligne");
          writer.newLine();
          writer.write("Deuxième ligne");
      }   // le fichier est créé ou écrasé
      
      // ajouter sans écraser : StandardOpenOption.APPEND
      Files.writeString(Path.of("notes.txt"), "Ajout\n", StandardOpenOption.APPEND);

      Lire un fichier

      // ligne par ligne — la méthode adaptée aux gros fichiers
      try (Stream<String> lignes = Files.lines(Path.of("notes.txt"))) {
          lignes.forEach(System.out::println);
      }
      
      // tout en mémoire — simple, à réserver aux petits fichiers
      String contenu = Files.readString(Path.of("notes.txt"));

      Différence importante : Files.readString() charge tout en mémoire. Sur un fichier de plusieurs gigaoctets, il faut lire ligne par ligne avec Files.lines(), qui reste en flux.

      Les exceptions contrôlées : à comprendre

      Une opération de fichier peut échouer : IOException. Le compilateur oblige donc à la traiter, soit par throws, soit par try/catch. C'est une garantie du langage, pas une nuisance.

      public void copier(Path source, Path cible) throws IOException {
          Files.copy(source, cible, StandardCopyOption.REPLACE_EXISTING);
      }
      
      public void copierSure(Path source, Path cible) {
          try {
              Files.copy(source, cible, StandardCopyOption.REPLACE_EXISTING);
          } catch (IOException e) {
              // on journalise et on ne bloque pas le programme
              System.err.println("Copie impossible : " + e.getMessage());
          }
      }

      Sérialisation d'objets

      La sérialisation transforme un objet en suite d'octets registrable, et l'inverse. Utile pour la persistance ou le transfert entre processus.

      class Employe implements Serializable {
          private final String nom;        // String et les types primitifs sont
          private final double salaire;     // toujours sérialisables
      
          Employe(String nom, double salaire) {
              this.nom = nom;
              this.salaire = salaire;
          }
      }
      
      try (ObjectOutputStream out =
               new ObjectOutputStream(Files.newOutputStream(Path.of("employe.bin")))) {
          out.writeObject(new Employe("Alice", 4200.0));
      }
      
      try (ObjectInputStream in =
               new ObjectInputStream(Files.newInputStream(Path.of("employe.bin")))) {
          Employe e = (Employe) in.readObject();
          System.out.println(e.getNom());
      }

      Précaution : un objet lu par désérialisation peut être consécutivement de type Serializable sans garantie de sécurité. Pour des données venues de l'extérieur, on privilégie JSON ou un format explicite plutôt que la sérialisation native.

      L'API NIO.2 et les chemins

      java.nio.file (NIO.2) est l'API moderne de gestion de fichiers : elle manipule des objets Path plutôt que des chaînes, et propose des opérations Enrichies (copier l'arborescence, parcourir un dossier…).

      Path chemin = Path.of("src/main/java");       // construction moderne
      Path absolu = chemin.toAbsolutePath();
      
      // lister un répertoire
      try (Stream<Path> fichiers = Files.list(chemin)) {
          fichiers.filter(p -> p.toString().endsWith(".java")).forEach(System.out::println);
      }
      
      // parcourir une arborescence en profondeur
      try (Stream<Path> arbre = Files.walk(chemin)) {
          long nbJava = arbre.filter(p -> p.toString().endsWith(".java")).count();
          System.out.println(nbJava + " fichiers Java");
      }
      
      // properties : format clé=valeur
      Properties props = new Properties();
      try (Reader r = Files.newBufferedReader(Path.of("config.properties"))) {
          props.load(r);
      }
      System.out.println(props.getProperty("db.url"));

      try-with-resources sur un Stream : un Stream est une ressource comme une autre. Sans le try, un parcours incomplet laisse des descripteurs de fichiers ouverts.

      Exercices pratiques

      1. Annuaire CSV : lisez un fichier contacts.csv (nom, email par ligne) et réécrivez-le trié par nom, en ignorant les lignes mal formées et les doublons.
      2. Compteur de mots : comptez les occurrences de chaque mot d'un fichier texte et écrivez le résultat dans un autre fichier, trié par fréquence décroissante.
      3. Trouver le bug : ce code fuit une ressource. Corrigez-le.
        BufferedReader br = new BufferedReader(new FileReader("data.txt"));
        String l = br.readLine();
        if (l == null) { return; }        // le flux n'est jamais fermé ici
        System.out.println(l);
        br.close();
    4. Introduction aux interfaces graphiques en Java

      Jusqu'ici les programmes affichaient dans la console. Une interface graphique repose sur un modèle simple et stable :

      1. Composants (widgets) : boutons, champs, listes, étiquettes.
      2. Conteneurs : panels et frames qui regroupent et positionnent les composants.
      3. Gestionnaires de mise en page (layout managers) : la position et la taille sont calculées, jamais fixées à la main.
      4. Événements : l'utilisateur agit, un listener exécute le code associé.

      Point essentiel : un composant est affiché uniquement après un appel à setVisible(true), et Swing n'est pas thread-safe — toute modification d'interface doit donc être faite depuis l'Event Dispatch Thread.

      Swing : composants de base et layout

      import javax.swing.*;
      import java.awt.*;
      
      public class Fenetre extends JFrame {
      
          private final JTextField champNom = new JTextField(20);
          private final JLabel resultat = new JLabel(" ");
      
          public Fenetre() {
              super("Mon application");
      
              JButton bouton = new JButton("Valider");
              bouton.addActionListener(e -> valider());   // listener : le clic
      
              JPanel panel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 10));
              panel.add(new JLabel("Nom : "));
              panel.add(champNom);
              panel.add(bouton);
              panel.add(resultat);
      
              setContentPane(panel);            // on remplace le contenu de la fenêtre
              setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
              pack();                          // taille ajustée au contenu
              setLocationRelativeTo(null);     // centrée à l'écran
              setVisible(true);
          }
      
          private void valider() {
              String nom = champNom.getText().trim();
              resultat.setText(nom.isEmpty() ? "Veuillez saisir un nom" : "Bonjour, " + nom + " !");
          }
      
          public static void main(String[] args) {
              javax.swing.SwingUtilities.invokeLater(Fenetre::new);
          }
      }

      Trois règles à retenir :

      • pack() plutôt que setSize(...) : la fenêtre s'ajuste à son contenu et suit la police système.
      • SwingUtilities.invokeLater(...) : Swing ne peut être manipulé que depuis l'EDT.
      • on n'écrit jamais la logique métier dans le listener ; on délègue à une méthode testable.

      Les layouts les plus courants

      • FlowLayout : composants côte à côte, passage à la ligne suivante quand la place manque (par défaut).
      • BorderLayout : cinq zones (NORTH, SOUTH, EAST, WEST, CENTER) — idéal pour un menu et un contenu.
      • GridBagLayout : grille avec alignement et étirement fins. Plus verbeux, mais c'est le seul qui permette des interfaces sérieuses.

      JavaFX : approche déclarative

      JavaFX inverse la logique : on décrit l'interface en XML (ou en Java), et la bibliothèque construit la scène. Le code devient plus lisible et l'interface séparée de la logique métier.

      package fr.academy.demo;
      
      import javafx.application.Application;
      import javafx.scene.Scene;
      import javafx.scene.control.*;
      import javafx.scene.layout.VBox;
      import javafx.stage.Stage;
      
      public class ApplicationDemo extends Application {
      
          private final TextField champ = new TextField();
          private final Label resultat = new Label("");
      
          @Override
          public void start(Stage stage) {
              Button bouton = new Button("Valider");
              bouton.setOnAction(e -> {
                  String nom = champ.getText().trim();
                  resultat.setText(nom.isEmpty() ? "Veuillez saisir un nom" : "Bonjour, " + nom + " !");
              });
      
              VBox racine = new VBox(10, new Label("Nom :"), champ, bouton, resultat);
              racine.setPadding(new Insets(20));
      
              stage.setTitle("JavaFX");
              stage.setScene(new Scene(racine, 320, 200));
              stage.show();
          }
      
          public static void main(String[] args) {
              launch(args);
          }
      }

      Quelle technologie choisir ? Swing est intégré au JDK, très répandu et fiable pour des interfaces de bureau classiques ; JavaFX est plus moderne et plus riche, mais demande un module supplémentaire. Pour une interface de bureau simple en formation, Swing reste le choix le plus simple à installer.

      Projet pratique

      Réalisez un convertisseur d'unités (euros ↔ dollars, °C ↔ °F) : un champ de saisie, une liste déroulante, un bouton « Convertir » et un résultat. Contraintes :

      1. la conversion doit être écrite dans une classe séparée Convertisseur, sans aucun code graphique, et être testable ;
      2. une saisie non numérique doit produire un message clair, pas une exception ;
      3. l'interface ne doit pas être redimensionnée de façon cassante.
  • Exercices, examens et projets

    Exercice

    Exercice Java

    Écrire et tester une classe Java.

    Obligatoire Consulter
    Examen

    Examen Java

    Évaluer POO, collections et exceptions.

    Obligatoire Consulter
    Projet

    Projet Java

    Construire une application Java maintenable.

    Livrables :

    • Code
    • Tests
    Obligatoire Consulter