Aller au contenu

Tour du langage Java

Nous allons faire un tour du langage Java sans forcément tout couvrir car il est très riche en fonctionnalités.

Warning

Comme le langage Java évolue apporte fréquemment des amélioration et simplifications, il se peut que les exemples de code vues ici soient différents de ce que vous trouvez dans la littérature.

Premiers pas

Hello world
///usr/bin/env jbang "$0" "$@" ; exit $?

import static java.lang.System.*;

public class helloworld {

  // main est appelé un point d'entrée
  public static void main(String... args) {
    out.println("Hello World");
  }
}
Premières instructions
///usr/bin/env jbang "$0" "$@" ; exit $?

import static java.lang.System.*;

class Calculator {
  public int a;
  public int b;
  static double PI = 3.14;

  public double add() {
    return a + b + Calculator.PI;
  }

  static double multiply(int x, int y) {
    return x * y * PI;
  }
}

class Calculator2 {
  public int a;
  public int b;

  public int add() {
    return a + b;
  }
}

public class hello {

  public static void main(String... args) {
    out.println("Hello World");
    // typage explicite (on donne le nom du type)
    int i = 10;
    long j = 1_000_000;
    // var permet de faire du typage implicite (le type est déduit par Java)
    var message = "hello";
    message = "world";

    // Instruction illégale car java fait du typage statique
    // -> i est un entier et ne peut pas changer en String
    // L'opposé de typage statique est typage dynamique
    // i = "Hello";
    // message = 10;

    var c = new Calculator();
    c.add();

    Calculator.multiply(10, 1);
  }
}
Opérations de base
///usr/bin/env jbang "$0" "$@" ; exit $?

import static java.lang.System.*;

public class operators {

  public static void main(String... args) {
    // unary operator: takes one operand and return a value
    System.out.println(+3);
    int x = 55;
    System.out.println(-x);
    // Binary operator: takes two operands and returns a value
    System.out.println(5 * 4);
    System.out.println(-x / 4);
    // % is the remainder of the division
    System.out.println(9 % 2);
    // Binary operators for comparison: >, ==, !=, <, >=, <= takes two comparable
    // values (numbers and booleans in Java)
    System.out.println(x > 10);
    // Unary operator for boolean algebra: !
    boolean comparison = x % 5 == 0;
    System.out.println(!comparison);
  }
}
Types primitifs
///usr/bin/env jbang "$0" "$@" ; exit $?

public class BaseTypesDemo {
  public BaseTypesDemo() {
    System.out.println("Tour demo constructor");
  }

  public void showSomeVariables() {
    System.out.println("some vars");
    byte b = 10; // de -127 à 128. 1 octet
    short s = 2; // -32,768 à 32,767. 2 octets
    int i = 10; // -2^(31) à 2^(31)-1. 4 Octets (32 bits)
    long l = 100_000_000; // -2^(63) à 2^(63) - 1. 8 octets (64 bits)
    System.out.println(b);
    String message = "I love Java";
    String text = String.format("s = %d, i = %d, l = %d, message = %s", s, i, l, message);
    System.out.println(text);

    // nombre à virgule flottante. Attention à la précision !
    float x = 10.1f;
    double y = 10.1;
    System.out.println(String.format("x: %.2f, y: %f", x, y));
    System.out.println("x: " + x + ", y: " + y);
  }

  public String getMessage() {
    return "Hello";
  }

  public static void main(String[] args) {
    System.out.println("Base types demo");
    BaseTypesDemo baseTypesDemo = new BaseTypesDemo();
    baseTypesDemo.showSomeVariables();
  }
}
Lire au clavier
///usr/bin/env jbang "$0" "$@" ; exit $?

import java.util.Scanner;

public class ScannerDemo {

