Skip to the content.

Java

Ressources

Table des matières

Ecosysteme Java

Les trois parties les plus élémentaires de l’écosystème Java sont la machine virtuelle Java (JVM), l’environnement d’exécution Java (JRE) et le kit de développement Java (JDK).

java-ecosystem

Lire encore plus

Lire encore plus

Lire encore plus

javaFlow

POO concepts de base

Classes et Objets

Les différences entre objet et classe est donnée ci-dessous:

Objet Classe
Instance d’une classe La classe est un modèle à partir duquel les objets sont créés
Entité du monde réel telle qu’un stylo, un ordinateur portable, un mobile, un lit, etc. Une classe est un groupe d’objets similaires
Entité physique. Entité logique
Créé plusieurs fois selon les besoins Déclarée une seul fois.
L’objet alloue de la mémoire lorsqu’il est créé La classe n’alloue pas de mémoire lors de sa création.

Classes and Objects

La syntaxe de base pour définir une classe :

class NomClasse {
  [déclaration dattributs]
  [déclaration de methodes]
}

Exemple :

class Car {

  String modele;
  double prix;

  public Car(String modele, double prix){ // Constructeur
    this.modele = modele;  // "this" keyword refers to this class
    this.prix = prix;
  }
}

Pour créé un objet:

  Car objet1 = new Car("R4",3500.69);  // Objet 1
  Car objet2 = new Car("ZX",2200.69);  // Objet 2

Le principe d’encapsulation

Le principe d’encapsulation en poo est un mécanisme permettant d’envelopper les données (attributs) et le code agissant sur les données (méthodes) en une seule unité. Dans l’encapsulation, les variables d’une classe sont cachées aux autres classes et ne sont accessibles que par les méthodes de leur classe actuelle.

Classes and Objects
Figure 1.1 Data Encapsulation

L’encapsulation permet de déclarer des méthodes (services) qui permettent de:

Pour réaliser l’encapsulation en Java:

Exemple:

class Car {

  private String modele;
  private double prix;

  public Car(String modele, double prix){ // Constructeur
    this.modele = modele;  // "this" keyword refers to this class
    this.prix = prix;
  }

  public double getPrix(){
    return prix;
  }

  public String getModele(){
    return modele;
  }

  public void setPrix(double prix){
    this.prix = prix;
  }

  public void setModele(String Modele){
    this.modele = modele;
  }
}

Héritage

La syntaxe de l’héritage en Java:

class Car extends Vehicule {

   //methods et attributs

}

L’héritage permet d’assurer:

En JAVA, chaque classe est autorisée a hériter directement d’une seul classe, et chacune pourra avoir plusieurs classes dérivées.

Héritage
Figure1.2 Héritage en Java

Polymorphisme

Le mot polymorphisme vient du grec et signifie qui peut prendre plusieurs formes. Cette caractéristique est un des concepts essentiels de la programmation orientée objet. Alors que l’héritage concerne les classes (et leur hiérarchie), le polymorphisme est relatif aux méthodes des objets.

On distingue généralement trois types de polymorphisme :

<img src="https://www.askpython.com/wp-content/uploads/2019/12/Polymorphism-in-Python.png" width="500" " alt="Polymorphisme" >
Figure1.3 Polymorphisme d'héritage

Package

Classes abstraites et interfaces

Abstraction

L’abstraction est un concept de la programmation orientée objet qui ne montre que les attributs essentiels et cache les informations inutiles. L’objectif principal de l’abstraction est de cacher les détails inutiles aux utilisateurs. L’abstraction consiste à sélectionner des données dans un ensemble plus vaste pour ne montrer à l’utilisateur que les détails pertinents de l’objet. Elle permet de réduire la complexité et les efforts de programmation. C’est l’un des concepts les plus importants de la POO.

Classe abstraite et méthode abstraite

Un exemple d’une classe abstraite en JAVA:

abstract class NomClasse {
  abstract methode1(){}
  public void methode2(){
    // Implementation
  }
}

Interface

Pour déclarer une interface en JAVA:

interface NomInterface {
  // Corps de l'interface qui contient des variables statiques et/ou methodes abstraites
}

Les avantages de l’abstraction

Les Exceptions

