Les bases

Les bases #

Commentaires #

Un commentaire peut s’écrire sur une ou plusieurs lignes. N’abusez jamais des commentaires contrairement à ce que l’on vous a peut-être appris par le passé. Un bon commentaire est un commentaire qui n’existe pas. Le commentaire sert à expliquer une partie du code qui n’est pas compréhensible. Si tel est le cas, envisagez de rendre votre code plus lisible et simple à comprendre plutôt que de devoir l’annoter. Un code simple est plus facile à maintenir. On dit généralement qu’un code est écrit une fois et qu’il est lu une vingtaine de fois.

1
2
3
4
// commentaire sur une ligne

/* commentaire
   sur plusieurs lignes */

Exemple de commentaires inutiles:

1
2
3
4
int age = 20; // age de la personne
if (age >= 18) { // contrôle si la personne est majeure
  ...
}

La documentation sur les fonctionnalités publiques d’une API est par contre importante.

Convention de nommage pour identifiants #

De la même manière, un identifiant doit être explicite et signifier son intention. La notation se fait en camelCase (par. ex: value, globalTax, veryBigVariableName, …)

Deux règles d’or pour un identifiant :

  • Plus la visibilité d’une variable est grande, plus le nom doit être explicite.
  • Un identifiant doit être prononçable
  • La déclaration se fait le plus près possible de son utilisation

Exemples de bon ou mauvais identifiants:

1
2
3
4
5
6
int tsc; // très mauvais, n'exprime rien, imprononçable
int timeSinceCreation; // mieux, mais quelle est l'unité ?
int timeSinceCreationInDays; // parfait !
for (int i = 0; i<10; i++) { // ok, i a une visibilité très courte
    print( tab[i] ); 
}

Structure simple d’un programme #

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/*
 * Programme HelloWorld, le fichier doit s’appeler 
 * HelloWorld.java 
 */
public class HelloWorld {
  public static void main(String[] args) {

    System.out.println(Hello Guys!);

  }
}

Compilation et exécution:

1
2
javac HelloWorld.java
java HelloWorld

Voici quelques explications sur la syntaxe. Nous aborderons les méthodes statiques et la création de classes dans une prochain chapitre.

  • public class HelloWorld: le programme principal est une classe à visibilité publique
  • méthode main :
    • code du programme principal
    • static: n’est pas liée à un objet
    • void: ne retourne rien
    • String[] args: permet de récupérer les arguments

Java sans faire de programmation orientée objet #

Avant d’aborder le paradigme, nous pouvons présenter comment utiliser des fonctions sans faire de la programmation orientée objet.

Reprenons le même fichier, mais ajoutons des fonctions classiques. Une fonction qui n’est pas rattachée à un objet est dite “statique”.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class HelloWorld {
  public static double sumThreeTerms(double d1, double d2, double d3) {
    return d1 + d2 + d3;
  }
  public static double doubleThat(double d) {
    return 2.0 * d;
  }
  public static void main(String[] args) {

    System.out.println( doubleThat(sumThreeTerms(1.0, 2.0, 3.0)) ); // affiche 12.0

  }
}

Les types primitifs #

Voici un tableau de tous les types primitifs. Un type primitif est stocké sur la pile (référence d’un article: Que sont la pile et le tas )

désignation type
nombres entiers byte, short, int, et long
nombres réels float et double
caractère char
booléen boolean

Un type primitif commence par une minuscule. Chaque type primitif a son équivalent en classe (Byte, Integer, Double…) que nous découvrirons plus tard.

Conseil

Privilégiez toujours les types primitifs. Ils sont stockés sur la pile. Les objets sont stockés sur le tas, ce qui est beaucoup moins performant.

Voici un tableau détaillant la plage de valeur des différents types (source wikibooks ):

Types primitifs

Exemples de déclaration de variable avec le int:

