Environnement de développement

Dans ce module, nous utiliserons le langage Java et vous devrez donc installer un environnement de développement adéquat.

En ce qui concerne l’IDE, vous pouvez utiliser celui que vous souhaitez. Chez les étudiants, les plus populaires sont sans doute IntelliJ et VScode (précisons qu’IntelliJ est en général payant mais, en tant qu’étudiant(e), vous pouvez très simplement en obtenir une licence gratuite).

En termes de compilateur/librairies, si votre IDE ne l’a pas déjà fait, vous aurez besoin d’installer la dernière version du Java Development Kit (JDK).

Exercice 1 : Installation de l'environnement de développement.   

Installez l’IDE que vous comptez utiliser. Intallez également JDK. Si vous êtes sous Linux, le package s’appelle sans doute openjdk, jdk, jdk-openjdk ou quelque chose d’approchant. Sinon, il suffit de rechercher « download JDK » sur internet et de choisir la version Java SE (standard edition).

Du code source à l'exécution

En Java, le code source est stocké sous forme de fichiers dont l’extension est .java. La compilation s’effectue grâce à la commande : javac. Comme indiqué ci-dessous, elle produit des fichiers .class qui sont du bytecode :

La chaîne de production du bytecode

L’exécution du bytecode s’effectue grâce à la commande java. Le code est alors exécuté sur une machine virtuelle (JVM) qui le transforme en code natif pour être exécuté par le processeur de votre machine.

L'exécution du bytecode

L’avantage de passer par cette phase de bytecode est d’obtenir un code multi-plateformes : contrairement à un code C++ qui, une fois compilé sous Linux, ne pourra pas être exécuté sur MacOS ou Windows, un bytecode compilé sous Linux pourra être exécuté sur une JVM MacOS ou Windows car il est indépendant de la machine physique sur laquelle il est compilé. L’inconvénient réside dans le fait que la machine virtuelle Java, la JVM, doit tout de même transformer le bytecode en code machine, ce qui induit un overhead.

Premiers pas en Java

La syntaxe Java est vraiment très similaire à celle de C++ :

Il existe toutefois des différences et c’est ce sur quoi nous allons insister ici. Ci-dessous, le classique « hello world ».

hello.cpp
#include <iostream>

int main(int argc, char *argv[]) {
    std::cout << "hello world" << std::endl;
    return 0;
  }
}

En Java, l’équivalent du std::cout est System.out. De plus, là où l’on utilise l’opérateur << en C++, on utilise la méthode print() ou println() (si on veut rajouter un retour à la ligne) en Java. Java est un langage objet, donc on ne crée pas des fonctions, comme main en C++, mais on les encapsule dans une classe Main. Pour éviter d’avoir à créer une instance de cette classe pour appeler la méthode main, on indique que celle-ci est public static, comme on l’aurait fait en C++. Notez que le fichier ci-dessous s’appelle Main.java, c’est-à-dire qu’il a le même nom que la classe qu’il contient. C’est ainsi que l’on travaille en Java : chaque fichier contient une classe et le nom du fichier et de la classe sont identiques.

Main.java
public class Main {
  public static void main(String[] args) {
    System.out.println("hello world");
  }
}

La compilation de ce fichier se fait via la commande:

javac Main.java

Cela produira un fichier Main.class, que l’on pourra exécuter via la commande :

java Main

Notez qu’en Java, le main ne retourne rien, contrairement au C++ qui retourne un entier. En fait,quand le main() se termine, le programme Java se termine également et la JVM n’a plus rien à faire. Du coup, elle n’a pas besoin de ce code de retour. Si vous avec besoin de récupérer un code de retour après l’exécution de la commande java Main, il suffit d’utiliser System.exit :

Main.java
public class Main {
  public static void main(String[] args) {
    System.out.println("hello world");
    System.exit(42);
  }
}

Ainsi, l’exécution des commandes suivantes :

java Main
echo $?

produire l’affichage suivant :

hello world
42

Les variables et leur type

À l’instar du C++, en Java, on définit les variables en spécifiant leur type. Par exemple :

Main.java
public class Main {
  public static void main(String[] args) {
    int nombre = 34;
    char caractere = 'A';

    System.out.println(nombre);
    System.out.println(caractere);
  }
}

