Voici un algorithme qui permet de calculer une approximation de :
<< Aller dans un pub, s'assurer qu'il contient bien un jeu de fléchettes et que la cible est un disque inscrit dans un carré ; prier l'un des consommateurs de lancer des fléchettes n'importe où dans le carré ; compter le nombre de fléchettes qui sont plantées dans le disque ; faire le quotient de ce nombre par le nombre total de fléchettes plantées dans le carré ; multiplier par 4 ce quotient ; retourner ce produit. >>
Si vous ne disposez pas des ressources naturelles nécessaires à l'implémentation de cet algorithme, vous pouvez recourir à un ordinateur et le programmer comme suit ; vous devrez simuler le lancer de fléchettes par un tirage de nombres aléatoires, et représenter le quadrant supérieur droit du disque de la cible par une fonction , avec :
class Cible { private int dedans; private static double f(double x) { // x doit être compris entre -1 et 1 return Math.sqrt(1 - x*x); } Cible() { dedans = 0; } int valDedans() { return dedans; } void lancer() { // tirage d'un point dans le carré [0,1[ X [0,1[ double u = Math.random(); double v = Math.random(); if (v <= f(u)) dedans++; } } class Simulation { private static double calculAire(int n) { // évalue l'aire de la cible par une méthode de Monte-Carlo Cible cible = new Cible(); for (int i=0; i<n; i++) cible.lancer(); return (double)cible.valDedans()/n; } public static void main(String[] args) { int tirages = Integer.parseInt(args[0]); double pi = 4 * calculAire(tirages); System.out.println("Pi = " + pi); } }
Il s'agit de la définition de deux classes, Cible et Simulation. La première définit un modèle d'objets, la seconde un modèle de calcul. La classe Cible est destinée à être instanciée, c'est-à-dire à produire des objets de type Cible. Une fois ces objets créés, les deux opérations que l'on peut faire sur ceux-ci sont : lancer() et valDedans(). Ces deux opérations utilisent dedans, un champ de la classe Cible qui indique le nombre de fléchettes dans la cible et une fonction f() qui détermine le contour d'une cible ; ce champ et cette fonction sont des membres de la classe Cible. La classe Simulation n'est pas destinée à être instanciée. Elle rassemble deux fonctions, calculAire() et main(). C'est cette dernière, dite fonction principale, par laquelle l'exécution du programme commence. Plaçons ces deux définitions de classe dans un fichier de nom Simulation.java. Puis compilons ce fichier par la commande suivante sous Linux :
linux% javac Simulation.java
L'exécution de cette commande produit deux autres fichiers, Cible.class et Simulation.class , qui contiennent des instructions exécutables par la Machine Virtuelle Java . La classe Cible n'est pas exécutable, parce qu'elle ne contient pas de fonction principale. La classe Simulation peut être exécutée, parce qu'elle en contient une :
linux% java Simulation 1000000
La commande java démarre une Machine Virtuelle Java, lui fournit la classe principale Simulation et l'argument 1000000. La Machine Virtuelle Java charge la classe Simulation et les classes que celle-ci utilise, notamment la classe Cible ; elle exécute ensuite les instructions définies par la fonction principale de Simulation. Tout programme doit contenir la définition d'une fonction principale qui doit s'appeler main ; l'exécution d'un programme commence par l'exécution de la fonction main(), qui invoquera éventuellement d'autres fonctions.
public static void main(String[] args) { int tirages = Integer.parseInt(args[0]); double pi = 4 * calculAire(tirages); System.out.println("Pi = " + pi); }
Cette définition de fonction a un en-tête,
public static void main(String[] args)
et un corps, qui est placé entre l'accolade ouvrante {
et
l'accolade fermante }
. L'en-tête exprime que la fonction
main() a un paramètre de nom args et de type
String[] (c'est le (String[] args)
à la suite de
main) et ne retourne rien (c'est le type void); on dit
d'une telle fonction, qui ne retourne rien, qu'il s'agit d'une
procédure. Son corps définit les deux variables locales
tirages et pi, l'une de type int (un entier,
l'un des types primitifs), l'autre de type double (un nombre
flottant en double précision, un autre type primitif). À la première est
affectée la valeur de l'expression Integer.parseInt(args[0]), à
la seconde la valeur de l'expression 4 * calculAire(tirages)
. La
valeur de la première expression est l'entier
fourni comme argument à la ligne de commande. L'expression
calculAire(tirages)
étant une invocation de la fonction
calculAire()
, celle-ci est maintenant exécutée.
private static double calculAire(int n) { // évalue l'aire sous f par une méthode de Monte-Carlo Cible cible = new Cible(); for (int i=0; i<n; i++) cible.lancer(); return (double)cible.valDedans()/n; }
La fonction calculAire()
a un paramètre n, de type
int, et retourne un double. Son corps définit une
variable locale cible, de type Cible, crée un objet de
ce type, au moyen de new Cible(), et affecte à la variable
cible une référence à cet objet. Ensuite, au moyen de
l'itération
for (int i=0; i<n; i++)
la méthode lancer()
de l'objet cible est invoquée n fois. Enfin, la méthode
valDedans() de l'objet cible est invoquée, retourne un
entier qui est converti en double, divisé par n, puis
retournée comme valeur de l'expression calculAire(tirages)
qui
figurait dans la fonction principale. Revenons à cette fonction. La
valeur retournée est multipliée par 4 et le résultat est affecté à la
variable pi. La dernière ligne,
System.out.println("Pi = " + pi);
a pour effet d'écrire à l'écran la suite de caractères Pi =
,
suivie de la valeur de la variable pi, en notation décimale :
Pi = 3.14138
L'exécution est alors terminée. On obtient 3.14138 comme estimation de , soit seulement trois décimales exactes, après un million de lancers (c'est le 1000000 spécifié sur la ligne de commande). Il y a heureusement des méthodes de calcul de qui sont bien meilleures1.1. D'autres exécutions de ce programme peuvent conduire à des résultats différents, à cause du random.
Il reste à revoir la classe Cible qui, rappelons-le, définit un modèle d'objets (qui sont ses instances). Même si, dans ce programme, la classe Simulation ne crée qu'un seul objet de type Cible, on peut imaginer d'autres situations où plusieurs cibles seraient nécessaires. Chaque cible est dotée d'un champ dedans, dont la valeur sera le nombre de fléchettes dans la cible (la cible ignore celles qui frappent le mur). Ce champ, de type int, est déclaré private, ce qui signifie qu'il n'est pas accessible de l'extérieur de la classe. On veut cependant pouvoir lire la valeur de ce champ, c'est ce que permet la méthode valDedans(). La différence entre une fonction et une méthode est visible dans leurs en-têtes : une fonction est déclarée static, une méthode ne l'est pas. Une méthode est destinée à être appliquée à une instance de la classe, tandis qu'une fonction, qui ne dépend d'aucune instance, doit être invoquée via la classe où elle est définie. C'est pourquoi les fonctions sqrt() (racine carrée), random() (génératrice de nombres pseudo-aléatoires) et parseInt() (conversion d'une chaîne de caractères en entier) sont invoquées sous la forme Math.sqrt(x), Math.random(), et Integer.parseInt(s), les deux premières étant définies dans la classe Math, la troisième dans Integer. Ainsi, la méthode lancer() n'a de sens que relativement à une cible particulière, et est invoquée sous la forme cible.lancer(), mais toutes les cibles partagent une même fonction f() qui définit un même contour. La fonction f() est également déclarée private, car on ne souhaite pas qu'une cible soit utilisée simplement pour obtenir les valeurs de cette fonction : on interdit ainsi à une autre classe de faire appel à elle. Inversement, la fonction main() de la classe Simulation est déclarée public, car elle doit être invoquée de l'extérieur, par la Machine Virtuelle Java. Le corps de f() contient une seule instruction,
return Math.sqrt(1 - x*x);
qui contient l'expression << Math.sqrt(1 - x*x) >>, exprimant , laquelle est une invocation de la fonction Math.sqrt() ; l'argument de cet appel est l'expression << 1 - x*x >>. Cette dernière expression est formée à partir des opérateurs << - >> et << * >>, de la constante littérale 1 et du nom x.
Toutes ces définitions (de classe, de champ, de fonction, de méthode, de variable locale) ont plusieurs rôles :
On ne peut pas utiliser un nom sans que celui-ci soit déclaré quelque part, et il faut encore que ce quelque part soit accessible. La déclaration indique les conditions d'accessibilité, où (le nom est-il private, public ?) et comment (de quel type est-il ? si c'est une fonction ou une méthode, quels sont les types de ses arguments, de sa valeur de retour ?). Enfin, le corps d'une fonction ou d'une méthode, ou la valeur qui est affectée à une variable permettent de définir la signification attachée au nom. On notera que dans une déclaration de variable locale, comme dans une liste de paramètres, le type précède le nom, à la façon des adjectifs en anglais : Cible cible, int i, double x. Ces déclarations servent à contraindre l'usage qui peut être fait d'un nom : il est incorrect de former l'expression 2 * cible, car aucune opération de multiplication n'est définie entre un entier et un objet de type Cible, ou de former l'expression i.lancer(), car la méthode lancer() n'est pas définie sur le type int. Tout ce travail de vérification est fait par le compilateur, qui détecte ainsi les erreurs les plus grossières (qui sont souvent dues à une faute de frappe).
Le texte compris entre << // >> et la fin de la ligne est un commentaire : inutilisables par la Machine Virtuelle Java, les commentaires facilitent la compréhension des programmes.