Formation programmation JAVA
Java est un langage orienté objets
Les premiers ordinateurs étaient programmés en langage machine, c’est-à-dire en utilisant directement les instructions comprises par le processeur. Ces instructions ne concernaient évidemment que les éléments du processeur : registres, opérations binaires sur le contenu des registres, manipulation du contenu d’adresses mémoire, branchement à des adresses mémoire, etc. Le premier langage développé a été l’assembleur, traduisant d’une part mot à mot les instructions de la machine sous forme de mnémoniques plus facilement compréhensibles, et d’autre part masquant la complexité de certaines opérations en libérant le programmeur de l’obligation de décrire chaque opération dans le détail (par exemple, choisir explicitement un mode d’adressage).
L’assembleur, comme le langage machine, permet d’exprimer tous les problèmes qu’un ordinateur peut avoir à résoudre. La difficulté, pour le programmeur, réside dans ce que le programme (la « solution » du problème) doit être exprimé en termes des éléments du processeur (registres, adresses, etc.) plutôt qu’en termes des éléments du problème lui-même. Si vous avez à programmer un éditeur de texte, vous serez bien ennuyé, car l’assembleur ne connaît ni caractères, ni mots, et encore moins les phrases ou les paragraphes. Il ne permet de manipuler que des suites plus ou moins longues de chiffres binaires.
Certains programmeurs pensèrent alors qu’il serait plus efficace d’exprimer les programmes en termes des éléments du problème à résoudre. Les premiers ordinateurs étaient utilisés uniquement pour les calculs numériques. Aussi, les premiers langages dits « de haut niveau » exprimaient tous les problèmes en termes de calcul numérique. En ce qui concerne le déroulement des programmes, ces langages reproduisaient purement et simplement les fonctions des processeurs : tests et branchements.
Est rapidement apparue la nécessité de mettre un frein à l’anarchie régnant au sein des programmes. Les données traitées par les ordinateurs étant en majorité numériques, c’est au déroulement des programmes que fut imposée une structuration forte. Ainsi, le déroulement des programmes devenait (en théorie) plus facilement lisible (et donc ceux-ci devenaient plus faciles à entretenir), mais les données était toujours essentiellement des nombres (mais de différents formats) et éventuellement des caractères, voire des chaînes de caractères, ou même des tableaux de nombres ou de chaînes, ou encore des fichiers (contenant, bien sûr, des nombres ou des chaînes de caractères). Le pionnier de ces langages dits « structurés » fut Pascal, que sa conception extrêmement rigide limitait essentiellement à l’enseignement. Le langage C, universellement employé par les programmeurs professionnels, marqua l’apogée de ce type de programmation.
Tous les problèmes se présentant à un programmeur ne concernent pas forcément des nombres ou des caractères uniquement. Cela paraît évident, mais cette évidence était probablement trop flagrante pour entrer dans le champ de vision des concepteurs de langages, jusqu’à l’apparition des premiers langages orientés objets. Ceux-ci, en commençant par le précurseur, Smalltalk, permettent d’exprimer la solution en termes des éléments du problème, plutôt qu’en termes des outils employés.
Cela représente un énorme pas en avant pour la résolution de problèmes complexes, et donc pour la productivité des programmeurs. Bien sûr, toute médaille a son revers. En termes de performances pures, rien ne vaudra jamais un programme en assembleur. Toutefois, dans le cas de projets très complexes, il est probable que l’écriture et surtout la mise au point de programmes en assembleur, voire en langages « structurés », prendraient un temps tendant vers l’infini.
Le langage orienté objets le plus répandu est sans équivoque C++. Il s’agit d’une version de C adaptée au concept de la programmation par objets. Dans ce type de programmation, on ne manipule pas des fonctions et des procédures, mais des objets qui s’échangent des messages. Le principal avantage, outre le fait que l’on peut créer des objets de toutes natures représentant les véritables objets du problème à traiter, est que chaque objet puisse être mis au point séparément. En effet, une fois que l’on a défini le type de message auquel un objet doit répondre, celui-ci peut être considéré comme une boîte noire. Peu importe sa nature. Tout ce qu’on lui demande est de se comporter conformément au cahier des charges. Il devient ainsi extrêmement facile de mettre en œuvre un des concepts fondamentaux de la programmation efficace : d’abord écrire un programme de façon qu’il fonctionne. Ensuite, l’améliorer afin qu’il atteigne une rapidité suffisante.
Ce concept est particulièrement important, sinon le plus important de la programmation orientée objets. En effet, avec les langages des générations précédentes, cette approche, souvent utilisée, conduisait généralement à une solution médiocre, voire catastrophique. Il était d’usage de développer un prototype fonctionnel, c’est-à-dire un programme conçu rapidement dans le seul but de reproduire le fonctionnement souhaité pour le programme final. Les programmeurs les plus sérieux utilisaient pour cela un langage de prototypage. Une fois un prototype satisfaisant obtenu, ils réécrivaient le programme dans un langage plus performant. L’avantage était qu’ils avaient ainsi l’assurance de repartir sur des bases saines lors de l’écriture de la version finale. L’inconvénient était qu’il fallait faire deux fois le travail, puisque l’intégralité du programme était à réécrire. De plus, il était nécessaire de connaître deux langages différents, ce qui n’augmentait pas la productivité.
Une autre approche consistait à développer un prototype dans le même langage que la version finale, avec la ferme décision de réécrire ensuite, de façon « propre », les éléments qui auraient été mal conçus au départ. Satisfaisante en théorie, cette solution n’était pratiquement jamais réellement appliquée, la version finale étant presque toujours la version initiale agrémentée de nombreux replâtrages là où il aurait fallu une réécriture totale, celle-ci s’avérant impossible en raison des nombreuses connexions apparues en cours de développement entre des parties du code qui auraient dû rester indépendantes.
Les langages orientés objets, et en particulier Java, apportent une solution efficace et élégante à ce problème en définissant de manière très stricte la façon dont les objets communiquent entre eux. Il devient ainsi tout à fait possible de faire développer les parties d’une application par des équipes de programmeurs totalement indépendantes. C’est d’ailleurs le cas pour le langage Java lui-même, comme on peut s’en apercevoir en examinant les noms choisis pour les méthodes des différents objets du langage. Certains sont courts et obscurs, d’autres longs et explicites, en fonction de la personnalité du programmeur les ayant choisis. (Ces différences ont d’ailleurs tendance à disparaître dans les nouvelles versions du langage, marquant en cela une volonté d’unification appréciable.)
Java est extensible à l’infini
Java est écrit en Java. Idéalement, toutes les catégories d’objets (appelées classes) existant en Java devraient être définies par extension d’autres classes, en partant de la classe de base la plus générale : la classe Object. En réalité, cela n’est pas tout à fait possible et certaines classes doivent utiliser des méthodes (nom donné à ce que nous considérerons pour l’instant comme des procédures) natives, c’est-à-dire réalisées sous forme de sous-programmes écrits dans le langage correspondant à une machine ou un système donné.
Le but clairement annoncé des concepteurs du langage Java est de réduire les méthodes natives au strict minimum. On peut en voir un bon exemple avec les composants Swing, intégrés à la Java 2. Ceux-ci remplacent les composants d’interface utilisateur (boutons, listes déroulantes, menus, etc.) présents dans les versions précédentes et qui faisaient appel à des méthodes natives. Les composants Swing sont intégralement écrits en Java, ce qui simplifie la programmation et assure une portabilité maximale grâce à une indépendance totale par rapport au système. (Il est néanmoins tout à fait possible d’obtenir un aspect conforme à celui de l’interface du système utilisé. Ainsi, le même programme tournant sur une machine Unix, sur un PC et sur un Macintosh pourra, au choix du programmeur, disposer d’une interface strictement identique ou, au contraire, respecter le « look and feel » du système utilisé.)
Par ailleurs, Java est extensible à l’infini, sans aucune limitation. Pour étendre le langage, il suffit de développer de nouvelles classes. Ainsi, tous les composants écrits pour traiter un problème particulier peuvent être ajoutés au langage et utilisés pour résoudre de nouveaux problèmes comme s’il s’agissait d’objets standard.
Java est un langage à haute sécurité
Contrairement à C++, Java a été développé dans un souci de sécurité maximale. L’idée maîtresse est qu’un programme comportant des erreurs ne doit pas pouvoir être compilé. Ainsi, les erreurs ne risquent pas d’échapper au programmeur et de survivre aux procédures de tests. De la même façon, en détectant les erreurs à la source, on évite qu’elles se propagent en s’amplifiant.
Bien sûr, il n’est pas possible de détecter toutes les erreurs au moment de la compilation. Si on crée un tableau de données, il est impossible au compilateur de savoir si l’on ne va pas écrire dans le tableau avec un index supérieur à la valeur maximale. En C++, une telle situation conduit à l’écriture des données dans une zone de la mémoire n’appartenant pas au tableau, ce qui entraîne, au mieux, un dysfonctionnement du programme et, au pire (et le plus souvent), un plantage généralisé. Avec Java, toute tentative d’écrire en dehors des limites d’un tableau ne conduit simplement qu’à l’exécution d’une procédure spéciale qui arrête le programme. Il n’y a ainsi aucun risque de plantage. Un des problèmes les plus fréquents avec les programmes écrits en C++ est la « fuite de mémoire ». Chaque objet créé utilise de la mémoire.
Une fois qu’un objet n’est plus utilisé, cette mémoire doit être rendue au système, ce qui est de la responsabilité du programmeur. Contrairement au débordement d’un tableau, ou au débordement de la pile (autre problème évité par Java), la fuite de mémoire, si elle est de faible amplitude, n’empêche ni le programme ni le système de fonctionner. C’est pourquoi les programmeurs ne consacrent pas une énergie considérable à traquer ce genre de dysfonctionnement. C’est aussi pourquoi les ressources de certains systèmes diminuent au fur et à mesure de leur utilisation. Et voilà une des raisons pour lesquelles vous devez redémarrer votre système de temps en temps après un plantage. (C’est le cas de Windows. Ce n’est pas le cas d’autres systèmes qui se chargent de faire le ménage une fois que les applications sont terminées. Cependant, même dans ce cas, les fuites de mémoire demeurent un problème tant que les applications sont actives.)
Avec Java, vous n’aurez jamais à vous préoccuper de restituer la mémoire une fois la vie d’un objet terminée. Le garbage collector (littéralement « le ramasseur de déchets ») s’en charge. Derrière ce nom barbare se cache en effet un programme qui, dès que les ressources mémoire descendent au-dessous d’un certain niveau, permet de récupérer la mémoire occupée par les objets qui ne sont plus utiles. Un souci de moins pour le programmeur et surtout un risque de bug qui disparaît. Bien sûr, là encore, il y a un prix à payer. Vous ne savez jamais quand le garbage collector s’exécutera. Dans le cas de contrôle de processus en temps réel, cela peut poser un problème. (Il existe cependant d’ores et déjà des versions de Java dans lesquelles le fonctionnement du garbage collector est contrôlable, mais cela sort du cadre de ce livre.)