  public static void main(String... args) {
    Scanner scanner = new Scanner(System.in);
    int p1 = scanner.nextInt();
    System.out.println(p1);
    if (p1 == 10) {
      System.out.println("Is 10");
    } else {
      System.out.println("Is not 10");
    }
    float p2 = scanner.nextFloat();
    System.out.println(p2);
    String input = scanner.next();
    if (input.equals("Java")) {
      System.out.println("I love Java");
    } else {
      System.out.println(String.format("You love %s", input));
    }
    // Fermer le scanner dès qu'on n'en n'a plus besoin
    scanner.close();
  }
}
Nombres aléatoires
///usr/bin/env jbang "$0" "$@" ; exit $?

import java.util.random.RandomGenerator;

public class RandomDemo {

  public static void main(String... args) {
    RandomGenerator randomGenerator = RandomGenerator.getDefault();
    // i++ -> i = i + 1 -> i += 1
    for (int i = 0; i < 10; i++) {
      int randomInt = randomGenerator.nextInt(1, 21);
      System.out.println(randomInt);
    }

  }
}

Classes, héritage et interfaces

  • En java, une classe ne peut hériter que d'une seule classe (héritage simple) et peut implémenter (terme utilisé quand une classe hérite d'une interface) plusieurs interfaces.
    • Pour info, Python et C++ permettent l'héritage multiple.
  • Une classe qui implémente une interface doit implémenter toutes les méthodes de cette interface.
  • Interface: une classe où il n'y a que les synopsis des méthodes dans leur implémentation.
    • Synopsis (ou la signature): le nom, les arguments et le type de retour de la méthode
  • Les classes modélisent la relation être, tandis que les interfaces modélisent des attributs, des traits ou des caractéristiques.
  • Visibilité des propriétés et méthodes d'une classe:
    • Privé : private : visible uniquement dans la classe
    • Public : public : visible dans tout le projet
    • Protégé : protected : visible dans la classe, les sous-classes et les classes du même package
///usr/bin/env jbang "$0" "$@" ; exit $?

import static java.lang.System.*;

interface Gamer {
  public void play();

  public void takeABreak();
}

interface HungryEater {
  public void eat();
}

class Human {
  private String name;

  public Human() {
    this("anonymous");
  }

  public Human(String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }
}

class HumanGamer extends Human implements Gamer {
  @Override
  public void play() {
    System.out.println("Je joue");
  }

  @Override
  public void takeABreak() {
    System.out.println("Pause");
  }
}

class HungryGamerHuman extends Human implements HungryEater, Gamer {

  @Override
  public void eat() {
    System.out.println("Je mange en étant affamé");
  }

  @Override
  public void play() {
    System.out.println("Je joue mais j'ai faim");
  }

  @Override
  public void takeABreak() {
    System.out.println("Je fais dodo");
  }
}

class Lion implements HungryEater {
  @Override
  public void eat() {
  }
}

class Student extends Human {
  private String idNumbeString;

  public Student(String name, String idNumbeString) {
    super(name);
    this.idNumbeString = idNumbeString;
  }

  public String getIdNumberString() {
    return idNumbeString;
  }

  public void setIdNumberString(String idNumbeString) {
    this.idNumbeString = idNumbeString;
  }
}

public class ClasseInterface {

  static void giveFood(HungryEater eater) {
    System.out.println("miam mia pour un HungryEater");
    eater.eat();
  }

  static void runGame(Gamer gamer) {
    System.out.println("Lancement d'un jeu");
    gamer.play();
  }

  public static void main(String... args) {
    out.println("Hello World");
    HungryGamerHuman hungryGamerHuman = new HungryGamerHuman();
    Lion lion = new Lion();

    giveFood(hungryGamerHuman);
    giveFood(lion);

    runGame(hungryGamerHuman);
  }
}

Les record

  • Un record est une classe qui ne peut pas être étendue et qui ne peut pas avoir de méthodes abstraites.
  • Il est utilisé pour représenter des données immuables.
  • Les méthodes equals, hashCode et toString sont automatiquement générées par le compilateur.
///usr/bin/env jbang "$0" "$@" ; exit $?

import static java.lang.System.*;

class ViennoiserieClass {
  private String name;
  private String texture;
  private int price;

  public ViennoiserieClass(String name, String texture, int price) {
    this.name = name;
    this.texture = texture;
    this.price = price;
  }

  public String getName() {
    return this.name;
  }

