Skip to content

Algorithmique et programmation pour l'ingénieur

De la Procédure à l'Objet : Restitution du TD machine 1


Contexte et Systèmes Embarqués

Origines du langage

  • 1991 (Projet "Oak") : Initialement conçu par James Gosling (Sun Microsystems) pour la télévision interactive et les décodeurs. Les contraintes étaient la fiabilité et la portabilité matérielle.
  • 1995 (Lancement) : Le langage est réorienté vers le développement applicatif et web, porté par la promesse d'une exécution agnostique au système d'exploitation.
  • 2010 : Rachat de Sun Microsystems par Oracle, qui assure depuis le développement et la maintenance de la plateforme.

Déploiement dans l'industrie

Java constitue aujourd'hui l'un des standards majeurs de l'industrie logicielle.

  • Plus de 45 milliards de Machines Virtuelles Java (JVM) actives recensées.1
  • Largement majoritaire dans les systèmes d'information critiques (secteur bancaire, assurances, aérospatial).
  • Présence transversale : du back-end distribué (Hadoop, Kafka) jusqu'aux systèmes embarqués à fortes contraintes.

Pertinence pour l'informatique embarquée

Malgré sa réputation de langage de haut niveau, Java conserve des caractéristiques adaptées au matériel :

Avantages architecturaux

  • Abstraction matérielle : Le code compilé s'exécute de manière identique sur différentes architectures de processeurs.
  • Sécurité d'exécution : L'absence de manipulation directe des adresses mémoire prévient les dépassements de tampon (buffer overflows) fréquents en C.

Le profil Java Card

L'une des implémentations les plus répandues au monde concerne les environnements ultra-contraints.

  • Principe : Un sous-ensemble de Java optimisé pour les microcontrôleurs disposant de quelques kilo-octets de mémoire (RAM et ROM).
  • Applications : Cartes bancaires (standard EMV), cartes SIM, passeports biométriques.
  • Volume : Environ 6 milliards de composants Java Card sont produits chaque année.2

Accélération matérielle

L'interprétation logicielle du code Java consomme des ressources CPU. L'industrie matérielle a développé des solutions spécifiques :

  • Processeurs natifs (picoJava) : Architectures matérielles où le jeu d'instructions du processeur correspond directement au code binaire Java.
  • Coprocesseurs (ARM Jazelle) : Intégration matérielle permettant d'exécuter une majorité d'instructions Java directement sur le processeur, réduisant l'empreinte mémoire et la consommation énergétique.

Modèle de Compilation

Rappel : La compilation native (C/C++)

