Public cible
Débutants, étudiants et professionnels qui souhaitent acquérir une pratique structurée dans ce domaine.
Formation pratique sur Java : notions, méthodes, ateliers et livrables vérifiables.
Débutants, étudiants et professionnels qui souhaitent acquérir une pratique structurée dans ce domaine.
Comprendre les notions essentielles et les appliquer dans un projet vérifiable.
Aucune connaissance préalable n’est strictement obligatoire ; les prérequis détaillés sont précisés dans les ateliers.
Tous niveaux
La validation repose sur les évaluations actives et les livrables pratiques du cours.
Un ordinateur, un navigateur et les outils indiqués dans le premier atelier sont suffisants pour commencer.
Construire une application Java maintenable.
Livrables :
Construire une application Java maintenable.
Livrables :
Construire une application Java maintenable.
Livrables :
Construire une application Java maintenable.
Livrables :
Construire une application Java maintenable.
Livrables :
~120 heures
Présentiel ou en ligne (selon les besoins)
À l'issue de ce cours, les participants seront capables de :
Oak (nommé ainsi en référence à un chêne devant le bureau de James Gosling).
Renommé Java en 1995, en hommage au café de Java, symbolisant simplicité et puissance.
En 1995, le projet a été renommé "Java". Ce nouveau nom est un hommage au café de Java, une île indonésienne connue pour la production de café.
Le choix du nom "Java" était motivé par plusieurs raisons :
JAVA_HOME et le PATH pour une utilisation correcte des outils Java en ligne de commande.java --versionjavac --versionjavac.java.Un programme Java simple suit toujours une structure bien définie.
public class HelloWorld
{
public static void main(String[] args)
{
System.out.println("Bonjour, monde !");
}
}
public class HelloWorld
public static void main(String[] args)
public : Accessible depuis d'autres parties du code.static : Méthode associée à la classe et non à une instance spécifique.void : Ne renvoie aucune valeur.String[] args : Un tableau d'arguments passés en ligne de commande (souvent vide pour un programme simple).System.out.println("Bonjour, monde !") :Affiche du texte dans la console.Les importations permettent d'utiliser des classes prédéfinies situées dans d'autres packages (bibliothèques).
import java.util.Scanner; // Importe la classe Scanner
public class InputExample
{
public static void main(String[] args)
{
Scanner scanner = new Scanner(System.in); // Crée un Scanner
System.out.print("Entrez votre nom : ");
String name = scanner.nextLine();
System.out.println("Bonjour " + name + " !");
}
}
.java.javac.
javac HelloWorld.java
java HelloWorld
Une variable est une boîte qui stocke des données dans un programme. En Java, chaque variable a un type qui définit la nature des données qu'elle peut contenir.
Java propose des types de données prédéfinis :
int : pour les nombres entiers (ex. : 42).double : pour les nombres décimaux (ex. : 3.14).char : pour un caractère unique (ex. : 'A').boolean : pour les valeurs `true` ou `false`.
public class VariablesDemo
{
public static void main(String[] args)
{
// Déclaration et initialisation de variables
int age = 25;
double pi = 3.14159;
char grade = 'A';
boolean isJavaFun = true;
// Affichage des valeurs
System.out.println("Âge : " + age);
System.out.println("Valeur de Pi : " + pi);
System.out.println("Note : " + grade);
System.out.println("Java est amusant : " + isJavaFun);
}
}
System.out.printlnLa méthode System.out.println() est utilisée pour afficher des messages ou des valeurs dans la console.
Pour interagir avec l'utilisateur, on utilise la classe Scanner pour lire des données entrées via le clavier.
import java.util.Scanner;
public class InputOutputDemo
{
public static void main(String[] args)
{
Scanner scanner = new Scanner(System.in);
// Lecture d'un nom
System.out.print("Entrez votre nom : ");
String name = scanner.nextLine();
// Lecture d'un âge
System.out.print("Entrez votre âge : ");
int age = scanner.nextInt();
// Affichage des données
System.out.println("Bonjour " + name + ", vous avez " + age + " ans.");
}
}
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.
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.
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.final sur les attributs) est souvent préférable.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.
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() });
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 ».
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.
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 :
com/entreprise/bank pour com.entreprise.bank) ;public doit être dans un fichier portant son
nom ;import java.util.* masque l'origine des symboles et rend le
code illisible.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.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.class Panier {
List<Article> articles = new ArrayList<>();
void copierDans(Panier autre) { autre = this; } // que fait cette ligne ?
}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.
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.HashSet sans doublon et sans ordre garanti,
TreeSet trié selon l'ordre naturel des éléments.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é).
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"));
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.
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.
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
}
Map<String, String> associe un
nom à un numéro. Ajoutez ajouter, supprimer,
chercher, et l'affichage trié par nom.HashMap à l'implémentation manuelle.List<String> resultat = new ArrayList<>();
for (String s : liste) {
if (s != null && !s.isBlank() && s.length() > 3) {
resultat.add(s.toUpperCase().trim());
}
}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
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);
// 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.
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());
}
}
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.
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.
contacts.csv
(nom, email par ligne) et réécrivez-le trié par nom, en ignorant les lignes
mal formées et les doublons.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();Jusqu'ici les programmes affichaient dans la console. Une interface graphique repose sur un modèle simple et stable :
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.
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.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 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.
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 :
Convertisseur, sans aucun code graphique, et être testable ;Callable et Future
Runnable et Callable
Callable pour récupérer des résultats des threads
start(), run(), sleep(), join(), interrupt()
synchronized
Publié le 25/09/2026