  public String getTexture() {
    return texture;
  }

  public int getPrice() {
    return price;
  }

  @Override
  public String toString() {
    return String.format("name: %s, texture %s, price %d", this.getName(), this.getTexture(), this.getPrice());
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((name == null) ? 0 : name.hashCode());
    result = prime * result + ((texture == null) ? 0 : texture.hashCode());
    result = prime * result + price;
    return result;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getClass() != obj.getClass())
      return false;
    ViennoiserieClass other = (ViennoiserieClass) obj;
    if (name == null) {
      if (other.name != null)
        return false;
    } else if (!name.equals(other.name))
      return false;
    if (texture == null) {
      if (other.texture != null)
        return false;
    } else if (!texture.equals(other.texture))
      return false;
    if (price != other.price)
      return false;
    return true;
  }

}

// Equivalent of the above class
record Viennoiserie(String name, String texture, int price) {
}

public class RecordDemo {

  public static void main(String... args) {
    Viennoiserie croissant1 = new Viennoiserie("Croissant", "Feuilletée", 100);
    out.println(croissant1);
    Viennoiserie croissant2 = new Viennoiserie("Croissant", "Feuilletée", 100);
    out.println(croissant1);
    Viennoiserie viennoiserie = new Viennoiserie("Pain au chocolat", "Feuilletée", 100);
    Viennoiserie viennoiserie2 = new Viennoiserie("Chocolatine", "Feuilletée", 100);
    Viennoiserie viennoiserie3 = new Viennoiserie("Petit pain", "Feuilletée", 100);
    out.println(croissant1.equals(croissant2));
    out.println(viennoiserie.equals(viennoiserie2));
    out.println(viennoiserie2.equals(viennoiserie3));

    // remove get from getters
    System.out.println("price of croissant 1: " + croissant1.price());
  }
}

Collections

  • Tableaux : Java propose plusieurs types pour travailler avec des tableaux (ou listes) d'objets. Voici les plus importants:
    • Tableau : Array : tableau de taille fixe.
    • List : List : tableau de taille dynamique (on peut ajouter ou supprimer des éléments). On utilise généralement List en type de variable et ArrayList pour instancier un tableau dynamique.
  • Dictionnaire : Appelé Map en Java, il permet de stocker des paires clé-valeur. Il est possible d'utiliser n'importe quel type d'objet comme clé ou valeur. On utilise généralement Map en type de variable et HashMap pour instancier un dictionnaire.
  • Les listes et les dictionnaires n'acceptent pas les types primitifs (ou les types de base) comme int et double, il faut passer par les types objets (comme Integer ou Double) pour les utiliser dans ces collections.
  • L'itérateur est un moyen alternatif de parcourir les éléments d'une collection sans avoir à gérer les indices.
///usr/bin/env jbang --enable-preview "$0" "$@" ; exit $?

import java.util.*;
import java.util.Map.Entry;

public class CollectionDemo {