Exactement comme en Javascript, mais contrairement au C++, les types de Java sont classés en deux catégories différentes, qui ont leur propre comportement :

Les types primitifs:

Ce sont les types de base du langage :

Chaque fois que vous créez une variable avec l’un de ces types, cela alloue un nouvel espace en mémoire pour stocker la valeur de la variable. Autrement dit, si l’on écrit le code suivant :

Main.java
public class Main {
  public static void main(String[] args) {
    int x = 3;
    int y = x;

    System.out.println(x);
    y += 4;  // on modifie y mais pas x
    System.out.println(x);
  }
}

On verra deux fois apparaître le nombre 3. En effet, à l’issue des deux instructions sur fond jaune, la mémoire correspond à l’image ci-dessous :

La mémoire avec des types primitifs

Les types "référence":

Les Types Référence correspondent à tout ce qui n’est pas type primitif, c’est-à-dire aux données complexes comme, par exemple, les objets, les chaînes de caractères ou les tableaux. Comme leur nom l’indique, quand on crée une variable d’un tel type, on stocke une référence vers l’espace mémoire qui contient sa valeur. On peut considérer que cela correspond en gros aux pointeurs du C++.

Main.java
 1public class Main {
 2  public static void main(String[] args) {
 3    int[] x = {1, 2, 3};
 4    int[] y = x;
 5
 6    System.out.println(x[0]);
 7    y[0] += 4;
 8    System.out.println(x[0]);
 9  }
10}

Quand on exécute le code ci-dessus, on verra apparaître un 1 et un 5 car, comme indiqué dans la figure ci-dessous, y et x pointent sur le même espace mémoire et, donc, modifier y[0] modifie également x[0]. Ici x et y sont des tableaux d’entiers.

La mémoire avec des types référence

Attention

En principe, pour créer un type référence, on a besoin d’utiliser l’opérateur new. Il existe parfois des raccourcis qui évitent ce new. C’est le cas de l’instruction de la ligne 3 ci-dessus, qui utilise les accolades pour remplir le tableau. On a également un raccourci pour les chaînes de caractères, comme le montre le code ci-dessous.

Main.java
public class Main {
  public static void main(String[] args) {
    int[] x = new int[3];  // un tableau de taille fixe = 3
    x[0] = 1;
    int[] y = x;
    String s1 = new String("toto");
    String s2 = "titi";  // c'est un raccourci de la ligne du dessus
    System.out.println(x[0]);
    System.out.println(s1);
    System.out.println(s2);
  }
}

Les types Référence introduisent une problématique intéressante : que veut dire l’opérateur == ? Est-ce qu’il doit comparer si les références (donc les pointeurs) sont les mêmes ou bien si les valeurs pointées sont les mêmes ? En Java, == va comparer les références. Ainsi, dans le code ci-dessous, si l’on compare deux String contenant la même chaîne avec ==, on obtiendra un false. Heureusement, les types Référence proposent des opérateurs comme equals qui permettent de comparer les valeurs référencées.

Main.java
public class Main {
  public static void main(String[] args) {
    String x = new String ("toto");
    String y = new String ("toto");

    System.out.println("Comparaison x == y: " + (x == y));
    System.out.println("Comparaison x.equals(y): " + x.equals(y));
  }
}

Convention de nommage :

En Java, il est usuel d’utiliser la notation camel pour les variables, les noms de méthodes et leurs paramètres:

int maSuperVariable = 3;
public void maMethodeDeOuf (int premierParam, float deuxiemeParam) {}

Attention

Il est important de donner des noms qui reflètent les informations contenues dans la variable ou ce que fait la méthode. Utiliser des noms x, s1, etc., n’est pas une bonne pratique si vous souhaitez éviter les bugs. De même, ne pas appeler une variable « L » minuscule (l) ou « O » majuscule (O) car cela ressemble trop aux nombres 1 et 0.

Par opposition, les noms des classes sont usuellement en notation Pascal:

public class MaMegaClass {}

Exercice 2 : L'infâme Fizzbuzz   

Le fizzbuzz est un exercice classique des recruteurs en informatique. Pour un nombre entier n, le fizzbuzz de n est égal :

