Manipuler des objets

Notion d'objet

On peut considérer un objet comme un ensemble d'informations liées les unes aux autres, associé à des mécanismes de manipulation des informations en question. Un exemple simple est fourni par les objets String de Java. Dans ce cas, on a :

informations
une suite de caractères
mécanismes
nombre de caractères, accès à un caractère précis, chercher un caractère, etc.

En Java, les méthodes d'un objet (appelées méthodes d'instance) réalisent les manipulations des informations contenues dans l'objet.

Type objet

Pour définir informatiquement un objet, il faut décrire l'ensemble d'informations qui le constitue et les mécanismes de manipulation de ces informations. Cette description est fournie par une Classe en Java. Par exemple, il existe une classe String qui décrit les objets String. La classe est en fait le type de l'objet et on dit que l'objet est une instance de la classe, simplement pour insister sur le fait que son contenu et son comportement sont décrits par la classe (qui joue donc le rôle de patron/modèle pour les objets).

Création d'un objet

Sauf pour le type String, la création d'un objet se fait toujours avec l'instruction new, en utilisant la forme générale new {nom de la classe de l'objet}({paramètres}) dans laquelle paramètres désigne d'éventuels paramètres de création de cet objet. Par exemple :

Scanner scan = new Scanner(System.in);

s'interprète comme la création d'un objet de type Scanner paramétré pour lire le clavier (ce qui est représenté par le paramètre System.in).

Le résultat de la création d'un objet est un pointeur (une référence dans le vocabulaire Java) vers l'objet créé sur le tas. Dans l'exemple au dessus, on place ce pointeur dans une variable scan de type Scanner (elle aussi).

Dans le cas spécifique des String, on peut utiliser des valeurs littérales en écrivant par exemple

String s = "Toto";

mais rien n'empêche d'utiliser new comme dans la version suivante de l'exemple

String s = new String("Toto");

Attention, les résultats sont subtilement différents…

Manipulation d'un objet

Pour manipuler les informations contenues dans un objet, on utilise des méthodes d'instance avec une syntaxe générale de la forme {référence vers un objet}.{méthode}({paramètres}) comme dans l'exemple suivant :

public class DemoString {
    public static void main(String[] args) {
	String s = new String("Bonjour");
	System.out.println(s.length());
	System.out.println(s.charAt(0));
	System.out.println(s.toUpperCase());
	System.out.println("Toto".substring(2));
    }
}

qui affiche

7 B BONJOUR to

Identité

Un principe fondamental de la programmation orientée objet est celui de l'identité : chaque objet est unique, même si les informations qu'il représente sont identiques à celle d'un autre objet. C'est ce principe qui explique que le programme suivant

public class Identity {
    public static void main(String[] args) {
	String s = new String("Bonjour");
	String t = new String("Bonjour");
	System.out.println(s == t);
    }
}

affiche

false

En effet, bien que s et t désignent des objets représentant tous les deux le texte Bonjour, ces deux objets sont distincts, ce qui se manifeste par des références (pointeurs) différentes. La comparaison des variables s et t s'effectuant sur le contenu de ces variables, elle renvoie la valeur de vérité faux puisque ces références sont différentes.

En Java, le principe d'identité s'applique aussi aux tableaux bien que ces derniers ne soient pas vraiment des objets. Ceci explique pourquoi le programme suivant

public class ArrayIdentity {
    public static void main(String[] args) {
	int[] x = { 1, 2 };
	int[] y = { 1, 2 };
	System.out.println(x == y);
    }
}

affiche

false

Méthode equals

Le principe d'identité est parfois gênant en pratique. Pour contourner cette limitation, les objets possèdent une méthode equals. Dans la plupart des cas, la méthode equals a la même sens que la comparaison obtenue par ==. Cependant, pour certains objets, la méthode correspond bien à une comparaison des informations associées aux objets. C'est le cas pour les String, par exemple. Le programme suivant

public class StringEquals {
    public static void main(String[] args) {
	String s = new String("Bonjour");
	String t = new String("Bonjour");
	System.out.println(s == t);
	System.out.println(s.equals(t));
    }
}

affiche

false true

En effet, la première comparaison s'effectue sur les objets en tant qu'entités. En application du principe d'identité, on obtient donc false. Au contraire, la deuxième comparaison utilise la méthode equals qui réalise ainsi une comparaison des informations, soit ici du texte représenté par chaque chaîne, ce qui conduit naturellement au résultat true.

Pour les tableaux, on dispose d'une méthode particulière dans la classe Arrays, comme le montre l'exemple suivant :

import java.util.Arrays;
public class ArrayEquals {
    public static void main(String[] args) {
	int[] x = { 1, 2 };
	int[] y = { 1, 2 };
	System.out.println(x == y);
	System.out.println(Arrays.equals(x,y));
    }
}

qui affiche

false true

pour les mêmes raisons que dans l'exemple des String.

Conversion vers String (affichage)

Pour faciliter l'interaction avec l'utilisateur, et même la programmation, il est pratique de pouvoir afficher les informations contenues dans (représentées par) un objet. Or, Java ne sait afficher que les chaînes de caractères (String) et les types fondamentaux (int, double, etc.). Plus précisément, tout objet possède une méthode toString() qui produit une chaîne de caractères censée représenter les informations associées à l'objet. Quand on tente d'afficher un objet (dans un System.out.println, par exemple), Java utilise automatiquement cette méthode. Malheureusement, le résultat n'est pas très utile en général, comme le montre l'exemple suivant. Le programme proposé crée un objet Random (qui fabrique des nombres aléatoires) et tente de l'afficher :

import java.util.Random;
public class RandomPrint {
    public static void main(String[] args) {
	Random rng = new Random();
	System.out.println(rng);
    }
}

On obtient :

java.util.Random@57543bc5

Comme pour les tableaux, le résultat n'est pas très utile. Il correspond au type de l'objet (avec son package ici) suivi d'une valeur entière correspondant au hash de l'objet écrit en hexadécimal (en général, ce hash est l'adresse mémoire de l'objet). Notons que tout se passe comme si on avait écrit System.out.println(rng.toString()) et donc que le problème vient de la méthode toString() dont l'intérêt pratique est en général assez limité.

Pour beaucoup d'objets cependant, la méthode toString() donne quelque chose de relativement utile. Le programme

import java.util.Calendar;
import java.util.Date;
public class CalendarDemo {
    public static void main(String[] args) {
	Calendar maintenant = Calendar.getInstance();
	System.out.println(maintenant);
	Date today =  maintenant.getTime();
	System.out.println(today);
    }
}

produit l'affichage complexe suivant :

java.util.GregorianCalendar[time=1351505071671,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="Europe/Paris",offset=3600000,dstSavings=3600000,useDaylight=true,transitions=184,lastRule=java.util.SimpleTimeZone[id=Europe/Paris,offset=3600000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=2,startMonth=2,startDay=-1,startDayOfWeek=1,startTime=3600000,startTimeMode=2,endMode=2,endMonth=9,endDay=-1,endDayOfWeek=1,endTime=3600000,endTimeMode=2]],firstDayOfWeek=2,minimalDaysInFirstWeek=4,ERA=1,YEAR=2012,MONTH=9,WEEKOFYEAR=44,WEEKOFMONTH=5,DAYOFMONTH=29,DAYOFYEAR=303,DAYOFWEEK=2,DAYOFWEEKINMONTH=5,AMPM=0,HOUR=11,HOUROFDAY=11,MINUTE=4,SECOND=31,MILLISECOND=671,ZONEOFFSET=3600000,DSTOFFSET=0] Mon Oct 29 11:04:31 CET 2012