  public static void main(String... args) {
    // Tableau crée avec [] a une taille fixe
    int[] numbers = { 1, 20, 30 };
    System.out.format("premier élément %d, nombre d'éléments %d\n", numbers[0], numbers.length);
    numbers[1] = -4;
    System.out.format("Deuxième élément après modification %d\n", numbers[1]);
    // boucle for classique
    for (int i = 0; i < numbers.length; i++) {
      System.out.format("%d, ", numbers[i]);
    }
    System.out.println();
    // boucle for each
    for (int number : numbers) {
      System.out.format("%d, ", number);
    }
    System.out.println();

    List<Integer> items = new ArrayList<>();
    items.add(-3);
    items.add(11);
    items.add(22);
    for (int k = 0; k < items.size(); k++) {
      System.out.format("%d, ", items.get(k));
    }
    System.out.println();
    var otherItems = List.of(-2, 11, 22);
    for (Integer otherItem : otherItems) {
      System.out.format("%d, ", otherItem);
    }
    System.out.println();
    // Conversion d'une liste en tableau (array)
    // (Integer[]) ... s'appelle du casting -> forcer le type d'une expression
    Integer[] arrayOfItems = items.toArray(new Integer[items.size()]);
    System.out.println(arrayOfItems[0]);
    // Conversion d'un array d'un type objet en list
    List<Integer> listOfNumbers1 = Arrays.asList(arrayOfItems);
    List<Integer> listOfNumbers2 = Arrays.stream(arrayOfItems).toList();
    System.out.format("listOfNumbers1 size %d\n", listOfNumbers1.size());
    System.out.format("listOfNumbers2 size %d\n", listOfNumbers2.size());

    Iterator<Integer> iter = items.iterator();
    System.out.format("next: %d, has next ? %b\n", iter.next(), iter.hasNext());
    System.out.format("next: %d, has next ? %b\n", iter.next(), iter.hasNext());
    System.out.format("next: %d, has next ? %b\n", iter.next(), iter.hasNext());
    System.out.println();
    System.out.println("Iter for loop");
    for (var iter2 = items.iterator(); iter2.hasNext();) {
      Integer value = iter2.next();
      System.out.print(value + ", ");
    }
    System.out.println();
    System.out.println("for each utilise un itérateur derrière les rideaux");
    for (Integer item : items) {
      System.out.print(item + ", ");
    }
    System.out.println();

    System.out.println("Map");
    // 6786L => Litéral de type long (type de base)
    Map<String, Long> userIds = Map.of("Hugo", 6786L, "Rémy", 343L);
    System.out.println(userIds.get("Rémy"));
    for (var userIdEntry : userIds.entrySet()) {
      System.out.print(userIdEntry.getKey() + "->" + userIdEntry.getValue());
      System.out.print(", ");
    }
    System.out.println();

    Map<String, Long> mutableUserIds = new HashMap<>(userIds);
    mutableUserIds.put("toto", 3L);
    for (var userIdEntry : mutableUserIds.entrySet()) {
      System.out.print(userIdEntry.getKey() + "->" + userIdEntry.getValue());
      System.out.print(", ");
    }
    System.out.println();

    Iterator<Entry<String, Long>> iterUserIds = userIds.entrySet().iterator();
    System.out.println(iterUserIds.next() + ", " + iterUserIds.hasNext());
    var entry = iterUserIds.next();
    System.out.println(entry.getKey() + "->" + entry.getValue() + ", " + iterUserIds.hasNext());
  }
}

Types génériques

///usr/bin/env jbang "$0" "$@" ; exit $?

import static java.lang.System.*;
import java.util.*;

class IntegerCalculator {
  private Integer x;

  public IntegerCalculator(Integer x) {
    this.x = x;
  }

  Integer add(Integer b) {
    x += b;
    return x;
  }

  boolean isPositive() {
    return x >= 0;
  }
}

class GenericCalculator<Toto extends Number> {
  private Toto x;

  public GenericCalculator(Toto x) {
    this.x = x;
  }

  boolean isPositive() {
    return x.doubleValue() >= 0;
  }
}

public class GenericsDemo {

  public static void main(String... args) {
    var c1 = new GenericCalculator<Integer>(Integer.valueOf(10));

    List<String> items = new ArrayList<>();
    // Java déduit que le type du générique est "Integer"
    var integers = List.of(Integer.valueOf(19));
    System.out.println(integers);
  }
}

Programmation fonctionnelle

  • Les fonctions sont des éléments de première classe : Les fonctions sont comme des variables
  • Utilisation intensive de fonctions pures : fonction sans effet de bord, toujours le même résultat pour les mêmes entrées
    • exemples de fonctions par pure: print (car elle change la console)
  • Immutabilité
    • On ne peut pas changer la valeur d'une variable une fois initialisée
    • On ne peut pas changer les propriétés d'un object une fois instancié
    • On ne peut pas ajouter ou supprimer des éléments d'une collection
  • On le code est développé sous forme d'une chaîne de traitements (comme dans une usine)

Relation entre la POO et la programmation fonctionnelle

  • La POO est la prog. fonctionnelle ne sont pas mutuellement exclusifs
  • On peut développer en POO avec un style fonctionnelle:
    • Les méthodes ne font pas de mutation de champs de l'objet
    • Les propriétés sont uniquement en read-only
    • Les records simplifient la création de ce genre de classes