1
2
3
4
5
int i1;         // attribution de la valeur 0 par défaut
int i2 = 3; 
int i3 = 3 * 2; // attribution d’une expression
int i4 = 42, i5 = 22;     // attribution multiple
int i6 = 12, i7, i8 = 15; // attention, ici i7 vaut 0
Seules les lignes 2 et 3 sont recommandées dans ce cours

Les types entiers #

Le tableau ci-dessous indique la taille réservée pour stocker les différents types d’entiers.

type octets
byte 1
short 2
int 4
long 8

Le nombre de valeurs possibles est donc limité à la taille réservée. Par exemple, le type byte comprend les valeurs comprises entre -128 et 127.

L’arithmétique est dite modulaire lorsqu’un dépassement de capacité de la valeur maximum génère un débordement vers la valeur minimum. L’ensemble des valeurs forme un cycle. (La classe utilitaire Math propose plusieurs fonctionnalités pour traiter ce problème).

1
2
3
4
5
6
7
jshell> int j = 2147483647 + 1 // 2147483647 est la valeur max d'un int
j ==> -2147483648 // débordement !

jshell> Math.addExact(2147483647, 1) // fonction utilitaire
|  Exception java.lang.ArithmeticException: integer overflow
|        at Math.addExact (Math.java:825)
|        at (#1:1)

Deux types différents peuvent être compatibles. Java propose un mécanisme de conversion implicite s’il ne peut y avoir de perte de précision. Un byte peut être converti implicitement en short, mais la conversion implicite inverse est impossible.

Quelques règles de conversions importantes sur les entiers #

  1. Un littéral entier est de type int.
    • Un littéral est une valeur donnée explicitement dans le code, par exemple: int i = 3; // 3 est un littéral.
  2. Lors d’une affectation1, le compilateur refuse tout littéral entier dont la valeur n’appartient pas au domaine de définition du type de la variable.
1
2
3
4
5
int i = 12;  // 12 est un littéral
byte b1 = 127; // Ok, 127 est converti en byte
byte b2 = 128; // Erreur, 128 est hors du domaine de définition d’un byte
short s1 = 10; // Ok
short s2 = 20; // Ok
  1. Pour convertir un littéral en long, il est nécessaire de le post-fixer par l ou L.
1
2
long l2 = 3000000000; // Erreur, 3mia n’est pas un int 
long l3 = 3000000000L; // Ok
  1. Promotion numérique: la promotion numérique est une conversion automatique qui a lieu sur des opérateurs de type entier:
    • la valeur d’une expression entière (comme l’addition de deux opérandes) est de type int
    • dans une expression, les opérandes de types byte ou short sont convertis en int
    • corolaire: une division de deux entiers retourne un entier !
1
2
3
4
5
6
7
short s1 = 10;
short s2 = 20;
short s3 = s1 + s2; // Erreur, le résultat s1+s2 retourne
                    // un entier. Il est impossible de 
                    // l'attribuer à un short
short s4 = s1;      // Ok, la valeur d'affectation n'est pas une expression
3 / 2;              // ==> 1 !!!!

La deuxième règle concerne l’affectation et non l’invocation d’une fonction/méthode. L’extrait ci-dessous n’est pas équivalent :

1
2
short s = 42; /* ok, la valeur est dans l'ensemble de définition
               * la règle s'applique pour une affectation */

1
2
3
4
void test(short s) { ... }
...
test(42); /* ko, ne compile pas. La règle ne s'applique pas 
             pour l'invocation */

Nous verrons plus tard qu’il est possible de surcharger une fonction ; ce qui permet d’avoir des fonctions qui ont le même nom, mais des types ou un nombre d’arguments qui diffèrent. Dans de tels cas, le compilateur fixe le type pour déterminer la fonction candidate à invoquer.

Les types réels #

Les types réels font partie du standard IEEE 754 floating point . Le tableau ci-dessous indique la taille réservée pour stocker les différents types de réels:

type octets
float 4
double 8

Quelques règles #

  1. Littéral réel est de type double
  2. Le compilateur refuse de l’attribuer à un float
  3. Conversion d’un double en float: post-fixer le littéral de f ou F
  4. Pas de promotion numérique: i. une opération sur deux floats retourne un float

Exemple d’assignation de réels:

1
2
3
4
5
float f1 = 10.0;  // Erreur, 10.0 est un double, ne peut être 
                  // attribué à un float
float f2 = 10.0f; // Ok
double d = 2e3;  // Notation scientifique = 2*10^3 = 2000.0
float f3 = f2 + f2;  // Ok, pas de promotion numérique

Le standard permet les valeurs particulières NaN, Infinity et -Infinity:

1
2
3
4
5
6
7
8
jshell> 0.0 / 0.0
$1 ==> NaN

jshell> 3 / 0.0
$2 ==> Infinity

jshell> -3 / 0.0
$3 ==> -Infinity

Danger
Etant donné que le nombre de bits est limité pour tenter de représenter toutes les valeurs réelles possibles, certains nombres ne peuvent être constitué. C’est ce que l’on appelle les problèmes de précisions sur les virgules flottantes.

1
2
3
4
5
jshell> 0.1 * 3 == 0.3
$9 ==> false

jshell> 0.1 * 3
$10 ==> 0.30000000000000004 // problème de précision

Evitez-les quand vous pouvez et lorsque la précision est importante. Pour représenter des nombres monétaires, vous pouvez par exemple employer des entiers et travailler en centimes. Vous pouvez également utiliser la technique du seuil de comparaison à l’aide de la valeur absolue de leur soustraction:

1
2
3
4
jshell> double d1 = 0.1 * 3;
jshell> double d2 = 0.3;
jshell> Math.abs(d1 - d2) < 0.0001 // précision au millième
$11 ==> true

Une autre solution consiste à utiliser une précision arbitraire à l’aide de la classe BigDecimal. Une précision arbitraire permet de représenter des nombres tant que la place mémoire le permet (cf: fr.qwe.wiki ).

1
2
new BigDecimal("0.3").divide(new BigDecimal("3"))
$12 ==> 0.1

Le type caractère #

Le type char occupe 2 octets pour stocker un caractère. Il utilise le standard Unicode. Les guillemets simples sont utilisés pour ce type alors que les guillemets doubles sont réservés pour les chaînes de caractères (classe String). Un caractère est interprété comme un entier non signé. Il respecte donc la promotion numérique.

1
2
3
4
5
6
7
8
9
char c1;       // initialisé à chaîne vide ''
char c2 = 'A';

int i = 10;   
char c3 = i;    // Erreur, conversion implicite int -> char 
                // impossible !

char c4 = 10;   // Ok, le compilateur comprend que 10 est 
                // dans l’ensemble de définition d’un char

Un objet de type String permet d’instancier une chaîne de caractères: String s = "Hello";. Nous verrons cette classe plus tard.

Le type booléen #

Ce type occupe 1 octet et permet deux valeurs possibles (true et false). Il n’y a pas de conversion implicite d’un autre type vers un boolean comme en C, JavaScript ou Python.

1
2
3
4
boolean b1 = 2;     // Erreur
boolean b2;         // b2 vaut false
boolean b3 = true;
boolean b4 = 3 > 4; // b4 vaut false

Compatibilité et équivalence de types primitifs #

Les types sont non équivalents entre eux : leur espace mémoire diffère, il existe des types signés et non signés… Par contre, en Java (contrairement à Rust ou Ada), ils sont comparables, voire compatibles. C’est-à-dire qu’il y a une conversion implicite lors d’une expression impliquant des types différents pour autant qu’il n’y ait pas de perte de magnitude (et non une perte de précision). Si une perte de magnitude est possible, le compilateur refuse l’expression. Il est alors obligatoire de réaliser une conversion explicite à l’aide d’un cast au risque de perdre de l’information.

Il n’y a pas de perte de magnitude si la valeur source se situera dans la fourchette de valeurs du type cible. Par contre, une perte de précision est possible! Autrement dit, si le type T1 est compris dans l’ensemble de définition de T2 alors T1 peut être converti implicitement en T2.

Conversions implicites #

Règles de conversions implicites:

  • une conversion implicite est légale s’il n’y a pas de perte de précision (conversion non dégradante)
  • conversion possible selon la hiérarchie suivante :
1
2
byte -> short -> int -> long -> float -> double
char -> int -> long -> float -> double

Les conversions implicites short -> char ou char -> short sont impossibles: ils ont la même taille, mais le short est signé, alors que le char ne l’est pas !

Exemples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
short s = 3;
float f = s; // ok, conversion short -> float

// définition d'une fonction (appelé méthode en POO)
void h(int i, float f) { ... }

short s; float f; 
h(s, s); // ok arg1 -> int, arg2 -> float
h(f, f); // erreur, f -> int impossible
h(s, 10.0); // erreur 10.0 est de type double

Perte de précision sans perte de magnitude #

1
2
3
4
5
6
// autre exemple
jshell> int origin = 123456789;
jshell> float target = origin; 
$1 ==> 1.23456792E8
jshell> (int)target - origin
$1 ==> 3 // perte de précision

Récapitulation des conversions implicites #

Voici un résumé des deux types de conversions implicites principales:

  • promotion numérique
    • concerne les opérateurs (addition, soustraction…) sur les entiers
    • les opérandes de type byte, short, char sont convertis en int
    • le résultat est toujours de type int
  • ajustement de types
    • si le type T1 est compris dans l’ensemble de définition de T2 alors T1 peut être converti implicitement en T2
    • par ex: int vers long
    • hiérarchie des conversions
      • byte -> short -> int -> long -> float -> double
      • char -> int -> long -> float -> double

Exemple illustrant une expression en raisonnant sur les types:

1
2
3
4
5
6
7
(short + short) + double
   |       |               // Promotion numérique short -> int
 (int  +  int) +  double
      int      +  double
       |                   // ajust. int -> double
     double    +  double
             double        // résultat

Conversions explicites (cast) #

Exemple de conversion explicite

1
2
3
4
5
6
short s1 = 32767;  // Ok, 32767 est la valeur max d'un short
short s2 = 32768;  // Erreur, 32768 hors ens. défin. short
short s3 = (short)32768;   // Conversion explicite
                           // Un cycle s’effectue lors
                           // d’un dépassement 
                           // ici s3 vaut -32768

Quiz #

Quelles sont les valeurs de n et de p ?

1
2
3
4
5
byte b = Byte.MAX_VALUE;
int n = b + 1;

int i = Integer.MAX_VALUE;
long p = i + 1;

Chaîne de caractères #

La chaîne de caractères String n’est pas un type primitif, mais une classe. Vous pouvez l’utiliser sans connaître le concept de classes/objets pour l’instant:

1
2
String s = "Hello";
String w = s + " Joel"; // concatenation

Inférence des types #

L’inférence des types est possible depuis Java 10 avec le mot-clé var.

1
2
3
var i = 3; // inféré en int
var d = 33.2; // inféré en double
var s = "Coucou"; // inféré en String

Non-attribution de valeur #

Une valeur arbitraire “neutre” est attribuée par défaut si aucune valeur n’est affectée explicitement à une déclaration de variable

  • 0 pour les nombres entiers
  • 0.0 pour les nombres réels
  • false pour les booléens
  • '' pour les caractères
  • null pour les objets

Déclaration de constantes #

Conseil
Une bonne pratique veut que toute référence non modifiée soit une constante. Ou plutôt, que toute référence devrait être constante par défaut, sauf s’il est vraiment nécessaire de la changer.

Une constante se déclare en précédant le type par le mot clé final. En programmation contemporaine, la constante est très largement utilisée et il ne devient pas nécessaire d’écrire l’identifiant en majuscule si sa visibilité est courte.

1
2
3
final double PI = 3.141592;
final var userId = 1;
final var result = weightInKg / (heightInCm*heightInCm);

Nous verrons que le mot-clé final peut être utilisé dans d’autre contexte.


  1. attribution d’une valeur à une variable ↩︎












comments powered by Disqus