Syntaxe et types¶
La syntaxe de Java te sera familière (accolades, points-virgules, comme en C++). Ce chapitre se concentre sur les types de base et sur les endroits où Java se comporte différemment de ce que tu attends.
Dans ce chapitre
- Les 8 types primitifs, sans entiers non signés, et le dépassement silencieux
- Divisions, conversions et
charen UTF-16 var,finalet les chaînesString(immuables, à comparer avecequals)switchmoderne (expression) et ancien (avec fall-through), boucles et labels- Tableaux, méthodes
static, surcharge et varargs (mais ni valeurs par défaut ni arguments nommés)
Les exemples de ce chapitre utilisent la forme compacte (void main(), voir Installer et lancer) : copie-les dans un fichier et lance java Fichier.java.
Les types primitifs¶
Java a exactement 8 types primitifs. Tout le reste est un objet.
| Type | Taille | Plage ou rôle | Rust |
|---|---|---|---|
byte |
8 bits | -128 à 127 | i8 |
short |
16 bits | -32 768 à 32 767 | i16 |
int |
32 bits | environ ±2,1 milliards | i32 |
long |
64 bits | environ ±9,2 × 1018 | i64 |
float |
32 bits | IEEE 754 simple précision | f32 |
double |
64 bits | IEEE 754 double précision | f64 |
char |
16 bits | une unité UTF-16 (non signée) | u16 (pas char) |
boolean |
— | true / false |
bool |
Les tailles sont fixes sur toutes les plateformes, contrairement au C++ où int et long varient.
Piège : pas d'entiers non signés
Il n'y a ni u8, ni u32, ni unsigned int. Un byte va de -128 à 127, ce qui surprend quand on lit des octets bruts (fichier, réseau) :
byte b = (byte) 200;
IO.println(b); // affiche : -56
IO.println(Byte.toUnsignedInt(b)); // affiche : 200
IO.println(Integer.toUnsignedString(-1)); // affiche : 4294967295
Pour les calculs non signés, il existe des méthodes utilitaires (Integer.divideUnsigned, Long.compareUnsigned...) et l'opérateur >>> (décalage à droite sans propagation du signe).
Les variables locales doivent être initialisées avant usage (error: variable x might not have been initialized), comme en Rust. Les champs d'un objet, eux, reçoivent une valeur par défaut : 0, false ou null.
Littéraux¶
int million = 1_000_000; // _ comme séparateur, comme en Rust
long grand = 3_000_000_000L; // suffixe L obligatoire au-delà de 2^31 - 1
float f = 3.14f; // suffixe f : sans lui, 3.14 est un double
double d = 3.14;
int hexa = 0xFF; // 255
int binaire = 0b1010; // 10
char c = 'A'; // guillemets simples ; "A" serait une String
Sans suffixe, le compilateur refuse :
float f = 3.14; → error: incompatible types: possible lossy conversion from double to float
int i = 3_000_000_000; → error: integer number too large
Arithmétique : les pièges¶
Le dépassement est silencieux¶
int max = Integer.MAX_VALUE;
IO.println(max + 1); // affiche : -2147483648
IO.println(Math.abs(Integer.MIN_VALUE)); // affiche : -2147483648 (!)
long l = Integer.MAX_VALUE + 1; // calcul fait en int, PUIS élargi en long
IO.println(l); // affiche : -2147483648
long m = Integer.MAX_VALUE + 1L; // un opérande long : calcul en long
IO.println(m); // affiche : 2147483648
Java fait toujours du complément à deux avec rebouclage (wrapping), en debug comme en production. Rust panique en mode debug, le C++ déclare le dépassement signé comme comportement indéfini, Python passe en entier illimité. Quand le dépassement est une erreur, demande-le explicitement :
try {
Math.addExact(max, 1); // aussi : subtractExact, multiplyExact, toIntExact
} catch (ArithmeticException e) {
IO.println(e.getMessage()); // affiche : integer overflow
}
Pour les très grands nombres, java.math.BigInteger (entier illimité) et BigDecimal (décimal exact, obligatoire pour l'argent) existent, sans opérateurs : a.add(b), a.multiply(b).
Division et modulo¶
IO.println(7 / 2); // affiche : 3 (division entière)
IO.println(-7 / 2); // affiche : -3 (tronque vers zéro, comme Rust et C++)
IO.println(-7 % 2); // affiche : -1 (Python donnerait 1)
IO.println(Math.floorMod(-7, 2)); // affiche : 1 (le modulo « mathématique »)
IO.println(7 / 2.0); // affiche : 3.5 (un double suffit)
IO.println(1.0 / 0); // affiche : Infinity
IO.println(0.1 + 0.2); // affiche : 0.30000000000000004
1 / 0 entre entiers lève une ArithmeticException: / by zero.
Conversions (casts)¶
- Élargissement implicite :
byte→short→int→long→float→double, sans rien écrire. - Rétrécissement explicite :
(int) x, sinon erreurpossible lossy conversion.
long l = 42; // int → long : implicite
int i = (int) 3.99; // 3 : tronque vers zéro
int j = (int) 3_000_000_000L; // -1294967296 : garde les 32 bits de poids faible
int k = (int) 1e20; // 2147483647 : sature (comme `as` en Rust)
Toute opération sur des byte, short ou char est faite en int : byte c = a + b; ne compile pas (possible lossy conversion from int to byte). Curiosité : a += b; compile, car l'opérateur composé contient un cast caché.
char est une unité UTF-16¶
Un char n'est pas un caractère Unicode comme en Rust, mais un entier 16 bits :
IO.println('A' + 1); // affiche : 66 (char + int = int)
char c = 'A';
c++;
IO.println(c); // affiche : B
String emoji = "😀";
IO.println(emoji.length()); // affiche : 2 (deux unités UTF-16)
IO.println(emoji.codePointCount(0, emoji.length())); // affiche : 1
Pour du texte arbitraire, raisonne en code points (s.codePoints()), pas en char.
var et final¶
var (Java 10) infère le type d'une variable locale à partir de son initialisation. Le typage reste statique : ce n'est pas du Python.
var compteur = 0; // int
var noms = new ArrayList<String>(); // ArrayList<String>
// compteur = "texte"; // erreur : compteur est un int
var est interdit pour les champs, les paramètres et les types de retour, et exige un initialiseur non null (var v; et var n = null; sont refusés avec cannot infer type for local variable).
final interdit la réaffectation :
C'est l'inverse de Rust : en Java tout est réaffectable par défaut, et final est l'option. Mais final porte sur la variable, pas sur l'objet : une final List reste modifiable. On y revient dans Mémoire et références.
Les chaînes : String¶
String est une classe (pas un primitif) et ses objets sont immuables : chaque méthode renvoie une nouvelle chaîne.
String nom = "Duncan";
String maj = nom.toUpperCase();
IO.println(nom + " " + maj); // affiche : Duncan DUNCAN
IO.println("Total : " + 1 + 2); // affiche : Total : 12 (de gauche à droite !)
IO.println("Total : " + (1 + 2)); // affiche : Total : 3
IO.println(1 + 2 + " euros"); // affiche : 3 euros
+ avec une String convertit l'autre opérande en texte. Méthodes courantes : length(), charAt(i), substring(debut, fin), indexOf, contains, startsWith, strip(), isBlank(), split(","), repeat(3), String.join("-", liste).
Text blocks et formatage¶
String json = """
{
"nom": "%s",
"age": %d
}
""".formatted(nom, 30);
IO.print(json);
// affiche :
// {
// "nom": "Duncan",
// "age": 30
// }
Un text block """ est une chaîne multiligne ; l'indentation commune (alignée sur le """ fermant) est retirée. formatted (ou String.format) utilise la syntaxe de printf : %s, %d, %5.2f, %n.
Construire une chaîne en boucle : StringBuilder¶
Concaténer avec += dans une boucle recrée une chaîne à chaque tour (coût quadratique). Utilise un StringBuilder, l'équivalent d'une String Rust qu'on remplit avec push_str :
var sb = new StringBuilder();
for (int i = 0; i < 5; i++) {
sb.append(i).append(',');
}
sb.setLength(sb.length() - 1); // retire la dernière virgule
IO.println(sb); // affiche : 0,1,2,3,4
Comparer des chaînes¶
Piège : == sur des String
== compare les références (est-ce le même objet ?), pas le contenu. Utilise equals :
String a = "java";
String b = new String("java");
IO.println(a == b); // affiche : false
IO.println(a.equals(b)); // affiche : true
Le piège est sournois : deux littéraux identiques sont partagés par la JVM, donc "java" == "java" vaut true... jusqu'au jour où l'une des chaînes vient d'un calcul ou d'une saisie. Explication complète dans Mémoire et références.
Structures de contrôle¶
Les conditions doivent être des boolean : if (n) avec un int ne compile pas (int cannot be converted to boolean), contrairement au C++ et à Python. Le ternaire cond ? a : b existe.
switch : la forme moderne¶
Depuis Java 14, switch est une expression avec des flèches ->, proche du match de Rust :
String typeJour(String jour) {
return switch (jour) {
case "samedi", "dimanche" -> "week-end";
case "lundi" -> {
IO.println("(courage)");
yield "début de semaine"; // yield : la valeur d'un bloc
}
default -> "semaine";
};
}
- Plusieurs valeurs par
case, séparées par des virgules ; pas de fall-through. - En expression, le
switchdoit être exhaustif : sansdefaultici, erreurthe switch expression does not cover all possible input values. - On peut aussi s'en servir comme instruction, avec
->et sans valeur. - Il accepte entiers,
char,String, enums, et des motifs de type (voir Records, enums, sealed).
switch : l'ancienne forme¶
Tu la croiseras dans du code existant. Avec :, l'exécution continue dans les case suivants tant qu'il n'y a pas de break, comme en C++ :
int mois = 2;
switch (mois) {
case 1:
IO.println("janvier");
case 2:
IO.println("février");
case 3:
IO.println("mars");
break;
default:
IO.println("autre");
}
// affiche :
// février
// mars
Dans du code neuf, préfère toujours la forme -> : pas de fall-through accidentel, et l'exhaustivité est vérifiée.
Boucles et labels¶
for (int i = 0; i < 3; i++) { ... } // classique, comme en C++
for (int note : notes) { ... } // for-each, comme `for x in` (tableaux et collections)
while (cond) { ... }
do { ... } while (cond); // au moins un tour
externe: // label, comme 'externe: en Rust
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (i * j == 2) {
IO.println("trouvé " + i + "," + j); // affiche : trouvé 1,2
break externe; // sort des deux boucles
}
}
}
Pas de for i in 0..10 : pour une plage, c'est la boucle classique (ou IntStream.range(0, 10), vu dans Lambdas et streams).
Tableaux¶
Un tableau Java a une taille fixe choisie à la création, comme [T; N] en Rust, mais allouée sur le tas.
int[] zeros = new int[3]; // [0, 0, 0] : initialisé par défaut
String[] vides = new String[2]; // [null, null]
int[] premiers = {2, 3, 5, 7};
IO.println(premiers.length); // affiche : 4 (un champ, pas une méthode)
IO.println(premiers); // affiche par ex. : [I@60bd273d (type + hash)
IO.println(Arrays.toString(premiers)); // affiche : [2, 3, 5, 7]
int[][] grille = new int[2][3]; // tableau de tableaux
Arrays.sort(premiers); // tri en place
Les accès sont vérifiés : premiers[4] = 11; lève ArrayIndexOutOfBoundsException: Index 4 out of bounds for length 4. Comme une panique Rust, jamais de corruption mémoire silencieuse comme en C++.
Piège : trois façons d'avoir la taille
tableau.length (champ), chaine.length() (méthode), liste.size() (méthode). t.length() sur un tableau donne cannot find symbol. Et dans la pratique, on préfère une ArrayList qui grandit toute seule (voir Génériques et collections).
Méthodes¶
Hors fichier compact, il n'existe pas de fonction libre : une fonction utilitaire est une méthode static d'une classe, appelée par NomClasse.methode(...) (comme Math.abs, Arrays.sort). Une méthode renvoie une seule valeur ; pour en renvoyer plusieurs, on crée un record (voir Records, enums, sealed).
public class Tableaux {
static int somme(int... valeurs) { // varargs : valeurs est un int[]
int total = 0;
for (int v : valeurs) total += v;
return total;
}
static String saluer(String nom) { // surcharge : même nom,
return saluer(nom, "Bonjour"); // autres paramètres
}
static String saluer(String nom, String formule) {
return formule + ", " + nom;
}
public static void main(String[] args) {
System.out.println(somme() + " " + somme(1, 2, 3)); // affiche : 0 6
System.out.println(saluer("Ada")); // affiche : Bonjour, Ada
System.out.println(saluer("Ada", "Salut")); // affiche : Salut, Ada
}
}
Piège : ni valeurs par défaut, ni arguments nommés
saluer(String nom, String formule = "Bonjour") et saluer(formule: "Salut") ne compilent pas. La surcharge (comme ci-dessus) remplace les valeurs par défaut. Quand il y a beaucoup de paramètres, on passe un objet de configuration ou un builder (Personne.builder().nom("Ada").age(36).build()), un motif que tu croiseras souvent avec Spring.
La surcharge est résolue à la compilation, selon les types statiques des arguments. Contrairement à Rust, deux méthodes peuvent avoir le même nom ; contrairement à Python, aucune méthode n'écrase l'autre.
Exercices¶
1. Sans exécuter, prédis ce qu'affichent ces lignes, puis vérifie.
IO.println("1" + 2 + 3);
IO.println(1 + 2 + "3");
IO.println((byte) 130);
IO.println(-7 / 2);
IO.println('a' + 'b');
IO.println((char) ('a' + 1));
IO.println(10 / 4 * 4.0);
Solution
123 ← "1" + 2 donne "12", puis "12" + 3
33 ← 1 + 2 = 3 (entiers), puis 3 + "3"
-126 ← 130 ne tient pas dans un byte : 130 - 256
-3 ← division entière, tronquée vers zéro
195 ← deux char additionnés donnent un int (97 + 98)
b ← on recaste l'int en char
8.0 ← 10 / 4 vaut 2 (entiers) AVANT la multiplication par 4.0
2. Écris int compterVoyelles(String texte) (voyelles : a, e, i, o, u, y, majuscules comprises) en utilisant un switch expression. Indice : texte.toCharArray() donne un char[].
Solution
int compterVoyelles(String texte) {
int n = 0;
for (char c : texte.toLowerCase().toCharArray()) {
n += switch (c) {
case 'a', 'e', 'i', 'o', 'u', 'y' -> 1;
default -> 0;
};
}
return n;
}
void main() {
IO.println(compterVoyelles("Bonjour Duncan")); // affiche : 5
IO.println(compterVoyelles("RYTHME")); // affiche : 2
}
3. Écris long factorielle(int n) qui lève une exception au lieu de déborder. Teste avec 20 et 21.
Solution
long factorielle(int n) {
long resultat = 1;
for (int i = 2; i <= n; i++) {
resultat = Math.multiplyExact(resultat, i); // ArithmeticException si dépassement
}
return resultat;
}
void main() {
IO.println(factorielle(20)); // affiche : 2432902008176640000
try {
IO.println(factorielle(21));
} catch (ArithmeticException e) {
IO.println("21! : " + e.getMessage()); // affiche : 21! : long overflow
}
}
4. Écris une méthode max qui accepte un nombre quelconque d'int, mais au moins un (l'appel max() doit être refusé à la compilation).
Solution
Un paramètre normal suivi de varargs impose le premier argument :
public class Max {
static int max(int premier, int... autres) {
int m = premier;
for (int x : autres) {
if (x > m) m = x;
}
return m;
}
public static void main(String[] args) {
System.out.println(max(4)); // affiche : 4
System.out.println(max(4, 9, 2)); // affiche : 9
// max(); → error: method max in class Max cannot be applied to given types;
}
}
Avec max(int... valeurs), max() compilerait et tu devrais gérer le tableau vide à l'exécution.
À retenir¶
- 8 primitifs de taille fixe, aucun non signé ;
charest une unité UTF-16, pas un caractère. - Le dépassement entier reboucle en silence :
Math.addExactet consorts pour le détecter,BigDecimalpour l'argent. - Élargissement implicite, rétrécissement par cast explicite ; division entière tronquée vers zéro.
varinfère, mais le typage reste statique ;finalempêche la réaffectation, pas la mutation.Stringest immuable ; compare avecequals, jamais==;StringBuilderdans les boucles.switchavec->: expression, exhaustif, sans fall-through ; l'anciencase X:a du fall-through.- Tableaux de taille fixe et vérifiés (
.length,Arrays.toString) ; pas de fonction libre, pas de valeur par défaut ni d'argument nommé, mais surcharge et varargs.