Opérations binaires

Ce tutoriel traite des opérations binaires en Java. Les explications suivantes pourront néanmoins être utilisées dans tout autre langage. Ce turoriel a été à l'origine publié par le défunt magazine login.

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

Introduction

Les opérateurs binaires sont au nombre de deux en Java. Nous distinguerons ainsi les opérateurs ET et OU. Ceux-ci s'écrivent respectivement & et |. Attention cependant à ne pas les confondre avec leurs équivalents logiques (exclusifs), notés && et ||. L'utilisation conjointe de ces trois opérateurs offrira de nouvelles perspectives au programmeur non au fait de leur existence. De la sorte, nous allons découvrir comment se passer d'une liste interminable de booléens et comment coder une couleur de composantes RGB (Rouge, Vert, Bleu) sous forme d'entier.

1. Les binaires

En informatique, toute donnée, et donc tout nombre, est codée sous forme d'une succession de 0 et de 1 (les bits). L'ordinateur utilise donc une base 2 pour compter, dite « binaire ». Lorsque l'on écrit une succession de bits, on définit une somme de puissances de 2. Ainsi, la suite : 0101 se lit : « 0*2^3+1*2^2+0*2^1+1*2^0 ». La valeur indiquée est donc 5. Java fonctionne exactement ainsi. C'est à dire que chaque type primitif est codé sur un certain nombre de bits. Plus ce nombre est important, plus le type est en mesure d'enregistrer une valeur numérique importante. Voici donc un récapitulatif du nombre de bits que Java utilise pour coder certains types primitifs :

  • long: 64 bits
  • int: 32 bits
  • byte: 8 bits
  • boolean: 1 bit (en fait codé sur 32 bits)

En sus des opérateurs binaires décrits ci-dessus, Java offre des opérateurs de décalage binaire, extrêmement utiles et pratiques. Le premier, << , décale une série de bits de x bits vers la gauche. Chaque bit désignant une puissance de 2 supérieure au précédent, et le bit de poids fort (c'est à dire celui de plus haute puissance) étant à gauche, cet opérateur est comparable à une multiplication par 2 si le décalage est de 1. Ainsi: 2 << 1 = 4 car 0010 << 1 = 0100. Le second, >>, décale la série de x bits vers la droite, entraînant une division par 2 du nombre pour un décalage de 1. Même si ces opérateurs sont souvent utilisés pour leurs propriétés multiplicatives (en effet, << 1 est plus rapide à l'exécution que * 2), ils nous seront très utiles pour analyser des séries de bits.

2. Les opérateurs binaires

Voici les tables de vérité des opérateurs ET et OU:
& 0 1 | 0 1
0 0 0 0 0 1
1 0 1 1 1 1

Ayez bien en mémoire ces tables car elles vous seront nécessaires pour comprendre la suite de cet article. Comme premier exemple, nous allons décomposer un entier en quatre octets (bytes). Pourquoi quatre ? Tout simplement parce qu'un entier est codé sur 32 bits et un type byte sur 8 bits. Et 32 divisé par 8 égal ... 4 ! Voici donc le code :

 
Sélectionnez

byte[] b = new byte[4];
b[0] = (byte) (0xFF & (entier >> 8 * 0));
b[1] = (byte) (0xFF & (entier >> 8 * 1));
b[2] = (byte) (0xFF & (entier >> 8 * 2));
b[3] = (byte) (0xFF & (entier >> 8 * 3));