Définition : Une exception est un événement qui se produit pendant l’exécution d’un programme et qui perturbe le déroulement normal des instructions, du coup le programme/l’application se termine de manière anormale, ce qui n’est pas recommandé, c’est pourquoi ces exceptions doivent être traitées.

Une exception peut se produire pour de nombreuses raisons différentes. Voici quelques scénarios dans lesquels une exception se produit:

Certaines de ces exceptions sont dues à une erreur de l’utilisateur, d’autres à une erreur du programmeur, et d’autres encore à des ressources physiques qui ont connu une défaillance d’une manière ou d’une autre.

On distingue 3 types d’ Exceptions:

<img src="https://5balloons.info/wp-content/uploads/2017/07/exceptions1.jpg" width="500" " alt="Exceptions" >
Figure2.1 Hierarchy des exceptions

Lors de la détection d’une erreur, un objet qui hérite de la classe Exception est créé (on dit qu’une exception est levée) et propagé à travers la pile d’exécution jusqu’à ce qu’il soit traité.

Pour traiter ces exceptions on utilise le bloc try/catch:

try{
  // instructions susceptibles de produire des erreurs ou des exceptions
} catch(NomExcepion e) {
  // Traiter l'excetion
}
// Reste du code est executé

On peut également ajouter autant de block catch qu’on souhaite:

try{
  // instructions susceptibles de produire des erreurs ou des exceptions
} catch(NomExcepion1 e1) {
  // Traiter l'excetion
} catch(NomExcepion2 e2) {
  // Traiter l'excetion
} catch(NomExcepion3 e3) {
  // Traiter l'excetion
}
// Reste du code est executé

Extension:

try{
  // instructions susceptibles de produire des erreurs ou des exceptions
} catch(NomExcepion e) {
  // Traitement de l'excetion
}finally{
  // après que l'exception est intercépté dans le bloc catch le block finally est executé
}
// Reste du code est executé

Lire encore plus

Généricité

En Programmation Orientée Object (POO), la généricité est un concept permettant de définir des algorithmes (types de données et méthodes) identiques qui peuvent être utilisés sur de multiples types de données. Cela permet donc de réduire les quantités de codes à produire.

Les méthodes génériques et les classes génériques en Java permettent aux programmeurs de spécifier, avec une seule déclaration de méthode, un ensemble de méthodes liés, ou avec une seule déclaration de classe, un ensemble de types liés, respectivement.

La généricité permettra d’avoir un code plus fortement typé ce qui permet aux programmeurs de détecter les erruers (types invalides) au moment de la compilation.

Exemples d’utilisation

ArrayList<String> arrStr = new ArrayList<>(); // Créer un tableau de chaine de caractère
class PairValue<T> {

  //Variable d'instance
  private T valeur1, valeur2;

  //Constructeur avec paramètre inconnu pour l'instant
  public PairValue(T val1, T val2){
    this.valeur1 = val1;
    this.valeur2 = val2;
  }
  public void setValue1(T val){
    this.valeur1 = val;
  }
  public void setValue2(T val){
    this.valeur2 = val;
  }
  public T getValue1(){
    return this.valeur1;
  }
  public T getValue2(){
    return this.valeur2;
  }

}
class PairValue<T, S> {

  //Variable d'instance
  private T valeur1,
  private S valeur2;

  //Constructeur avec paramètre inconnu pour l'instant
  public PairValue(T val1, S val2){
    this.valeur1 = val1;
    this.valeur2 = val2;
  }
  public void setValue1(T val){
    this.valeur1 = val;
  }
  public void setValue2(S val){
    this.valeur2 = val;
  }
  public T getValue1(){
    return this.valeur1;
  }
  public S getValue2(){
    return this.valeur2;
  }

}
public static void main(String[] args) {
    List<Voiture> listVoiture = new ArrayList<Voiture>();
    List<VoitureSansPermis> listVoitureSP = new ArrayList<VoitureSansPermis>();

    listVoiture = listVoitureSP;   //Interdit !
}
ArrayList<?> list; // list qui accepte n'importe quel objet
List<? extends Voiture> listVoitureX; // list qui accepte n'importe quelle classe qui hérite de la classe Voiture

Lire encore plus

Collections

Definition

En Java une collection est simplement un objet qui regroupe plusieurs éléments en une seule unité. Les collections sont utilisées pour stocker, récupérer, manipuler et communiquer des données. En général, elles représentent des éléments de données qui forment un groupe homogène, comme une main de poker (une collection de cartes) ou un annuaire téléphonique (une correspondance entre des noms et des numéros de téléphone).

