Exercices sur les exceptions et la récursivité

Exercices sur les exceptions et la récursivité #

Questions/Réponses #

Veuillez répondre mentalement, sur papier ou bien en créant le code nécessaire pour répondre à ces questions avant de regarder la réponse.

Quand on vous demande de produire du code, vous devez le tester. C'est une erreur commune chez les étudiants: ils produisent rapidement du code en supposant qu'il est fonctionnel. Prenez le temps de vous relire, d'être attentif. Et testez votre code. Encore et encore.

Prenez note qu'il est permis d'utiliser le robot conversationnel du cours lors des exercises. Cependant vous devriez vous entraîner à produire vos propres réponses.

Réponses uniques? #

Les exercices comportent une solution vous permettant de comparer votre approche avec la nôtre. Il n'y a pas de solution unique aux problèmes en général. Vous pouvez arriver avec une solution qui est préférable ou moins bonne que celle que nous offrons. Pour faire ces questions, vous devez avoir fait toutes les lectures préalables. Vous disposez alors toujours des fondements nécessaires pour faire les exercices. Nous vous encourageons tout de même à faire vos propres recherches en complément de vos lectures. Dans certains cas, au sein de la solution que nous offrons, nous pouvons utiliser des notions techniques qui n'ont pas été vues directement dans le cours, mais qui devraient vous être facilement accessibles.

Question 1 #

Expliquez ce qu’est la récursivité en Java. Donnez un exemple simple de fonction récursive qui calcule la factorielle d’un nombre.

Réponse

La récursivité est une technique où une fonction s'appelle elle-même pour résoudre un problème en le divisant en sous-problèmes plus simples. Exemple :

int factorielle(int n) {
    if (n <= 1) return 1;
    else return n * factorielle(n - 1);
}

Question 2 #

Qu’est-ce qu’une exception en Java ? Donnez un exemple de code qui attrape une exception lors d’une division par zéro.

Réponse

Une exception est un mécanisme qui permet de gérer les erreurs ou situations inattendues lors de l'exécution d'un programme. Exemple :

try {
    int x = 10 / 0;
} catch (ArithmeticException e) {
    System.out.println("Division par zéro !");
}

Question 3 #

Que se passera-t-il si vous placez l’instruction return dans le bloc « try » ou « catch » ? Le bloc « finally » s’exécutera-t-il ?

Réponse

Oui, le bloc finally s'exécutera toujours, même si un return est exécuté dans le bloc try ou catch. Exemple :

public int exemple() {
    try {
        return 1;
    } catch (Exception e) {
        return 2;
    } finally {
        System.out.println("Bloc finally exécuté");
    }
}

Le message du bloc finally sera affiché avant que la méthode ne retourne sa valeur.

Question 4 #

Écrivez une fonction récursive qui calcule la somme des éléments d’un tableau d’entiers.

Réponse
int somme(int[] t, int n) {
    if (n == 0) return 0;
    else return t[n-1] + somme(t, n-1);
}
// Appel : somme(t, t.length)

Question 5 #

Que se passe-t-il si une fonction récursive n’a pas de cas d’arrêt (condition d’arrêt) ?

Réponse

La fonction s'appellera elle-même indéfiniment, ce qui provoquera une erreur de type StackOverflowError (dépassement de pile) en Java.

Question 6 #

Quelles sont les différences entre les exceptions vérifiées (checked) et non vérifiées (unchecked) en Java ?

Réponse

Les exceptions vérifiées (checked) doivent être déclarées dans la signature de la méthode ou capturées avec un bloc try/catch. Elles héritent de Exception (sauf RuntimeException). Les exceptions non vérifiées (unchecked) héritent de RuntimeException et ne nécessitent pas d’être déclarées ni capturées explicitement.

Question 7 #

Que se passe-t-il si une exception n’est pas capturée dans un bloc try/catch ?

Réponse

Si une exception n’est pas capturée, elle remonte la pile d’appels jusqu’à ce qu’un bloc catch la capture. Si aucune méthode ne la capture, le programme se termine avec un message d’erreur (stack trace).

Question 8 #

Comment créer sa propre exception personnalisée en Java ?

Réponse

On crée une classe qui hérite de Exception (pour une exception vérifiée) ou de RuntimeException (pour une non vérifiée). Exemple :

class MonException extends Exception {
    public MonException(String message) {
        super(message);
    }
}