L'objet Date est donc affiché en utilisant les conventions américaines (deuxième objet). Le premier affichage décrit en détail l'objet Calendar. Il s'agit ici du calendrier grégorien (standard en occident), ainsi que diverses informations contenues dans l'objet, comme la date courante, la zone temporelle, etc.

Rôle de la classe

La classe d'un objet joue le rôle de patron pour cet objet. Elle peut aussi proposer des méthodes de classe, ainsi que des constantes de classe. Contrairement aux méthodes d'instance, une méthode de classe n'est pas appelée à partir d'un objet, mais à partir d'une classe, sous la forme générale {Classe}.{méthode}({paramètres}). Par exemple, la classe Math fournit de nombreuses méthodes mathématiques de la forme Math.abs(-4) qui calcule la valeur absolue de son paramètre ou Math.sqrt(5) qui calcule la racine carrée de son paramètre.

En pratique, les méthodes de classe servent surtout à créer des objets dans des conditions spéciales, sans utiliser la technique new {nom de la classe de l'objet}({paramètres}). Par exemple, la classe String possède un ensemble de méthodes valueOf pour créer des chaînes de caractères à partir de valeurs de types fondamentaux. Par exemple String.valueOf(5) produit la chaîne de caractères ="5"=. Le programme au dessus contient un exemple de même nature (Calendar.getInstance()), de même que l'exemple complet ci-dessous (BigInteger.valueOf(k)).

Les constantes de classe sont utilisée pour des objets particuliers. Par exemple System.out est un objet (de type PrintStream) qui permet de faire des affichages. De même BigInteger.ONE est l'entier long 1 (cf en dessous). Ce sont des constantes, car une affectation de la forme BigInteger.ONE=... est interdite.

Objets immuables et objets modifiables

Comme les objets sont toujours manipulés par référence, on peut être confronté aux mêmes phénomènes d'alias que pour les tableaux. Voici un exemple révélateur :

public class StringBuilderAlias {
    public static void main(String[] args) {
	StringBuilder s = new StringBuilder();
	StringBuilder t = s; // attention, c'est le même objet
	System.out.println(t==s); // comme le confirme ceci
	System.out.println(s); // chaîne vide
	s.append("abc");
	System.out.println(s); // affiche abc
	System.out.println(t); // et donc on obtient le même affichage qu'au dessus
    }
}

Ce programme affiche :

true

abc abc

En effet les variables s et t désignent le même objet (c'est pourquoi s==t vaut true). De ce fait, toute modification de cet objet peut se « voir » en utilisant n'importe laquelle des variables. Le résultat est surprenant car on modifie l'objet avec la variable s et on voit le résultat avec une autre variable t.

Ce phénomène ne peut apparaître que si l'objet est modifiable. Or, il existe de nombreux objets immuables, c'est-à-dire dont la valeur est fixée définitivement au moment de la création. L'exemple suivant montre la différence avec le cas modifiable :

public class StringAlias {
    public static void main(String[] args) {
	String s = new String();
	String t = s; // attention, c'est le même objet
	System.out.println(t==s); // comme le confirme ceci
	System.out.println(s); // chaîne vide
	s = s + "abc";
	System.out.println(s); // affiche abc
	System.out.println(t); // affiche toujours une chaîne vide
	System.out.println(t==s); // car les objets sont différents !
    }
}

Ce programme affiche :

true

abc

false

Il n'existe en effet aucune méthode de modification d'un objet de type String. Toutes les opérations qui semblent modifier l'objet fabriquent en fait une nouvelle chaîne, un nouvel objet String. Ici, la variable s contient une nouvelle chaîne "abc" et la chaîne vide de départ n'a pas été modifiée, ce qui explique l'affichage obtenu.

Classes à connaître

String

Caractéristiques générales

objectif
un objet de type String représente une chaîne de caractères ;
immuable
les objets String ne sont pas modifiables après leur création ;
toString
pas de besoin de méthode toString() dans ce cas ;
equals
la méthode equals compare le contenu des chaînes.

Constructeurs

La classe String propose divers constructeurs (cf sa documentation). On utilisera en particulier les constructeurs suivants :

  • String() : création d'une chaîne vide (longueur 0) ;
  • String(s) : création d'une chaîne copie de la chaîne paramètre (ce sont deux objets indépendants qui représentent le même texte) ;
  • String(sb) : création d'une chaîne de caractères à partir d'un StringBuilder (cf ci-dessous).

Méthodes importantes

La classe String propose de très nombreuses méthodes dont les suivantes sont très utilisées :

  • charAt(index) : renvoie le caractère (char) de position index (numérotation à partir de 0) ;
  • concat(str) : renvoie une nouvelle chaîne de caractères constituée de la mise bout à bout de la chaîne appelante et de la chaîne str ;
  • indexOf(ch) : renvoie la position de la première apparition du caractère ch dans la chaîne (ou -1 si le caractère n'apparaît pas dans la chaîne). Il existe de nombreuses variantes (recherche depuis la fin, recherche d'une chaîne entière, etc.) ;
  • length() : donne la longueur de la chaîne (nombre de caractères) ;
  • substring(beginIndex,endIndex) : renvoie une nouvelle chaîne constituée des caractères de numéros beginIndex à endIndex-1 ;
  • String.valueOf(x) : méthode de classe qui fabrique un objet String correspondant à la représentation en chaîne de caractère de x.

StringBuilder

Caractéristiques générales

objectif
un objet de type StringBuilder représente une chaîne de caractères modifiable ;
modifiable
les objets StringBuilder sont modifiables, c'est leur principal intérêt par rapport aux objets String ;
toString
renvoie un objet String représentant la même chaîne que l'objet appelant ;
equals
la méthode equals compare les références (cf la discussion sur l'identité des objets et sur equals).

Constructeurs

La classe StringBuilder propose quatre constructeurs (cf sa documentation) dont les trois suivants sont les plus utiles :

  • StringBuilder() : création d'un StringBuilder représentant la chaîne vide et de capacité 16 (cf ci-dessous) ;
  • StringBuilder(capacité) : création d'un StringBuilder représentant la chaîne vide et de capacité capacité (cf ci-dessous) ;
  • StringBuilder(s) : création d'un StringBuilder représentant la chaîne de caractères donnée par l'objet s (de type String).

Comme un StringBuilder est modifiable, la chaîne qu'il représente peut changer de longueur. Pour rendre les changements plus efficaces, chaque StringBuilder contient de la place pour un certain nombre de caractères, c'est la capacité de l'objet. Si on augmente la taille du StringBuilder au delà de cette capacité, celle-ci est mise à jour automatiquement, mais il est préférable de lui donner une valeur adaptée dès la création de l'objet.

Méthodes importantes

La classe StringBuilder propose de très nombreuses méthodes dont les suivantes sont très utilisées :

  • charAt(index) : renvoie le caractère (char) de position index (numérotation à partir de 0) ;
  • append(x) : modifie l'objet appelant en lui ajoutant (à la fin) la représentation en chaîne de caractère de x ;
  • insert(offset, x) : modifie l'objet appelant en insérant à la position offset la représentation en chaîne de caractère de x (la fin de la chaîne est décalée) ;
  • length() : donne la longueur de la chaîne (nombre de caractères) ;
  • setCharAt(index,ch) : remplace le caractère de position index par ch (en modifiant l'objet).

Exemple d'utilisation

Le programme suivant contient une démonstration des fonctions des StringBuilder.

public class DemoStringBuilder {
    public static void main(String[] args) {
	StringBuilder s = new StringBuilder();
	System.out.println(s.length());
	StringBuilder t = s; // attention, même objet
	t.append("bla");
	System.out.println(s); // même objet !
	System.out.println(t.charAt(1));
	StringBuilder u = new StringBuilder("bla"); // objet différent
	System.out.println(s == t); // même objet
	System.out.println(t == u); // objets différents
	System.out.println(t.equals(u)); // comparaison des identités
	String v = u.toString();
	// démonstration de l'indépendance des représentations
	u.append(" et toto");
	System.out.println(u); 
	System.out.println(v);
	v = v + "bli";
	System.out.println(v);
	System.out.println(u); // dans l'autre sens
	u.setCharAt(3,'-');
	System.out.println(u);
	u.insert(7,"raoul et ");
	System.out.println(u);
    }
}

Il affiche :

0 bla l true false false bla et toto bla blabli bla et toto bla-et toto bla-et raoul et toto

BigInteger

Caractéristiques générales

objectif
un objet de type BigInteger représente un entier naturel sans limite de nombre de chiffres ;
immuable
les objets BigInteger ne sont pas modifiables après leur création ;
toString
produit l'écriture en base 10 du nombre ;
equals
la méthode equals compare les valeurs numériques des objets concernés ;
import
attention, il faut faire un import java.math.BigInteger pour utiliser ces objets

Constructeurs

La classe BigInteger propose des constructeurs d'utilisation assez technique (cf sa documentation). On leur préfère donc en général la méthode de classe valueOf (cf ci-dessous). On pourra aussi utiliser les constructeurs suivants :

  • BigInteger(s) : création d'un BigInteger représentant l'entier donné par la chaîne de caractères s, en base 10 ;
  • BigInteger(s, base) : création d'un BigInteger représentant l'entier donné par la chaîne de caractères s, en base base.

Méthodes importantes

La classe BigInteger propose de très nombreuses méthodes dont les suivantes sont très utilisées :

  • opération(val) : il s'agit d'un ensemble de méthodes de calcul. Chaque méthode renvoie le résultat d'un calcul impliquant l'objet appelant et l'objet val. Les opérations sont :
    • add(val) : addition
    • divide(val) : division (euclidienne, c'est-à-dire le quotient)
    • multiply(val) : multiplication
    • remainder(val) : reste de la division euclidienne
    • subtract(val) : soustraction
  • doubleValue() : renvoie une version approchée de l'entier sous forme d'un réel ;
  • longValue() : renvoie une version approchée de l'entier sous forme d'un réel (cette approximation peut donner quelque chose de complètement faux, attention) ;
  • BigInteger.valueOf(val) : méthode de classe qui fabrique un objet BigInteger correspondant à l'entier val.

La classe BigInteger propose aussi trois constantes dont les noms sont explicites : ZERO, ONE et TEN.

Exemple d'utilisation

Le programme suivant calcule la factorielle d'un entier saisie par l'utilisateur en s'appuyant sur les objets de type BigInteger.

import java.util.Scanner;
import java.math.BigInteger;
public class BigFact {
    public static void main(String[] args) {
	Scanner scan = new Scanner(System.in);
	System.out.print("N = ");
	int n = scan.nextInt();
	BigInteger factN = BigInteger.ONE;
	for(int k = 2; k <= n ; k++) {
	    BigInteger bK = BigInteger.valueOf(k);
	    factN = factN.multiply(bK);
	}
	System.out.println(n + "! = " + factN);
    }
}

On obtient par exemple :

N = 57 57! = 40526919504877216755680601905432322134980384796226602145184481280000000000000