Un Framework de collections

Une architecture unifiée pour représenter et manipuler des collections. Tous les frameworks de collections contiennent les éléments suivants :

L’architecture du framework Collections de java


Figure4.1 Hierarchy du framework Collections


Iterator iterator();


Exemple d’implementation :

List<String> list = new ArrayList<>();


Exemple d’implementation :

Set<String> hash_Set = new HashSet<String>();

Gestion des Flux

Une entrée/sortie en Java consiste en un échange de données entre le programme et une autre source, par exemple la mémoire, un fichier, le programme lui-même… Pour réaliser cela, Java emploie ce qu’on appelle un stream (qui signifie « flux»). Celui-ci joue le rôle de médiateur entre la source des données et sa destination. Java met à notre disposition toute une panoplie d’objets permettant de communiquer de la sorte. Toute opération sur les entrées/sorties doit suivre le schéma suivant : ouverture, lecture/ecriture, fermeture du flux


Figure 5.1 Java Streams

Java a décomposé les objets traitant des flux en deux catégories :

Types de flux

Selon les données qu’un flux contient, il peut être classé en:

InputStream

Cette classe abstraite est la superclasse de toutes les classes représentant un flux d’octets d’entrée.

Afin d’utiliser les fonctionnalités de InputStream, nous pouvons utiliser ses sous-classes. Certains d’entre eux sont:


Figure 5.2 Sous-classes de InputStream

Methods of InputStream

La classe InputStream fournit différentes méthodes qui sont implémentées par ses sous-classes. Voici quelques-unes des méthodes couramment utilisées:

OutputStream

La classe OutputStream du package java.io est une superclasse abstraite qui représente un flux de sortie d’octets.

Afin d’utiliser les fonctionnalités de OutputStream, nous pouvons utiliser ses sous-classes. Certains d’entre eux sont:


Figure 5.2 Sous-classes de OutputStream

Methods of OutputStream

La classe InputStream fournit différentes méthodes qui sont implémentées par ses sous-classes. Voici quelques-unes des méthodes couramment utilisées:

Sockets

Une socket est un point d’extrémité d’une liaison de communication bidirectionnelle entre deux programmes exécutés sur le réseau. Le package java.net fournit deux classes - Socket et ServerSocket - qui implémentent respectivement le côté client de la connexion et le côté serveur de la connexion.


Figure 5.3 Socket

La classe ServerSocket est utilisée côté serveur : elle attend simplement les appels du ou des clients. C’est un objet du type Socket qui prend en charge la transmission des données.

Cette classe représente la partie serveur du socket. Un objet de cette classe est associé à un port sur lequel il va attendre les connexions d’un client. Généralement, à l’arrivée d’une demande de connexion, un thread est lancé pour assurer le dialogue avec le client sans bloquer les connexions des autres clients.

Le mise en oeuvre de la classe ServerSocket suit toujours la même logique :

Exemple de ServerSocket

import java.io.* ;
import java.net.* ;
public class Serveur {

  public static void main (String args[]) throws IOException {

    int port=1000;
    ServerSocket sersoc = new ServerSocket (port) ;
    System.out.println ("serveur active sur port " + port) ;
    while (true){
      Socket soc = sersoc.accept();
      InputStream flux = soc.getInputStream ();
      BufferedReader entree = new BufferedReader (new InputStreamReader (flux)) ;
      String message = entree.readLine() ;
      System.out.println("message reçu sur le serveur = " + message);

    }
  }
}

Les sockets implémentent le protocole TCP (Transmission Control Protocol). La classe Socket contient les méthodes de création des flux d’entrée et de sortie correspondants. Les sockets constituent la base des communications par le réseau.

Comme les flux Java sont transformés en format TCP/IP, il est possible de communiquer avec l’ensemble des ordinateurs qui utilisent ce même protocole. La seule condition importante au niveau du système d’exploitation est qu’il soit capable de gérer ce protocole.

Cette classe encapsule la connexion à une machine distante par le réseau. Elle gère la connexion, l’envoi de données, la réception de données et la déconnexion.

Le mise en oeuvre de la classe Socket suit toujours la même logique :

Exemple de Socket