Le fonctionnement de ce bout de code n'est pas très compliqué même s'il peut le paraître de prime abord. Le but consiste ici à déplacer la série de bits de façon à changer le groupe des 8 premiers. Nous commençons par créer une valeur codée sur 8 bits (donc de la longueur d'un octet). Nous utilisons pour cela la valeur 0xFF (hexadécimale), qui n'est autre que la valeur décimale 256 (11111111 sur 8 bits). Ensuite nous décalons l'entier de 8 * x bits vers la droite, x étant l'index de l'octet à créer. L'application de l'opérateur & aura l'effet suivant :
000...11111111 & 010...10010110 = 000...10010110
Regardez bien la table de vérité de &. Son intérêt est que un 0 avec un 0 ou un 1 donne un 0 mais que un 1 avec un 0 donne 0 et qu'un 1 avec un 1 donne 1. Ainsi, les bits du membre de droite correspondant à la suite de huit 1 dans le membre de gauche conserveront leur valeur. Tous les autres bits resteront à 0. Enfin, une simple conversion en byte ne laissera subsister que les 8 premiers bits. L'opération inverse est encore plus simple :

 
Sélectionnez

int entier = (b[3] << 24) | (b[2] << 16) | (b[1] << 8) | b[0];

Procédons étape par étape. Tout d'abord, le quatrième octet est décalé de sorte à occuper les 8 premiers bits d'une série de 32 bits : 10010110 >> 24 = 10010110[24 zéros] La même opération est appliquée aux trois autres octets de manière à placer chaque octet à la place précédente dans la série de bits. Finalement, l'opérateur | est utilisé pour mettre ces séries bout à bout. En effet, l'opération | est conservatrice si le membre de gauche est un 0. En pratique :
10010110 00000000 00000000 00000000 octet 4 >> 24
| 00000000 01110101 00000000 00000000 octet 3 >> 16
| 00000000 00000000 10101001 00000000 octet 2 >> 8
| 00000000 00000000 00000000 11000010 octet 1
= 10010110 01110101 10101001 11000010 entier complet

En réalité, nous venons de découvrir comment coder une couleur RGB sous forme d'un entier et comment récupérer les composantes RGB d'une couleur depuis un entier. La seule différence est qu'il faudra omettre le quatrième octet dans les opérations (sauf si une composante alpha, pour la transparence, est utilisée).

3. Ras-le-booléen !

Après cette mise en jambe, quelque peu complexe il est vrai, voici un exemple d'utilisation des opérateurs binaires qui pourra s'avérer très avantageuse dans bien des situations. Imaginons en effet que vous écriviez un programme nécessitant la création d'une quinzaine de booléens. Il sera fastidieux de les déclarer tous, puis d'écrire leurs accesseurs (de type setBoolean() getBoolean() ou isBoolean()). Vous auriez alors environ 30 méthodes ! Heureusement, il existe une manière autrement plus élégante de parvenir à vos fins. Le principe est très simple : nous allons associer chaque bit d'un type primitif (si vous avez besoin de moins de 32 booléens, utilisez un int, sinon un long) à un booléen. Voici comment procéder. Tout d'abord, déclarez des constantes servant à identifier chaque booléen ainsi que le type primitif stockant ces booléens. Ces constantes contiendront l'index du booléen dans le type primitif utilisé :

 
Sélectionnez

private int attributes = 0;
public static final int LOADED = 0;
public static final int LOADING = 1;
public static final int WAITING = 2;
public static final int ABORTED = 3;
// etc ...

Nous imaginerons ici que la valeur 1 d'un bit signifie vrai, et la valeur 0, faux. Pour stocker un booléen dans notre entier, nous allons décaler la valeur 1 (si le booléen doit être mis à true) d'un nombre de bits vers la gauche égal à l'identifiant dudit booléen. Ensuite, nous appliquons l'opérateur | sur attributes et le décalage effectué pour mettre forcément à 1 le bit correspondant. Dans le cas d'un booléen devant être mis à false, nous décalons la valeur 0 comme précédemment puis appliquons l'opérateur & pour obtenir forcément 0.

 
Sélectionnez

public void set(int b, boolean value)
{
  if (value)
    attributes |= (1 << b);
  else
    attributes &= ~(1 << b);
}

Avec cette méthode, écrire set(LOADING, true) équivaudrait à setLoading(true). Il ne nous reste plus qu'à écrire la méthode permettant de connaître la valeur des booléens ainsi créés. Tout ce que nous avons à faire est de décaler attributes vers la droite pour placer le bit du booléen en bout de série. Ensuite, nous utilisons & avec la valeur 0x1 pour obtenir un entier constitué de 31 zéros et de un bit supplémentaire. Si l'entier créé de la sorte vaut 1, le booléen est vrai :

 
Sélectionnez

public boolean isSet(int b)
{
  return (0x1 & (attributes >> b)) == 1 ? true : false;
}

Appeler la méthode isSet(ABORTED) équivaudrait à isAborted(). Vous conviendrez que l'usage de ces deux méthodes est beaucoup plus pratique que celui d'accesseurs classiques. De plus, cette façon de procéder permet de programmer la valeur des booléens grâce à leurs index (donc dans des boucles, etc...).
A première vue, les opérations binaires semblent quelques peu obscures et compliquées, mais n'hésitez pas à vous entraîner à les utiliser. Vous découvrirez rapidement leur intérêt et leur puissance, voire leur élégance comme dans le cas de la gestion de booléens.

Articles et tutoriels Java
L'essentiel de Java en une heure
L'API java.nio du JDK 1.4
Inversion de contrôle en Java
L'introspection
Le Java Community Process
Conception de tests unitaires avec JUnit
Les Strings se déchaînent
Présentation de SWT
La programmation réseau en Java avec les sockets
Du bon usage de l'héritage et de la composition
Les références et la gestion de la mémoire en Java
Constructeurs et méthodes exportées en Java
Les membres statiques, finaux et non immuables en Java
Les classes et objets immuables en Java
Comprendre et optimiser le Garbage Collector
Les principes de la programmation d'une interface graphique
Les opérateurs binaires en Java
Prenez le contrôle du bureau avec JDIC
Les Java Data Objects (JDO version 1.0.1)
La persistance des données avec Hibernate 2.1.8
Journalisation avec l'API Log4j
Java 5.0 et les types paramétrés
Les annotations de Java 5
Java 1.5 et les types paramétrés
Créer un moteur de recherche avec Lucene
Articles et tutoriels Swing
Threads et performance avec Swing
Rechercher avec style en utilisant Swing
Splash Screen avec Swing et Java3D
Drag & Drop avec style en utilisant Swing
Attendre avec style en utilisant Swing
Mixer Java3D et Swing
Articles et tutoriels Java Web
Redécouvrez le web avec Wicket
Cette création est mise à disposition sous un contrat Creative Commons (Paternité - Partage des Conditions Initiales à l'Identique).