Question 9 #

À quoi sert le mot-clé throw en Java ? Donnez un exemple d’utilisation.

Réponse

Le mot-clé throw permet de lancer explicitement une exception. Exemple :

if (x < 0) {
    throw new IllegalArgumentException("x doit être positif");
}

Question 10 #

Que se passe-t-il si on place plusieurs blocs catch à la suite d’un try ? Dans quel ordre sont-ils évalués ?

Réponse

Les blocs catch sont évalués dans l’ordre d’apparition. Le premier bloc dont le type correspond à l’exception levée sera exécuté ; les autres seront ignorés. Il faut placer les exceptions les plus spécifiques avant les plus générales.

Question 11 #

Qu’est-ce que le ramasse-miettes (garbage collector) en Java ? Quels sont ses avantages et ses inconvénients par rapport à la gestion manuelle de la mémoire ?

Réponse

Le ramasse-miettes (garbage collector) est un mécanisme automatique de la machine virtuelle Java (JVM) qui libère la mémoire occupée par les objets qui ne sont plus utilisés (inaccessibles). Il permet d’éviter les fuites de mémoire et les erreurs de libération (comme les doubles libérations en C), ce qui rend la gestion de la mémoire plus sûre et plus simple pour le programmeur.

Avantages : simplifie la programmation, réduit les risques d’erreurs, améliore la robustesse et la sécurité des programmes.

Inconvénients : le développeur a moins de contrôle sur le moment précis où la mémoire est libérée, et le ramasse-miettes peut provoquer des pauses imprévisibles dans l’exécution du programme, ce qui peut être gênant pour les applications temps réel ou très sensibles aux performances.

Question 12 #

Comment peut-on limiter le surcoût du ramasse-miettes (garbage collector) dans une application Java ? Donnez quelques bonnes pratiques pour réduire son impact sur les performances.

Réponse

Pour limiter le surcoût du ramasse-miettes, il est conseillé de :

  • Réduire la création d’objets temporaires inutiles (par exemple, réutiliser les objets ou utiliser des types primitifs quand c’est possible).
  • Privilégier les structures de données adaptées à l’usage (par exemple, préférer StringBuilder à la concaténation répétée de String).
  • Limiter la taille des objets et des collections pour éviter une consommation excessive de mémoire.
  • Pour les applications critiques, ajuster les paramètres de la JVM (options de tuning du garbage collector) selon le profil d’utilisation.

En appliquant ces bonnes pratiques, on réduit la pression sur le ramasse-miettes et on améliore la réactivité et la performance globale de l’application.

Question 13 #

Qu’est-ce que l’encodage UTF-16 et pourquoi Java l’utilise-t-il pour représenter les chaînes de caractères (String) ?

Réponse

L’UTF-16 est un encodage qui permet de représenter tous les caractères Unicode à l’aide de séquences de 16 bits (un ou deux char). Java utilise l’UTF-16 pour garantir la compatibilité avec l’ensemble des caractères internationaux, y compris les symboles, emojis et alphabets non latins. Cela permet de manipuler du texte multilingue de façon uniforme, mais implique que certains caractères occupent deux char au lieu d’un seul.

Question 14 #

Expliquez pourquoi la méthode charAt(int index) sur une String Java ne retourne pas toujours un caractère complet pour l’utilisateur. Donnez un exemple.

Réponse

En UTF-16, certains caractères Unicode (comme les emojis ou des symboles rares) sont codés sur deux char (une paire de substitution). La méthode charAt retourne un seul char à l’index donné, qui peut ne représenter qu’une partie d’un caractère complet. Par exemple :

String s = "A😊B";
System.out.println(s.charAt(1)); // Retourne un char de la paire, pas le smiley complet

Question 15 #

Comment peut-on parcourir correctement tous les caractères Unicode d’une String en Java, même ceux codés sur deux char ?

Réponse

Pour parcourir tous les caractères Unicode (code points) d’une chaîne, il faut utiliser les méthodes codePoints() ou codePointAt() de la classe String, ou la classe Character. Par exemple :

String s = "A😊B";
s.codePoints().forEach(cp -> System.out.println(Character.toChars(cp)));

Cette approche permet de traiter chaque caractère Unicode comme une entité logique, même s’il est codé sur deux char.

Question 16 #

