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

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

//DEPS com.google.guava:guava:32.1.3-jre

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");
    int i = 10;
    long j = 1_000_000;
    var message = "hello";
    message = "world";

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

    Calculator.multiply(10, 1);
  }
}
///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);
  }
}

Classes, héritage et interfaces

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

import static java.lang.System.*;

interface Gamer {
  public void play();
}

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() {
    out.println("je joue");
  }
}

class HungryGamerHuman extends Human implements HungryEater, Gamer {

  @Override
  public void eat() {
  }

  @Override
  public void play() {
    // TODO Auto-generated method stub
    throw new UnsupportedOperationException("Unimplemented method 'play'");
  }

}

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) {

  }

  static void runGame(Gamer gamer) {

  }

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

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);
  }
}

Collections

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

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

public class CollectionDemo {

  public static void main(String... args) {
    // Type de base int
    int i = 99;
    // Objet Integer
    Integer j = 10;
    System.out.println(i);
    System.out.println("Integer: " + j.compareTo(i) + ", " + j.floatValue());
    int[] numbers = { 1, 20, 30 };
    List<Integer> items = new ArrayList<>();
    items.add(-3);
    items.add(11);
    items.add(22);
    var otherItems = List.of(-2, 11, 22);
    for (int k = 0; k < items.size(); k++) {
      System.out.print(items.get(k) + ", ");
    }
    System.out.println();
    Iterator<Integer> iter = items.iterator();
    System.out.println(iter.next() + ", " + iter.hasNext());
    System.out.println(iter.next() + ", " + iter.hasNext());
    System.out.println(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("Iter for each");
    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"));
    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());
    for (var userIdEntry : userIds.entrySet()) {
      System.out.print(userIdEntry.getKey() + "->" + userIdEntry.getValue());
      System.out.print(", ");
    }
    System.out.println();
  }
}

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.