import java.net.* ;
import java.io.* ;
public class Client{

  public static void main (String args[]) throws IOException{

    String hote = "127.0.0.1" ;
    int port = 1000 ;
    Socket soc = new Socket (hote, port) ;
    OutputStream flux = soc.getOutputStream() ;
    OutputStreamWriter sortie = new OutputStreamWriter (flux) ;
    sortie.write("message envoye au serveur\n") ;
    sortie.flush(); // pour forcer l'envoi de la ligne

  }
}

Les Threads

Dans les deux cas, cela n’a pas d’impact sur le code qui reste le même.

Interface Runnable

import java.io.*;
import java.lang.*;

public class MonTraitement implements Runnable {
  public void run() {
    int i = 0;
    for (i = 0; i > 10; i++) {
      System.out.println("" + i);
    }
  }
}

Classe Thread

Le cycle de vie d’un thread

Un thread, encapsulé dans une instance de type classe Thread, suit un cycle de vie qui peut prendre différents états.

Le statut du thread est encapsulé dans l’énumération Thread.State

Valeur Description
NEW Le thread n’est pas encore démarré. Aucune ressource système ne lui est encore affectée. Seules les méthodes de changement de statut du thread start() et stop() peuvent être invoquées
RUNNABLE Le thread est en cours d’exécution : sa méthode start() a été invoquée
BLOCKED Le thread est en attente de l’obtention d’un moniteur qui est déjà détenu par un autre thread
WAITING Le thread est en attente d’une action d’un autre thread ou que la durée précisée en paramètre de la méthode sleep() soit atteinte.

Chaque situation d’attente ne possède qu’une seule condition pour retourner au statut Runnable :
- si la méthode sleep() a été invoquée alors le thread ne retournera à l’état Runnable que lorsque le délai précisé en paramètre de la méthode a été atteint
- si la méthode suspend() a été invoquée alors le thread ne retournera à l’état Runnable que lorsque la méthode resume sera invoquée
- si la méthode wait() d’un objet a été invoquée alors le thread ne retournera à l’état Runnable que lorsque la méthode notify() ou notifyAll() de l’objet sera invoquée
- si le thread est en attente à cause d’un accès I/O alors le thread ne retournera à l’état Runnable que lorsque cet accès sera terminé
TIMED_WAITING Le thread est en attente pendent un certain temps d’une action d’un autre thread. Le thread retournera à l’état Runnable lorsque cette action survient ou lorsque le délai d’attente est atteint
TERMINATED Le thread a terminé son exécution. La fin d’un thread peut survenir de deux manières :
- la fin des traitements est atteinte
- une exception est levée durant l’exécution de ses traitements

La création d’un thread

Il existe plusieurs façons de créer un thread :

public class TestThread {

  public static void main(String[] args) {
    Thread t = new Thread() {
      public void run() {
        System.out.println("Mon traitement");
      }
    };
    t.start();
  }
}
public class MonThread extends Thread {

  @Override
  public void run() {
    System.out.println("Mon traitement");
  }
}

public class TestThread {

  public static void main(String[] args) {
    MonThread t = new MonThread();
    t.start();
 }
}

public class MonTraitement implements Runnable {

  @Override
  public void run(){
    System.out.println("Mon traitement");
  }
}

public class TestThread {

  public static void main(String[] args){
    Thread thread = new Thread(new MonTraitement());
    thread.start();
  }
}

Il est préférable d’utiliser l’implémentation de Runnable car :

Il ne faut surtout pas invoquer la méthode run() d’un thread. Dans ce cas, les traitements seront exécutés dans le thread courant mais ne seront pas exécutés dans un thread dédié.

L’arrêt d’un thread

Par défaut, l’exécution d’un thread s’arrête pour deux raisons :

Historiquement la classe Thread possède une méthode stop() qui est déclarée deprecated depuis Java 1.1 et est conservée pour des raisons de compatibilité mais elle ne doit pas être utilisée car son comportement peut être aléatoire et inattendu.

La méthode stop() lève une exception de type ThreadDeath se qui interrompt brutalement les traitements du thread. C’est notamment le cas si un moniteur est posé : celui-ci sera libéré mais l’état des données pourrait être inconsistant.

Pour permettre une interruption des traitements d’un thread, il faut écrire du code qui utilise une boucle tant qu’une condition est remplie : le plus simple est d’utiliser un booléen.