Interfaces fonctionnelles

///usr/bin/env jbang "$0" "$@" ; exit $?

import static java.lang.System.*;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

// L'inteface semble optionnelle
@FunctionalInterface
interface MyCustomBiPredicate {
  boolean doSomething(int a, int b);
}
// equivalent en Kotlin typealias MyCustomPredicate = (Int, Int) -> Boolean ou
// (Int, Int) -> Boolean

public class FunctionalInterfaceDemo {
  static boolean returnFalse(int a, int b) {
    return false;
  }

  // Higher order function: a function that takes as argument another function
  static void callPredicate(MyCustomBiPredicate p) {
    System.out.println(p.doSomething(10, 0));
    System.out.println(p.doSomething(0, 0));
  }

  static List<String> filter(List<String> items, Predicate<String> predicate) {
    List<String> results = new ArrayList<>();
    for (String item : items) {
      if (predicate.test(item)) {
        results.add(item);
      }
    }
    return results;
  }

  public static void main(String... args) {
    MyCustomBiPredicate p = (a, b) -> a > b;
    System.out.println(p.doSomething(10, 20));
    p = FunctionalInterfaceDemo::returnFalse;
    System.out.println(p.doSomething(111, 0));

    callPredicate(p);
    callPredicate(FunctionalInterfaceDemo::returnFalse);
    callPredicate((a, b) -> a > b);

    Predicate<Integer> multipleOfThreePredicate = (a) -> a % 3 == 0;
    Predicate<String> isEmptyPredicate = (s) -> s.length() == 0;
    System.out.println(multipleOfThreePredicate.test(21));
    System.out.println(multipleOfThreePredicate.test(65));
    System.out.println(isEmptyPredicate.test("Hello"));
    System.out.println(isEmptyPredicate.test(""));

    List<String> words = List.of("I", "Love", "Java", "2024");
    List<String> items1 = filter(words, (word) -> word.length() == 4);
    System.out.println(String.join(" - ", items1));
    List<String> items2 = filter(words, (w) -> w.charAt(0) == 'J');
    System.out.println(String.join(" - ", items2));
  }
}

Equivalent en Kotlin:

fun doSomething(a: Int, b: Int) = a > b

class EntertainmentDevice(val name: String, var releaseYear: Int) {
  val isAfter2000: Boolean
    get() = releaseYear >= 2000
}

typealias MyCustomPredicate = (Int, Int) -> Boolean

fun main() {
  val p = ::doSomething
  println(p(10, 20))

  val p2: (Int, Int) -> Boolean = ::doSomething
  println(p2(10, 20))

  val p3: MyCustomPredicate = ::doSomething
}

Liste des interfaces fonctionnelles prédéfinies qui sont séparées en 4 catégories:

  • Consumer : Fonction qui prend des arguments génériques et ne renvoie rien (type de retour void)
  • Supplier : Fonction qui ne prend aucun argument et renvoie un valeur dont le type est générique
  • Function : Fonction qui peut prendre des arguments génériques et retourne une valeur générique.
  • Predicate : Fonction qui peut prendre des arguments génériques et retour un booléen. Un predicate peut être considéré comme un cas particulier d'une Function dont le type de retour est un booléen.

La convention est de rajouter le terme bi pour les fonctions avec deux arguments (comme BiPredicate). Voici des exemples de définition de quelques interfaces fonctionnelles:

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

package java.util.function;
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

@FunctionalInterface
public interface BiFunction<T, U, R> {
    R apply(T t, U u);
}

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

Streams

Permettent de manipuler des collections de tailles arbitraires de manière déclarative.

///usr/bin/env jbang "$0" "$@" ; exit $?

//https://navin-moorthy.github.io/blog/map-filter-reduce-animated/

import static java.lang.System.*;
import java.util.*;
import java.util.stream.*;

public class StreamDemo {