Écrivez un programme qui affiche le fizzbuzz de tous les nombres entiers de 1 à 15.

Exercice 3 : La reproduction des lapins   

Écrivez un programme qui affiche les valeurs de la suite de Fibonacci jusqu’à ce que celle-ci atteigne une valeur supérieure ou égale à 1000.

On rappelle que la suite de Fibonacci est définie par :

\[\begin{split}\begin{array}{l} u_0 = 1, u_1 = 1 \\ u_{n} = u_{n-1} + u_{n-2}, \mbox{ pour tout } n \geq 2 \end{array}\end{split}\]

Casting et var

Le système de cast correspond à ce que l’on a en C. Il est donc moins évolué que celui du C++. En voici un exemple, qui montre le principe général : si on risque de perdre de l’information d’un type vers un autre, on doit caster explicitement.

Main.java
public class Main {
  public static void main(String[] args) {
    double x = 3.5;
    int y = 2;
    long z;
    z = (long) x; // OK on caste le double en long avant de l'affecter
    z = x;        // Erreur de compil : on perd de l'info de double vers long
    z = y;        // OK car on ne perd pas d'info de int vers long
  }
}

À noter que, par défaut, une constante entière, 2 par exemple, est un int et qu’une constante réelle est par défaut un double. Par conséquent, le principe général implique :

Main.java
public class Main {
  public static void main(String[] args) {
    float x1 = 3.5;   // Erreur de compil : 3.5 = double
    float x2 = 3.5F;  // OK: un F ou un f indique qu'il s'agit d'un float
    float x3 = (float) 3.5;  // OK le double est casté explicitement en float

    long y1 = 2;   // OK le int a été casté en long
    long y2 = 2L;  // OK le L indique qu'il s'agit d'un long

    int  z1 = 12345678912;  // Erreur : nombre trop grand pour un int
    long z2 = 12345678912;  // Erreur : 12345678912 est censé être un int, trop grand
    long z3 = 12345678912L; // OK : 12345678912L est un long, ça tient en mémoire
  }
}

Casting à partir de et vers les chaînes de caractères :

Les types primitifs ont tous des wrappers objets qui permettent, entre autres, de transformer une chaîne de caractères en une valeur de leur type. À l’inverse, la classe String a des méthodes valueOf qui permettent de transformer des nombres en String. Une autre possibilité, moins élégante, consiste à concaténer via l’opérateur + deux chaînes de caractères dont la deuxième provient d’un casting implicite du nombre (cf. les lignes 15 et 16 ci-dessous).

Main.java
 1public class Main {
 2  public static void main(String[] args) {
 3    int     x1 = Integer.parseInt("42");
 4    long    x2 = Long.parseLong("42");
 5    float   x3 = Float.parseFloat("12.34");
 6    double  x4 = Double.parseDouble("123.456");
 7    boolean x5 = Boolean.parseBoolean("true");
 8
 9    String y1 = String.valueOf(42);
10    String y2 = String.valueOf(42L);
11    String y3 = String.valueOf(12.34F);
12    String y4 = String.valueOf(123.456);
13    String y5 = String.valueOf(true);
14
15    String y6 = "" + 3;
16    String y7 = "" + true;
17  }
18}

Attention

À noter que les string sont des objets non mutables, c’est-à-dire que l’on ne peut pas modifier leur contenu une fois créées.

Le mot-clef "var" :

JDK 10 a introduit un nouveau mot-clef « var », qui correspond au mot-clef « auto » du C++. L’idée est, quand on déclare une variable en l’initialisant, d’inférer de cette initialisation le type de la variable, ce qui évite des redondances dans le code.

Main.java
public class Main {
  public static void main(String[] args) {
    int[] x = new int[10]; // déclaration int[] un peu redondante
    var   y = new int[10]; // du int[10], var déduit que y est de type int[]
  }
}

Attention

N’abusez pas du « var » : vous avez intérêt à l’utiliser quand il y a un new à droite du signe = d’affectation. Par exemple, var x = 3; est une mauvaise pratique car on ne voit pas rapidement en lisant le code quel est le type de x.

Exercice 4 : Qui c'est ce type ?   

Écrivez un programme qui permet de déterminer quels types Java considère qu’ont les constantes 2, 123456, 123456L, 123.456, 123.456F.