Combien de mémoire une String Java utilise-t-elle par caractère ? Cette valeur est-elle toujours la même pour tous les caractères ?

Réponse

En Java, chaque élément du tableau interne d’une String occupe 2 octets (16 bits), car il s’agit d’un char en UTF-16. Pour la plupart des caractères courants (latin, accentués, etc.), un caractère occupe donc 2 octets. Cependant, certains caractères Unicode (comme les emojis ou des symboles rares) nécessitent deux char (soit 4 octets) pour être représentés, car ils sont codés sur une paire de substitution (surrogate pair). Ainsi, la mémoire utilisée par caractère visible peut varier selon le caractère.

Question 17 #

Écrivez un programme Java qui prend une chaîne de caractères en entrée et affiche la valeur numérique (code Unicode) de chaque char de la chaîne.

Réponse
import java.util.Scanner;

public class AfficheCodes {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.print("Entrez une chaîne : ");
        String s = sc.nextLine();
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            System.out.println("Caractère : '" + c + "' | Code Unicode : " + (int) c);
        }
    }
}

Ce programme lit une chaîne au clavier et affiche, pour chaque char, sa valeur numérique (code Unicode en décimal).

Question 18 #

Qu’est-ce qu’un stream en Java ? À quoi sert-il ?

Réponse

Un stream est une séquence d’éléments sur laquelle on peut effectuer des opérations de traitement en chaîne (filter, map, etc.). Il permet de manipuler des collections de façon déclarative et fonctionnelle, souvent en une seule ligne de code.

Question 19 #

Expliquez le rôle de la méthode filter dans un stream Java. Donnez un exemple.

Réponse

filter permet de ne conserver que les éléments qui satisfont une condition (prédicat). Exemple :

List<Integer> l = List.of(1, 2, 3, 4);
l.stream().filter(x -> x % 2 == 0).forEach(System.out::println); // Affiche 2, 4

Question 20 #

À quoi sert la méthode map dans un stream ? Donnez un exemple.

Réponse

map applique une fonction à chaque élément du stream et retourne un nouveau stream avec les résultats. Exemple :

List<String> noms = List.of("alice", "bob");
noms.stream().map(String::toUpperCase).forEach(System.out::println); // ALICE, BOB

Question 21 #

Expliquez l’utilité de la méthode limit dans un stream Java.

Réponse

limit permet de ne traiter qu’un nombre maximum d’éléments du stream. Exemple :

Stream.iterate(0, n -> n + 1).limit(5).forEach(System.out::println); // 0 1 2 3 4

Question 22 #

Que fait la méthode distinct sur un stream ? Donnez un exemple.

Réponse

distinct supprime les doublons du stream (en utilisant equals). Exemple :

List<Integer> l = List.of(1, 2, 2, 3);
l.stream().distinct().forEach(System.out::println); // 1 2 3

Question 23 #

Quel est le rôle de la méthode sorted dans un stream Java ?

Réponse

sorted trie les éléments du stream selon l’ordre naturel ou un comparateur fourni. Exemple :

List<String> noms = List.of("bob", "alice");
noms.stream().sorted().forEach(System.out::println); // alice, bob

Question 24 #

À quoi sert la méthode collect dans un stream ? Donnez un exemple d’utilisation avec Collectors.toList().

Réponse

collect permet de rassembler les éléments du stream dans une collection ou une autre structure de données. Exemple :

List<Integer> pairs = List.of(1, 2, 3, 4).stream()
    .filter(x -> x % 2 == 0)
    .collect(Collectors.toList());
System.out.println(pairs); // [2, 4]

Question 25 #

Expliquez la différence entre un stream et une collection en Java.

Réponse

Une collection (List, Set, etc.) stocke des données en mémoire et permet d’y accéder plusieurs fois. Un stream est une vue temporaire sur ces données, qui permet de les traiter de façon déclarative : il ne stocke pas les données et ne peut être consommé qu’une seule fois.

Question 26 #

Donnez un exemple d’utilisation de stream() sur une liste de chaînes pour obtenir la liste des longueurs distinctes, triées, de ces chaînes.

Réponse
List<String> mots = List.of("java", "code", "stream", "java");
List<Integer> longueurs = mots.stream()
    .map(String::length)
    .distinct()
    .sorted()
    .collect(Collectors.toList());
System.out.println(longueurs); // [4, 5, 6]