Exemple :

public class MonThread extends Thread {

  private volatile boolean running = true;

  public void arreter() {
    this.running = false;
  }

  @Override
  public void run() {
    while (running) {
      // traitement du thread
      try {
        Thread.sleep(500);
      } catch (InterruptedException ex) {
        ex.printStackTrace();
      }
    }
  }
}

Le Java Memory Model permet à un thread de conserver une copie local de ses champs : pour une exécution correcte, il faut utiliser le mot clé volatile sur le booléen pour garantir que l’accès à la valeur se fera de et vers la mémoire.

Une fois un thread terminé, il passe à l’état terminated. Il ne peut plus être relancé sans lever une exception de type IllegalStateException. Pour le relancer, il faut créer une nouvelle instance.

Threads démons

public class TestThreaddemon {

  public static void main(String[] args) {
    Thread daemonThread = new Thread(new Runnable() {
      @Override
      public void run() {
        try {
          while (true) {
            System.out.println("Execution demon");
          }
        } finally {
          System.out.println("Fin demon");
        }
      }
    }, "Demon");

    daemonThread.setDaemon(true);
    daemonThread.start();
  }
}

Le nombre de messages affichés varie de un à quelques uns avant l’arrêt de la JVM. Le message du bloc finally n’est jamais affiché.

Suspendre un Thread

La méthode static sleep() de la classe Thread permet de mettre en sommeil le thread courant pour le délai en millisecondes dont la valeur est fournie en paramètre.

Elle est bloquante, elle lève une exception de type InterruptedException au cours de son exécution si un autre thread demande l’interruption de l’exécution du thread.

try {
  Thread.sleep(5000);
} catch (InterruptedException e) {
  e.printStackTrace();
}

La méthode ` sleep()` est static : elle ne s’applique que sur le thread courant et il n’est pas possible de désigner le thread concerné.

Une surcharge de la méthode ` sleep()` attend en paramètre la durée en millisecondes et une durée supplémentaire en nanosecondes qui peut varier entre 0 et 999999. La précision de cette attente supplémentaire est dépendante de la machine et du système d’exploitation.

Contrairement à la méthode wait() de la classe Object, la méthode sleep() ne libère pas les verrous qui sont posés par le thread.

L’attente de la fin de l’exécution d’un thread

La méthode join() de la classe Thread permet d’attendre la fin de l’exécution du thread. Elle peut lever une exception de type InterruptedException.

Une surcharge de la méthode join() attend en paramètre un entier long qui définit la valeur en millisecondes d’un délai d’attente maximum.

La modification de la priorité d’un thread

Un thread possède une propriété qui précise sa priorité d’exécution. Pour déterminer ou modifier la priorité d’un thread, la classe Thread contient les méthodes suivantes:

Méthode Description
int getPriorty() retourner la priorité d’exécution du thread
int setPriority(int i) modifier la priorité d’exécution du thread

Généralement, la priorité varie de 1 à 10 mais cela dépend de l’implémentation de la JVM. Plusieurs constantes permettent de connaître les valeurs de la plage de priorités utilisables et la valeur de la priorité par défaut :

La valeur par défaut de la priorité lors de la création d’un nouveau thread est celle du thread courant.

La méthode setPriority() lève une exception de type IllegalStateException si la valeur fournie en paramètre n’est pas incluse dans la plage Thread.MIN_PRIORITY et Thread.MAX_PRIORITY.

Thread thread = new Thread();
thread.setPriority(Thread.MAX_PRIORITY);

Laisser aux autres threads plus de chance de s’exécuter

La méthode static yield() de la classe Thread tente de mettre en pause le thread courant pour laisser une chance aux autres threads de s’exécuter.

Attention : il n’y a aucune garantie sur le résultat de l’invocation de la méthode yield() car elle est dépendante de l’implémentation de la JVM.

Synchronisation

class Nombres{

  private int n=0, carre ;

  /* les méthodes calcul et affiche sont mutuellement
  exclusives */

  public synchronized void calcul(){
    n++ ;
    carre = n*n ;
  }
  public synchronized void affiche (){
    System.out.println (n + " a pour carre " + carre) ;
  }
}


