Concepts avancés en programmation Java
Concepts avancés en programmation Java
INTRODUCTION
A. LES ENTREES-SORTIES, LES THREADS, LES SOCKETS
1. LES ENTREES-SORTIES
2. LES THREADS
3. LES SOCKETS
B. LES ANNOTATIONS DE TYPE, LA REFLEXION, LES AGENTS
1. LES ANNOTATIONS DE TYPE
2. LA REFLEXION
3. LES AGENTS
C. LES FRAMEWORKS ET BIBLIOTHEQUES POPULAIRES EN JAVA :
SPRING, HIBERNATE, JUNIT
1. SPRING
2. HIBERNATE
3. JUNIT
CONCLUSION
INTRODUCTION
Le Java est un langage de programmation très populaire utilisé par les développeurs pour
créer des applications web et desktop. Il est connu pour sa portabilité, sa sécurité, sa performance
et sa facilité d'utilisation. Java avancée est une étape supérieure pour les développeurs qui
cherchent à maîtriser davantage les fonctionnalités et les technologies les plus complexes du
développement Java.
En utilisant ces fonctionnalités avancées, les développeurs Java peuvent créer des
applications plus complexes et plus efficaces, capables de répondre à des exigences plus
importantes. Par exemple, la synchronisation des threads peut être utilisée pour garantir que les
tâches sont exécutées en parallèle de manière efficace, la sérialisation peut être utilisée pour
transférer des objets entre différents environnements Java, et la réflexion peut être utilisée pour
insérer et modifier dynamiquement des éléments dans une application Java pendant son
exécution.
Java avancée peut être utilisée pour développer des solutions dans différents domaines,
tels que les applications web, les applications mobiles, les systèmes distribués, la finance, la
sécurité, le machine learning, l'analyse de données et bien plus encore. En somme, Java avancée
est un ensemble de fonctionnalités et de concepts qui permettent aux développeurs Java de mettre
en place des solutions plus puissantes et plus sophistiquées pour répondre aux besoins exigeants
de l'industrie.
A. LES ENTREES-SORTIES, LES THREADS, LES SOCKETS
1- LES ENTREES-SORTIES
Les entrées-sorties en Java avancé sont gérées par les classes du package [Link]
([Link], [Link], [Link] et [Link]). Ces
classes obéissent au patron de conception décorateur. Les entrées/sorties (E/S) en Java permettent
à un programme Java d'interagir avec le monde extérieur en lisant des données à partir de sources
externes ou en écrivant des données dans des destinations externes.
- La classe StringReader permet de parcourir une chaîne de caractères sous la forme d’un
flux de caractères.
- La classe FileReader permet de lire le contenu d’un fichier texte.
- La classe StringWriter permet d’écrire dans un flux caractères pour ensuite produire une
chaîne de caractères.
- La classe FileWriter permet d’écrire un flux de caractères dans un fichier.
La classe Scanner
La classe [Link] agit comme un décorateur pour différents types d’instance qui
représentent une entrée. Elle permet de réaliser des opérations de lecture et de validation de
données plus complexes que les classes du packages [Link].
Fichiers et chemins
En plus des flux de type fichier, le package [Link] fournit la classe File qui représente un
fichier. À travers, cette classe, il est possible de savoir si le fichier existe, s’il s’agit d’un
répertoire… On peut également créer le fichier ou le supprimer.
Accès au réseau
La classe URL, comme son nom l’indique, représente une URL. Elle déclare la
méthode openConnection qui retourne une instance de URLConnection. Une instance
de URLConnection ouvre une connexion distante avec le serveur et permet de récupérer des
informations du serveur distant. Elle permet surtout d’obtenir une instance de OutputStream si on
désire envoyer des informations au serveur et une instance de InputStream si on désire récupérer
les informations retournées par le serveur.
2- LES THREADS
Les ``threads'' ou ``processus légers'' sont des unités d'exécution autonomes qui peuvent
effectuer des tâches, en parallèle avec d'autres threads: ils sont constitués d'un identificateur, d'un
compteur de programme, d'une pile et d'un ensemble de variables locales. Le flot de contrôle d'un
thread est donc purement séquentiel. Plusieurs threads peuvent être associés à un ``processus
lourd'' (qui possède donc un flot de contrôle multiple, ou parallèle). Tous les threads associés à un
processus lourd ont en commun un certain nombre de ressources, telles que: une partie du code à
exécuter, une partie des données, des fichiers ouverts et des signaux.
En Java, le processus lourd sera la JVM (Java Virtual Machine) qui interprète le bytecode
des différents processus légers. Les threads coopèrent entre eux en échangeant des valeurs par la
mémoire commune (du processus lourd). L'intérêt d'un système ``multi-threadé'', même sur une
machine monoprocesseur, est que l'ordinateur donne l'impression d'effectuer plusieurs tâches en
parallèle. Les systèmes d'exploitation modernes (Linux, Windows XP, MacOS X etc.) sont tous
multi-threadés contrairement aux premiers OS de micro-ordinateurs. Le fait de pouvoir ouvrir en
même temps netscape, emacs, et un shell par exemple, et de pouvoir passer d'une fenêtre à l'autre
sans attente est la marque d'un tel système. L'application netscape même est multi-threadée. Une
tâche essaie de se connecter au site choisi, pendant qu'une autre imprime à l'écran etc. Imaginez
ce que ce serait si vous ne voyiez rien à l'écran tant que le site auquel vous vous connectez n'a pas
fini de vous transmettre toutes les données! En fait, un système d'exploitation comme Unix
comporte des processus (lourds) multiples, tels les démons systèmes (ou processus noyau) et
souvent un grand nombre de processus (lourds) utilisateurs. Il n'est pas rare d'avoir quelques
dizaines voire une centaine de processus lourds sur une machine à tout instant (faire ps -al par
exemple).
Sur une machine multiprocesseur, des threads peuvent être exécutés sur plusieurs processeurs
donc réellement en même temps, quand le système d'exploitation et le support pour les threads
sont étudiés pour (c'est le cas pour Windows NT, Solaris 2, Linux etc.). Pour rentrer un peu plus
dans les détails, les threads que nous programmerons sont des ``threads utilisateurs'' qui doivent
communiquer avec le noyau de système d'exploitation de temps en temps, ne serait-ce que pour
imprimer à l'écran, lire et écrire des fichiers etc. Tout thread utilisateur doit donc être lié d'une
façon ou d'une autre à un thread ``noyau''. Selon les implémentations, chaque tâche utilisateur
peut être liée à une tâche noyau, ou plusieurs tâches utilisateur à plusieurs tâches noyaux, ou
encore plusieurs tâches utilisateur à une tâche noyau. La première version de Solaris (``green
threads'') implémentait seulement la dernière possibilité qui est la seule qui ne permet pas de
bénéficier de vrai parallélisme sur une architecture multiprocesseur. A partir de Java 1.1 et pour
les versions plus récentes de Solaris et de Linux, on est dans le deuxième cas, qui offre le plus de
flexibilité et de performances.
Les threads (ou processus légers) sont définis dans le langage JAVA, et ne sont pas
comme en C ou C++, une extension que l'on peut trouver dans différentes bibliothèques.
Création
Une fois créé, on peut configurer Proc, par exemple lui associer une priorité . On pourra
ensuite l'exécuter en invoquant sa méthode start. Cette méthode va à son tour invoquer la
méthode run du thread. Comme la méthode run de la classe Thread ne fait rien, il faut la
surcharger. C'est possible par exemple si on définit Proc comme une instance d'une sous-classe
de Thread, dans laquelle on redéfinit la méthode run.
En général on a envie qu'un processus contienne des données locales, donc il est vraiment
naturel de définir un processus comme une instance d'une sous-classe de Thread en général,
Un peu plus utile maintenant: on peut recueillir un certain nombre d'informations sur les
threads présents à l'exécution. Par exemple, static Thread currentThread() renvoie la référence au
thread courant c'est-à-dire celui qui exécute currentThread(). La méthode int enumerate(Thread[]
threadArray) place tous les threads existants (y compris le main() mais pas le thread ramasse
miettes) dans le tableau threadArray et renvoie leur nombre. static int activeCount() renvoie le
nombre de threads actifs (on définira mieux ce que cela peut être aux sections suivantes).
Voici un petit exemple d'utilisation
Compte3(int val) {
valeur = val;
}
% java Compte3
1 2000 Le thread 0 est main
Le thread 1 est Thread-2
Le thread 2 est Thread-3
1 2000 1 2000 1 2000 1 2000 2000
1 1 2000 2000 1 1 2000 2000 1 1
2000 2000 1 1 2000 2000 1 1 2000
2000 1 1 2000 2000 1 1 ^C
%
3- LES SOCKETS
Le terme programmation de socket fait référence à l'écriture de programmes qui
s'exécutent sur plusieurs ordinateurs dans lesquels les périphériques sont tous connectés les uns
aux autres à l'aide d'un réseau.
Il existe deux protocoles de communication que nous pouvons utiliser pour la
programmation des sockets : User Datagram Protocol (UDP) et Transfer Control Protocol
(TCP) .
La principale différence entre les deux est que UDP est sans connexion, ce qui signifie
qu'il n'y a pas de session entre le client et le serveur, tandis que TCP est orienté connexion, ce qui
signifie qu'une connexion exclusive doit d'abord être établie entre le client et le serveur pour que
la communication ait lieu. .
Ce didacticiel présente une introduction à la programmation de sockets sur des
réseaux TCP/IP et montre comment écrire des applications client/serveur en Java. UDP n'est pas
un protocole courant et, en tant que tel, peut ne pas être rencontré souvent.
Configuration du projet
Java fournit une collection de classes et d'interfaces qui prennent en charge les détails de
communication de bas niveau entre le client et le serveur.
Ceux-ci sont principalement contenus dans le package [Link] , nous devons donc
effectuer l'importation suivante :
import [Link].*;Copie
Nous avons également besoin du package [Link] , qui nous donne des flux d'entrée et
de sortie pour écrire et lire tout en communiquant :
import [Link].*;Copie
Par souci de simplicité, nous exécuterons nos programmes client et serveur sur le même
ordinateur. Si nous devions les exécuter sur différents ordinateurs en réseau, la seule chose qui
changerait serait l'adresse IP. Dans ce cas, nous utiliserons localhost sur [Link] .
Exemple simple
Mettons-nous la main à la pâte avec les exemples les plus élémentaires impliquant un
client et un serveur . Ce sera une application de communication bidirectionnelle où le client
salue le serveur et le serveur répond.
Nous allons créer l'application serveur dans une classe appelée [Link] avec le
code suivant.
Nous inclurons la méthode principale et les variables globales pour attirer l'attention sur
la façon dont nous allons exécuter tous les serveurs dans cet article. Pour le reste des exemples de
cet article, nous omettons ce type de code répétitif :
Les annotations sont utilisées dans des domaines divers. Leur intérêt principal est de
fournir une méta-information qui pourra être exploitée par un programme.
Les annotations sont définies en Java à l'aide du mot-clé "@". Par exemple, @Deprecated
est une annotation qui indique que l'élément de code auquel elle est appliquée est obsolète et qu'il
ne devrait pas être utilisé dans un code de production.
Les annotations en Java peuvent être définies soit par le programmeur, soit par des
frameworks tiers. Les annotations sont également utilisées dans le cadre de certaines API Java
standard, telles que JPA (Java Persistence API) et JAX-RS (Java API for RESTful Web Services).
Les annotations en Java peuvent être utilisées à la fois pour la documentation du code et
pour la génération automatique de code. Les outils de génération de code, tels que les IDE,
peuvent utiliser les annotations pour générer du code supplémentaire ou pour augmenter la
productivité du développeur
Une annotation est un type (comme une classe ou une interface) du langage Java : elle
peut être référencée par son nom complet ou importée depuis un autre paquet grâce au mot-
clé import.
Une annotation n’est pas instanciée, elle est accolée à l’élément qu’elle vient enrichir :
package [Link];
@Override
public String toString() {
return "une voiture";
}
}
L’annotation Override est définie dans le package [Link] (c’est pour cela qu’il n’est pas
nécessaire de l’importer explicitement). Cette annotation est utilisable uniquement sur les
méthodes pour indiquer que la méthode est une redéfinition d’une méthode d’une classe parente
(dans l’exemple précédent, la méthode redéfinit [Link]). Cette annotation est exploitée
par le compilateur pour réaliser des vérifications supplémentaires.
Exemple d’annotation :
- Deprecated
Permet de générer des warnings afin d’informer les autres développeurs que quelque chose (une
classe, une méthode…) a été dépréciée et ne devrait plus être utilisée.
- FunctionalInterface
Permet au compilateur de s’assurer que l’interface qui porte cette annotation peut être
implémentée par un lambda (Cf. le chapitre sur les lambdas).
- Override
Signale qu’une méthode est une redéfinition d’une méthode déclarée dans une classe parente.
Cela permet au compilateur de signaler une erreur si ce n’est pas le cas.
- SuppressWarnings
Cette annotation s’ajoute à une méthode acceptant un paramètre variable (varargs) dont le
type est un générique. En effet, le principe de l’effacement de type (type erasure) dans la gestion
des classes génériques fait qu’il est possible de corrompre un type paramétré utilisé comme
paramètre variable sans que le compilateur et la JVM ne puissent le détecter. Pour pallier à ce
problème, le compilateur produit systématiquement un avertissement lorsqu’on utilise un type
générique comme paramètre variable. Cette annotation permet de supprimer l’avertissement à la
compilation et implique que le développeur s’est assuré que son implémentation est sûre.
L’API standard de Java (mais également des bibliothèques tierces) fournissent beaucoup
d’autres annotations qui ne sont pas interprétées par le compilateur mais par le programme lui-
même à l’exécution.
Déclaration d’une annotation
Comme pour les classes, les interfaces et les énumérations, on crée une annotation dans
un fichier portant le même nom que l’annotation avec l’extension .java. On déclare une
annotation avec le mot-clé @interface.
package [Link];
}
La déclaration des attributs d’une annotation est la suivante :
package [Link];
un type primitif,
une chaîne de caractères ([Link]),
une référence de classe ([Link]),
une Annotation ([Link]),
une énumération,
un tableau à une dimension d’un de ces types.
La déclaration d’une annotation peut elle-même être annotée par :
- Documented
Pour indiquer si l’annotation doit apparaître dans la documentation générée par un outil
comme javadoc.
- Inherited
Pour indiquer que l’annotation doit être héritée par la classe fille.
- Retention
Pour indiquer quels types d’éléments peuvent utiliser l’annotation : classe, méthode, attribut…
- Repeatable
Pour indiquer qu’une annotation peut être déclarée plusieurs fois sur un même élément.
2- LA REFLEXION
La réflexion en Java désigne la capacité d'un programme à examiner et manipuler à
runtime (au moment de l'exécution) les classes, les méthodes, les attributs, etc. d'un programme.
Elle permet d'obtenir des informations sur une classe (nom, champs, méthodes, annotations, etc.),
de créer des instances de classe dynamiquement, d'appeler des méthodes en utilisant des noms ou
des chaînes de caractères, ou encore de modifier des membres privés d'une classe.
Pour utiliser la réflexion en Java, il faut utiliser la classe Class, qui permet de représenter
une classe en Java. Cette classe propose des méthodes pour accéder aux informations relatives à
une classe (getDeclaredFields(), getDeclaredMethods(), getAnnotations(), etc.). On peut
également utiliser la classe Method pour exécuter une méthode dynamiquement, ou encore la
classe Constructor pour créer une instance de classe.
Class<?> c = [Link]("[Link]");
Si la classe n'a pas de constructeur sans paramètres, on peut d'abord obtenir le constructeur, puis
l'appeler. Par exemple :
Class<?> c = [Link]("[Link]");
3- LES AGENTS
Les agents en Java sont des programmes autonomes qui utilisent la plate-forme Java pour
effectuer diverses tâches. Ils sont souvent utilisés dans les systèmes distribués pour faciliter la
communication et la coordination entre les différents nœuds du réseau.
Les agents Java sont souvent utilisés dans les applications d'intelligence artificielle, de
gestion de connaissances, de surveillance et de sécurité, de contrôle de processus industriels, et
d'optimisation de chaînes logistiques. Les agents peuvent être exécutés sur des ordinateurs locaux
ou distants, et peuvent être programmés pour interagir avec des robots, des capteurs, des bases de
données, des services web, ou tout autre système connecté au réseau.
Les avantages des agents Java incluent leur modularité, leur adaptabilité, leur résilience,
leur interopérabilité, et leur capacité à traiter de grandes quantités de données en temps réel.
Cependant, ils peuvent être plus complexes à développer et à maintenir que d'autres types de
programmes, et nécessitent souvent des compétences spécialisées en intelligence artificielle et en
ingénierie logicielle
Classe d’agent
La classe d’agent d’instrumentation avec la méthode premain est utilisée pour récupérer les
informations dont nous avons besoin. L’implémentation de l’interface Instrumentation est passée
à la méthode premain. Nous utilisons la méthode getObjectSize définie par l’interface
d’instrumentation pour obtenir l’utilisation de la mémoire de l’objet Main au moment de
l’exécution.
package [Link];
import [Link];
public class MyAgentClass {
public static void premain(String agentArgs,
Instrumentation inst) {
[Link]([Link]
(new Main()))
}
}
Déploiement d’agents Java
Une fois qu’un agent est créé, il est déployé en tant que fichier JAR. L’attribut du fichier
manifeste spécifie la classe d’agent qui sera chargée pour démarrer l’agent. Notez qu’il existe de
nombreuses façons de démarrer un agent : en utilisant la ligne de commande, au moment de
l’exécution ou en tant qu’exécutables JAR. Nous allons utiliser la ligne de commande ici.
1- SPRING
Spring est un Framework open-source développé par pivotal et est un Framework
d'application et un conteneur d'inversion de contrôle pour la plate-forme Java qui fournit un
support d'infrastructure pour le développement d'applications
Utilité de spring
2- HIBERNATE
Hibernate est un Framework Java qui fournit un mappage objet-relationnel vers un
modèle orienté objet vers la base de données relationnelle. Cela signifie qu’Hibernate fournit des
classes Java aux tables de base de données et fournit également une fonction d'interrogation et de
récupération des données.
Utilité de Hibernate
Le Framework Hibernate fournit une couche d'abstraction qui signifie que les programmeurs
n'ont pas à se soucier de la mise en œuvre. Hibernate implémentera différents modules pour les
développeurs en interne, comme écrire des requêtes pour effectuer des opérations CURD sur la
base de données et établir une connexion à différents types de bases de données. Le Framework
Hibernate est utilisé pour développer une logique de persistance, ce qui signifie stocker et traiter
les données pour une utilisation prolongée. De manière précise, le Framework Hibernate est une
source ouverte pour développer des objets indépendants du logiciel de base de données et créer
une logique de persistance indépendante en Java pour toutes les éditions d'entreprise Java (JEE).
Les deux sont des choix populaires sur le marché ; discutons de certaines des différences
majeures :
3- JUNIT
Exemple de TestCase:
Une telle classe hérite de [Link]. La méthode annotée par @Before est
exécutée avant les méthodes de test, celle précédée par @After est appelée à la fin. De la même
manière, la méthode annotée par @BeforeClass est appelée au lancement du testCase, celle
précédée par @AfterClass est appelée juste avant la fin. Les tests sont des méthodes annotées
par @Test, elles font des traitements et vérifient le bon comportement des classes testées par des
méthodes assert***(), toute assertion non vérifiée est signalée comme défaillante. Un cas de test
(TestCase) peut avoir plusieurs sections @Test. Si une section @Test échoue, le TestCase ne
s'arrête pas mais continue sur les sections @Test suivantes (s'il y en a).
import [Link];
import [Link].*;
@BeforeClass
public static void setUpClass() throws Exception {
}
@AfterClass
public static void tearDownClass() throws Exception {
}
@Before
public void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
}
@Test
public void nomdutest1() {
}
@Test
public void nomdutest2() {
}
}
CONCLUSION
Le Java avancé est un ensemble de fonctionnalités additionnelles, qui vont au-delà des
bases du langage de programmation Java. Il comprend des concepts tels que la programmation
orientée objet, la gestion des threads, la gestion des exceptions, les annotations, les génériques et
les types sauvages, la réflexion, la gestion des flux IO, les expressions lambda, les interfaces
fonctionnelles, les collections, les annotations, les microservices, les frameworks de
développement d'applications web Java comme Spring Framework, Hibernate, etc. Ces concepts
rendent Java un langage de programmation polyvalent pour les développeurs pour le
développement d'applications web, mobiles et serveur.
Java avancée est un véritable défi pour les développeurs Java qui cherchent à se
perfectionner dans le développement Java. Même si le langage Java offre déjà des fonctionnalités
et des outils de développement robustes, le savoir-faire et la connaissance avancés peuvent
augmenter l'efficacité de la programmation et rendre le développement d'applications Java plus
facile, plus efficace et plus sûr.