  public static void main(String... args) {
    out.println("Hello World");
    Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 100);
    // Imperative programming style: describe how to do
    List<Integer> r = new ArrayList<>();
    for (Integer number : numbers.toList()) {
      if (number % 2 == 0) {
        r.add(number);
      }
    }
    numbers = Stream.of(1, 2, 3, 4, 100);
    // Style déclaratif: describe what we want to do
    var filteredNumbers = numbers.filter((n) -> n % 2 == 0);
    var doubleNumbers = filteredNumbers.map((n) -> n * 2);
    var sum = doubleNumbers.reduce(Integer::sum);
    System.out.println(sum);

    var otherNumbers = Stream.of(1, 2, 3, 4, 100);
    var result = otherNumbers
        .filter((n) -> n % 2 == 0)
        .map((n) -> n * 2)
        .reduce((acc, value) -> acc + value);

    System.out.println(result);

    List<Integer> evenNumbers = Stream.of(1, 2, 3, 4, 100)
        .filter((n) -> n % 2 == 0).toList();

    List<Integer> doubledEvens = evenNumbers.stream().map((n) -> n * 2).toList();
    var stringDoubles = doubledEvens.stream().map((n) -> n.toString()).toList();
    System.out.println(String.join(", ", stringDoubles));
  }
}

Une documentation plus complète est proposée le site de baeldung

Null safety

Définition: la null safety est toute fonctionnalité qui permet de ne plus avoir de null pointer exception à l'exécution.

Java propose deux possibilités pour aovir une sorte de null safety qui ne sont moins puissantes que ce que l'on peut trouver dans d'autres langages comme Kotlin, Swift ou TypeScript par exemple.

Type optionnel
///usr/bin/env jbang "$0" "$@" ; exit $?

// Optional<T> permet d'englober une valeur et fournit des méthodes pour la récupérer si elle est présente
// - Attention: l'objet Optionnel en lui-même peut être null
// - En plus, on peut récupérer la valeur contenue même si elle est null et on aura une exception autre que la NPE

import static java.lang.System.*;
import java.util.*;

public class OptionalDemo {

  static String getFromInternet() {
    return "dsfsdfdsf";
  }

  static Optional<String> getFromInternetOpt() {
    return Optional.of("dsfsdfdsf");
  }

  public static void main(String... args) {
    out.println("Hello World");

    Optional<String> myOptionalText = Optional.empty();
    if (myOptionalText.isPresent()) {
      System.out.println(myOptionalText.get());
    }
    var optioanlValue = getFromInternetOpt();
    if (optioanlValue.isPresent()) {
      System.out.println(optioanlValue.get());
    }
  }
}
Annotations null
/*
 * This source file was generated by the Gradle 'init' task
 */
package org.example;

import java.util.ArrayList;
import java.util.List;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

// Les annotations de nullabilité sont traitées par certains IDE et outils pour s'assurer avant la compilation qu'il n'y aura pas de NPE
// Les IDE génèrent généralement un avertissement et non une erreur
// Ces annotations sont ignorées par le compilateur Java (au moins jusqu'à la version 21)
// Il y a aussi une multitude d'annotations fournies par différentes librairies qui peuvent créer de la confusion

public class App {

  @Nonnull
  private List<String> items;

  @Nonnull
  List<String> getItems() {
    return this.items;
  }

  public App() {
    this.items = new ArrayList<>();
  }

  @Nonnull
  public String getGreeting() {
    return "dsfsdfd";
  }

  @Nonnull
  public String getValue(@Nullable String value) {
    if (value != null) {
      return value.toUpperCase();
    }
    return "";
  }

  public static void main(String[] args) {
    @Nonnull
    App app = new App();

    app.getValue(null);

    System.out.println(app.getItems().size());
    app.getItems().add(null);
    app.getItems().add("toto");
    System.out.println(app.getItems().size());
  }
}

Les exceptions

Les exceptions sont des retours alternatifs d'une méthode qui permettent de signaler une erreur. L'équivalent du return pour les exceptions est throw en java. Il permet de sortir de la fonction tout en retournant une valeur. Cette valeur doit hériter de la classe Throwable et peut être récupérée via un block try-catch.