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)
- exemples de fonctions par pure:
- 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.
///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());
}
}
}
/*
* 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
.