Indice 1 

Jusqu’à maintenant, dans la classe Main, on n’a utilisé qu’une seule méthode public static, le main(), mais rien n’interdit de créer d’autres méthodes public static. En exploitant le polymorphisme, ces méthodes peuvent toutes avoir le même nom mais prendre en paramètre des types différents.

Indice 2 

Voici un exemple d’exploitation du polymorphisme : on a deux méthodes du même nom, fonction, mais qui attendent des arguments de types différents. Si on appelle fonction en lui passant un int, ce sera la première méthode qui sera exécutée. Si on lui passe un long, ce sera la deuxième méthode.

Main.java
public class Main {
  public static void fonction(int x) {}

  public static void fonction(long x) {}

  public static void main(String[] args) {
    Main.fonction(2);   // appel à la 1ère fonction
    Main.fonction(2L);  // appel à la 2ème fonction
  }
}

Comment lire les entrées clavier ?

En C++, pour lire ce que l’utilisateur saisit au clavier, on utilise la classe cin:

std::cin >> variable1 >> variable2 >> variable3;

En C, on utilise plutôt un scanf :

scanf("%d %f %ld", &variable1, &variable2, &variable3);

En Java, il existe différentes méthodes mais celle qui semble être la plus populaire se rapproche du C, avec l’utilisation d’une instance de la classe Scanner :

Main.java
import java.util.Scanner;

public class Main {
  public static void main(String[] args) {
    System.out.print("saisie: ");

    Scanner scanner = new Scanner(System.in);
    byte   nombre_petit = scanner.nextByte();
    int    nombre_moyen = scanner.nextInt();
    float  nombre_reel  = scanner.nextFloat();
    String chaine       = scanner.nextLine();

    System.out.println("" + nombre_petit + " " + nombre_moyen + " " +
                       nombre_reel + " " + chaine);
  }
}
saisie: 12    1234    56.78 c'est la chaine
12 1234 56.78  c'est la chaine

L’utilisation de la classe Scanner est assez intuitive. Notez que, pour l’utiliser, on a besoin de l’importer (l’équivalent du #include du C++), cf. la ligne 1 du code ci-dessus.

Les constantes

Pour définir des constantes, Java n’utilise pas le mot-clef const comme C++ mais plutôt le mot-clef final :

Main.java
public class Main {
  public static void main(String[] args) {
    final double PI = 3.14;  // constante
    PI *= 2;  // Erreur
  }
}

La compilation de ce code par javac produira :

Main.java:4: error: cannot assign a value to final variable PI
    PI *= 2;
    ^
1 error

Attention

La notion de constante est complexe en informatique, cf. la vidéo de Herb Sutter sur const en C++ (à regarder en dehors de la séance).

Le mot-clef final signifie que l’on n’a pas le droit de changer la valeur de la variable (plus haut, PI). Donc, ok, forcément on a bien une constante… Stop ! Et si le type de la variable est un type référence, la valeur de celle-ci est un pointeur. Mais cela n’indique pas que l’on ne peut pas modifier le contenu de l’objet qui est pointé. Ainsi, le code ci-dessous compile et, une fois exécuté, il affiche la valeur 2.

Main.java
public class Main {
  public static void main(String[] args) {
    final int[] tableau = {1, 2, 3}; // tableau moyennement constant
    tableau[0] = 2;                  // on arrive à modifier les élément du tableau !
    System.out.println(tableau[0]);
  }
}

Exercice 5 : Les lapins - bis   

Écrivez un programme qui demande à l’utilisateur de choisir un nombre n strictement positif. On suppose que l’utilisateur saisit bien un nombre. Mais tant qu’il n’est pas strictement positif, on lui demande de ressaisir le nombre. Lorsque ce dernier est strictement positif, le programme calcule via un algorithme récursif la valeur du nième terme de la suite de Fibonacci et elle l’affiche à l’écran.

On rappelle que la suite de Fibonacci est définie par :

\[\begin{split}\begin{array}{l} u_0 = 1, u_1 = 1 \\ u_{n} = u_{n-1} + u_{n-2}, \mbox{ pour tout } n \geq 2 \end{array}\end{split}\]
 
© C.G. 2007 - 2024