En C, la chaîne de compilation est liée au processeur cible.

  • Le code source (main.c) est traité par le compilateur.
  • Le résultat est un fichier contenant du code machine natif (instructions binaires spécifiques à l'architecture, par exemple x86 ou ARM).

Contrainte technique

Un binaire compilé pour une architecture ARM sous Linux ne peut pas être exécuté sur un processeur x86 sous Windows. Une recompilation des sources est systématiquement requise.

Le modèle d'exécution Java (WORA)

Le paradigme "Write Once, Run Anywhere" s'appuie sur une architecture en deux phases :

  1. Compilation statique (javac) : Le fichier .java est traduit en un format intermédiaire indépendant : le Bytecode (.class).
  2. Interprétation dynamique (JVM) : La Machine Virtuelle Java lit ce Bytecode lors de l'exécution et le traduit en instructions natives adaptées à l'hôte.

Bénéfice

Le fichier .class est portable. La gestion des spécificités matérielles est déléguée à la JVM locale.


Analyse Syntaxique Comparée

Point de départ : Le programme minimal

#include <stdio.h>

int main(int argc, char* argv[]) {
    printf("Initialisation système...\n");
    return 0;
}
  • Appel au préprocesseur pour les bibliothèques d'entrée/sortie.
  • Définition globale de la fonction main.
  • Paramètres gérés sous forme de pointeurs de caractères (char*).
public class ProgrammeCentral {

    public static void main(String[] args) {
        System.out.println("Initialisation système...");
    }

}

Cette syntaxe révèle des différences paradigmatiques profondes.

Différence 1 : Encapsulation obligatoire

Les fonctions et les variables globales peuvent être définies de manière autonome dans n'importe quel fichier source.

Tout élément doit structurellement appartenir à une Classe. L'existence de méthodes isolées est proscrite par le compilateur. C'est pourquoi la méthode main est incluse dans le bloc class.

Différence 2 : Signature du point d'entrée

public static void main(String[] args)
  • public : Visibilité nécessaire pour que la JVM extérieure puisse l'invoquer.
  • static : Indique que la méthode dépend de la classe entière, et ne nécessite pas l'instanciation préalable d'un objet.
  • void : La remontée de code d'erreur vers l'OS est gérée différemment (via System.exit()).
  • String[] args : Le langage propose un véritable objet tableau contenant des objets chaînes de caractères.

Différence 3 : Gestion des types de base

Tableaux

En C, un tableau passé en argument se dégrade en simple pointeur. Sa dimension est perdue. En Java, le tableau est une entité complexe qui maintient sa propre taille de manière sécurisée (tableau.length).

Chaînes de caractères

La gestion manuelle de la mémoire et du caractère de fin de chaîne ('\0') disparaît au profit du type objet String, immuable et nativement outillé pour la concaténation (opérateur +).


Concepts Fondamentaux de la POO

Transition conceptuelle : Struct vs Classe

Les structures de données sont indépendantes des fonctions de traitement :

struct Capteur { double valeur; };
void calibrer(struct Capteur* c, double offset) { 
    c->valeur += offset; 
}

La Classe unit conceptuellement l'état (attributs) et le comportement (méthodes) :

public class Capteur {
    double valeur;
    void calibrer(double offset) { valeur += offset; }
}

Instanciation : new remplace l'allocation

La création d'une occurrence physique de la classe en mémoire est l'instanciation.

struct Capteur* c = malloc(sizeof(struct Capteur));
c->valeur = 0.0;
Capteur c = new Capteur();
c.valeur = 0.0;

Manipulation par Références

La notion de pointeur brut est absente en Java afin de prévenir les erreurs de segmentation et les accès illicites.

  • Les objets sont manipulés via des Références.
  • La référence est un identifiant sécurisé géré par la machine virtuelle.
  • L'accès aux membres de l'objet se fait uniformément via l'opérateur de résolution (le point .). L'opérateur flèche (->) n'existe pas.
  • Une référence non initialisée a pour valeur null.

Principe d'Encapsulation

L'accès direct aux attributs pose un problème de robustesse (données altérables sans contrôle).

Capteur c = new Capteur();
c.valeur = -999.0; // État potentiellement aberrant

L'encapsulation consiste à restreindre la visibilité des données internes et à imposer l'utilisation d'interfaces programmatiques (méthodes) pour toute modification.

Mise en œuvre de l'Encapsulation

  1. Verrouillage de l'attribut avec private.
  2. Implémentation d'accesseurs (Getters) et de mutateurs (Setters) intégrant une logique de validation.
public class Capteur {
    private double valeur; 

    public double getValeur() { return valeur; }

    public void setValeur(double v) {
        if(v >= 0) {
            valeur = v; // La modification est filtrée
        }
    }
}

Portée des méthodes : Instance et Classe

  • Méthode d'instance : Nécessite la création préalable d'un objet. Elle manipule l'état interne spécifique de cet objet. Exemple : capteur1.setValeur(10);
  • Méthode de classe (static) : Rattachée à la définition de la classe, indépendante de toute instance. Utilisée pour des routines utilitaires ou globales. Exemple : Math.abs(-5);

Constructeurs et Initialisation

Le besoin d'initialisation

Dans les exemples précédents, l'objet est créé vide, puis configuré via des appels successifs.

Capteur c = new Capteur();
c.setReference("TEMP_01"); // Configuration a posteriori

Pour forcer un objet à posséder un état cohérent dès le moment de son allocation en mémoire, Java introduit la notion de Constructeur.

Définition du Constructeur

Le constructeur est un sous-programme spécifique appelé automatiquement lors de l'instruction new.

  • Il porte exactement le nom de la classe.
  • Il ne possède aucun type de retour (pas même void).
public class Capteur {
    private String reference;

    // Le Constructeur
    public Capteur(String refInitiale) {
        reference = refInitiale;
    }
}
// Instanciation directe : Capteur c = new Capteur("TEMP_01");

Résolution de portée : Le mot-clé this

Il est courant et recommandé de nommer les paramètres d'un constructeur de la même manière que les attributs visés. Pour lever l'ambiguïté lexicale (le paramètre "masquant" l'attribut), on utilise le mot-clé this, qui désigne la référence de l'objet en cours de manipulation.

public class Capteur {
    private String reference; 

    public Capteur(String reference) { 
        // "this.reference" cible l'attribut de l'objet
        // "reference" cible l'argument reçu
        this.reference = reference; 
    }
}

Modèle Mémoire et Cycle de Vie

Espace d'exécution : La Pile (Stack)

L'organisation de la mémoire repose sur une dichotomie stricte.

Fonctionnement de la Pile

  • Architecture LIFO gérant les contextes d'appel de méthodes.
  • Stocke les variables locales de type primitif (int, double).
  • Stocke les références contenant les adresses des objets.
  • Libération immédiate à la sortie du bloc englobant.

Espace d'exécution : Le Tas (Heap)

Fonctionnement du Tas

  • Zone de mémoire allouée dynamiquement pour les structures complexes.
  • Espace exclusif de résidence pour tous les objets créés via l'opérateur new.
  • Persistance décorrélée de la portée d'exécution des méthodes d'origine.

Comportement lors de la copie

La distinction entre référence et objet implique une vigilance particulière lors de l'affectation.

Capteur a = new Capteur("A");
Capteur b = a; // Affectation de la référence

b.setReference("B"); 
System.out.println(a.getReference()); // Affiche "B"

Les variables a et b situées dans la pile pointent vers la même adresse mémoire dans le tas. L'objet physique n'est pas dupliqué.

Gestion mémoire : C vs Java

La gestion manuelle de la mémoire en C/C++ expose à plusieurs risques architecturaux :

  • Fuite mémoire : Omission de l'appel à free() entraînant un épuisement des ressources allouables.
  • Pointeurs fantômes (Dangling Pointers) : Utilisation d'une adresse mémoire pointant vers une zone préalablement libérée.
  • Double libération : Entraînant une corruption de l'allocateur système.

Automatisation : Le Garbage Collector

L'environnement Java supprime la libération explicite de mémoire (free et delete n'existent pas).

  • Un processus système, le Garbage Collector (ramasse-miettes), analyse périodiquement le graphe des références.
  • Tout objet résidant dans le tas n'étant plus pointé par aucune référence active est identifié comme obsolète.
  • La zone mémoire correspondante est automatiquement récupérée et compactée sans intervention du développeur.

L'Objet String et l'Immuabilité

La particularité de l'Objet String

Contrairement au C (char*), Java propose la classe String. Sa caractéristique majeure est son immuabilité.

Qu'est-ce que l'immuabilité ?

Une fois qu'un objet String est créé dans le Tas (Heap), son contenu ne peut plus jamais être modifié.

  • Sécurité : Une chaîne passée à une méthode critique (ex: ouverture de fichier) ne peut pas être altérée à votre insu.
  • Optimisation : Java utilise un "String Pool" pour réutiliser les chaînes identiques en mémoire.

Le piège de la concaténation (l'opérateur +)

Puisqu'une String est immuable, que se passe-t-il lorsque l'on utilise l'opérateur + pour la modifier ?

String texte = "Bonjour";
texte = texte + " le monde"; // Que se passe-t-il en mémoire ?

Impact mémoire

Java ne modifie pas l'objet "Bonjour". Il crée un nouvel objet contenant "Bonjour le monde", puis redirige la référence texte vers lui. L'ancien objet devient obsolète et sera détruit par le Garbage Collector.

Dans une boucle (ex: 1000 itérations), cela crée 1000 objets inutiles, inondant le Tas et forçant le Garbage Collector à travailler intensément !

La solution de performance : StringBuilder

Pour manipuler intensivement du texte (boucles, assemblages complexes), Java fournit une classe dédiée et muable (modifiable) : StringBuilder.

// 1. Création d'un constructeur de chaîne (1 seul objet)
StringBuilder constructeur = new StringBuilder("Bonjour");

// 2. Modification de l'objet existant (Pas de nouvel objet !)
for(int i = 0; i < 3; i++) {
    constructeur.append(" !");
}

// 3. Conversion finale en String immuable
String resultat = constructeur.toString();

Règle d'or

Utilisez String et + pour des assemblages simples (1 ou 2 variables). Utilisez StringBuilder dès qu'il y a une boucle.

L'optimisation silencieuse du compilateur

Les compilateurs Java modernes (javac) sont intelligents. Ils optimisent automatiquement les concaténations simples en utilisant StringBuilder (ou StringConcatFactory depuis Java 9).

Concaténation sur une ligne (Optimisée)

String s = "Capteur " + id + " : " + valeur;
// Le compilateur le transforme de lui-même en StringBuilder !

Le cas critique (Corner case) : La boucle

String s = "";
for(int i = 0; i < 1000; i++) { s += i; }
Ici, le compilateur crée un nouveau StringBuilder à chaque itération de la boucle. L'optimisation globale est inopérante, et le Tas (Heap) est tout de même inondé d'objets temporaires.


Conclusion et Perspectives

Bilan de la transition

  1. Architecture : Découplage de la compilation matérielle via le Bytecode et la JVM, assurant portabilité et sécurité.
  2. Syntaxe consolidée : Disparition de l'arithmétique de pointeurs au profit d'un système de références strict.
  3. Encapsulation : Architecture de défense des données internes.
  4. Gestion du cycle de vie : Création explicite par new et constructeurs, libération implicite par le Garbage Collector.

Programme de la prochaine séance

La programmation Objet offre des mécanismes avancés pour factoriser le code et modéliser des systèmes complexes. Concepts au programme :

  • L'Héritage : Créer de nouvelles classes basées sur des modèles existants (Ex: Dériver un CapteurThermique d'un Capteur).
  • Le Polymorphisme : Traiter des objets de natures différentes au travers d'un prisme commun.
  • Classes Abstraites et Interfaces : Définir des contrats logiciels stricts entre les différentes briques de votre application.

Page rédigée à l'aide de Gemini (pas par Gemini)


  1. Source : Oracle, données officielles partagées lors des 25 ans de Java (2020). 

  2. Source : Java Card Forum & Oracle, volume annuel estimé de puces déployées.