class ThrCalc extends Thread{
  // Classe Thread calculé
  private Nombres nomb ;
  public ThrCalc (Nombres nomb) { this.nomb = nomb ;}

  public void run () {
    try{
    while (!interrupted()) {
      nomb.calcul () ;
      sleep (50) ;
    }
    }
    catch (InterruptedException e) {}
  }
}

class ThrAff extends Thread {
  // Classe Thread affiché

  private Nombres nomb ;

  public ThrAff (Nombres nomb){
    this.nomb = nomb ;
  }

  public void run () {
    try {
    while (!interrupted()) { nomb.affiche() ; sleep (75) ; }
    }
    catch (InterruptedException e) {}
  }
}

public class Synchro1{
  public static void main (String args[])
  {
    Scanner s= new Scanner(System.in);
    Nombres nomb = new Nombres() ;
    Thread calc = new ThrCalc (nomb) ;
    Thread aff = new ThrAff (nomb) ;
    System.out.println ("Suite de carres - tapez retour pour
    arreter") ;
    calc.start() ; aff.start() ;
    s.next() ;
    calc.interrupt() ; aff.interrupt();
  }
}

Interblocage

L’attente et la notification

Lien utile

Accès aux Bases de Données

JDBC

JDBC est une API qui permet à une application java d’accéder à n’importe quelle base de données relationnelle: Oracle, SQL Server, MySQL…

L’API JDBC a été développée de manière à pouvoir se connecter à n’importe quelle base de données avec la même syntaxe. Cette API est dite indépendante du SGBD utilisé.

Les classes JDBC font partie du package java.sql et javax.sql

Il permet essentiellement de:

Pilote de bases de données ou driver JDBC

Architecture JDBC

JDBC fonctionne selon les deux modèles suivants :

Dans le modèle 2-tier, une application JAVA (ou une applet) dialogue avec le SGBD par l’intermédiaire du pilote JDBC. L’application JAVA et le pilote JDBC s’exécutent sur l’ordinateur client tandis que le SGBD est placé sur un serveur.

Dans le modèle three-tier, l’applet (ou l’application JAVA) ne dialogue plus directement avec un SGBD : un middle-tier fait le lien entre ces deux composants

Le SGBD exécute les requêtes SQL et envoie les résultats au middle tier. Ces résultats sont ensuite communiqués à l’applet sous forme d’appels http.

Classe de l’API JDBC

Plusieurs classes et interfaces sont fournies par JDBC :

Processus d’interrogation d’une BD

Tout programme JDBC fonctionne selon les étapes suivantes:

  1. Importer les packages nécessaires
  2. Connexion à la base de données
    • Chargement du pilote de la BDD
    • Demande de connexion: s’identifiant auprès du SGBD et en précisant la base utilisée
  3. Traitement des commandes SQL
  4. Traitement des résultats (Objet ResultSet)
  5. Fermeture de la connexion.

Applications

import java.sql.*;
public class FirstExample {

   static final String DB_URL = "jdbc:mysql://localhost/my_database";
   static final String USER = "root";
   static final String PASS = "";
   static final String QUERY = "SELECT id, first, last, age FROM Employees";

   public static void main(String[] args) {
      // Open a connection
      try{
         Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
         Statement stmt = conn.createStatement();
         ResultSet rs = stmt.executeQuery(QUERY);
         // Extract data from result set
         while (rs.next()) {
            // Retrieve by column name
            System.out.print("ID: " + rs.getInt("id"));
            System.out.print(", Age: " + rs.getInt("age"));
            System.out.print(", First: " + rs.getString("first"));
            System.out.println(", Last: " + rs.getString("last"));
         }
      }
      } catch (SQLException e) {
         e.printStackTrace();
   }
}
import java.sql.*;
public class FirstExample {

   static final String DB_URL = "jdbc:mysql://localhost/my_database";
   static final String USER = "root";
   static final String PASS = "";
   static final String QUERY = "Insert into Emp values(?,?,?,?)";

   public static void main(String[] args) {
      // Open a connection
      try {
         Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
         PreparedStatement stmt= conn.prepareStatement(QUERY);
         stmt.setInt(1,101);
         stmt.setFloat(2,22.5);
         stmt.setString(3,"Jonas");
         stmt.setString(4,"Tesla");
      } catch (SQLException e) {
         e.printStackTrace();
      }finally{
        conn.close();
      }

   }
}

–>