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).
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).
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 :
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’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.
La syntaxe Java est vraiment très similaire à celle de C++ :
les blocs sont représentés par des accolades {} ;
les types de base des variables (char, short, int, long, float, double) existent dans les deux langages ;
les structures de contrôle if/else, switch, les boucles for, while, do-while, les break et continue sont les mêmes en Java et C++ ;
les opérateurs logiques, de comparaison, et ternaires (?:) sont les mêmes ;
les commentaires /* */ et // sont identiques ;
les fonctions/méthodes se déclarent de la même manière, en spécifiant le type de retour ainsi que ceux des paramètres.
Il existe toutefois des différences et c’est ce sur quoi nous allons insister ici. Ci-dessous, le classique « hello world ».
#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.
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 :
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
À l’instar du C++, en Java, on définit les variables en spécifiant leur type. Par exemple :
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 :
Ce sont les types de base du langage :
les entiers (byte, short, int, long),
les nombres réels (float, double),
les caractères (char),
les booléens (boolean).
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 :
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 :
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++.
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.
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.
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.
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));
}
}
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 {}
Le fizzbuzz est un exercice classique des recruteurs en informatique. Pour un nombre entier n, le fizzbuzz de n est égal :
à la chaîne « fizz » si n est multiple de 3 ;
à la chaîne « buzz » si n est multiple de 5 ;
à la chaîne « fizzbuzz » si n est multiple à la fois de 3 et de 5 ;
au nombre n si n n’est multiple ni de 3 ni de 5.
Écrivez un programme qui affiche le fizzbuzz de tous les nombres entiers de 1 à 15.
É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 :
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.
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 :
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
}
}
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).
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.
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.
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.
Écrivez un programme qui permet de déterminer quels types Java considère qu’ont les constantes 2, 123456, 123456L, 123.456, 123.456F.
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 :
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.
Pour définir des constantes, Java n’utilise pas le mot-clef const comme C++ mais plutôt le mot-clef final :
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.
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]);
}
}
É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 :