Java 25 : Flexible Constructor Bodies (JEP 513)

Avec Java 25, la JEP 513 introduit les Flexible Constructor Bodies, une amélioration majeure du langage Java qui permet d'exécuter du code avant l'appel à super() ou this(). Cette fonctionnalité résout une limitation historique de Java et améliore la sécurité et la lisibilité des constructeurs.

Cette nouveauté fait partie des JEPs (Java Enhancement Proposals) finalisées dans Java 25.

Le problème historique

Depuis les débuts de Java, les constructeurs ont une contrainte stricte : l'appel à super() ou this() doit être la première instruction du constructeur. Cette règle, bien qu'ayant des raisons de sécurité, pose des problèmes pratiques dans de nombreux cas.

Exemple du problème

Imaginons une classe Utilisateur qui hérite de Personne :

// Avant Java 25 - CE CODE NE COMPILE PAS
public class Utilisateur extends Personne {
    public Utilisateur(String email) {
        // Erreur de compilation : super() doit être en premier
        if (email == null || !email.contains("@")) {
            throw new IllegalArgumentException("Email invalide");
        }
        super(email.toLowerCase());
    }
}

Ce code parfaitement logique ne compile pas car la validation de l'email se fait avant super(). Avant Java 25, les développeurs devaient utiliser des contournements.

Les contournements classiques

// Contournement 1 : méthode statique
public class Utilisateur extends Personne {
    public Utilisateur(String email) {
        super(validerEtNormaliser(email)); // OK mais verbeux
    }

    private static String validerEtNormaliser(String email) {
        if (email == null || !email.contains("@")) {
            throw new IllegalArgumentException("Email invalide");
        }
        return email.toLowerCase();
    }
}

// Contournement 2 : expression ternaire (limité)
public class Utilisateur extends Personne {
    public Utilisateur(String email) {
        super(email != null && email.contains("@")
            ? email.toLowerCase()
            : (() -> { throw new IllegalArgumentException("Email invalide"); }).get()
        ); // Illisible !
    }
}

Ces solutions sont verboses, difficiles à lire et à maintenir.

La solution Java 25 : Flexible Constructor Bodies

Avec Java 25, vous pouvez maintenant écrire du code avant l'appel à super() ou this(), tant que ce code n'accède pas à l'instance en cours de construction (pas de this implicite ou explicite).

// Java 25 - Code propre et lisible
public class Utilisateur extends Personne {
    public Utilisateur(String email) {
        // Validation AVANT super() - nouveauté Java 25
        if (email == null || !email.contains("@")) {
            throw new IllegalArgumentException("Email invalide: " + email);
        }

        // Préparation des données
        String emailNormalise = email.toLowerCase().trim();

        // Maintenant on peut appeler super()
        super(emailNormalise);
    }
}

Règles et restrictions

Le code exécuté avant super() ou this() est soumis à certaines restrictions pour garantir la sécurité du modèle objet Java :

Ce qui est autorisé

  • Déclaration et utilisation de variables locales
  • Validation des paramètres du constructeur
  • Appel de méthodes statiques
  • Création d'objets (autres que this)
  • Utilisation de boucles et conditions
  • Lancement d'exceptions
  • Appel de méthodes sur les paramètres

Ce qui est interdit

  • Accès à this (explicite ou implicite)
  • Accès aux champs d'instance
  • Appel de méthodes d'instance
  • Accès à super (autre que l'appel au constructeur)
public class Exemple extends Parent {
    private String nom;

    public Exemple(String valeur) {
        // ✅ OK - variable locale
        String temp = valeur.toUpperCase();

        // ✅ OK - méthode statique
        String valide = Validateur.valider(temp);

        // ✅ OK - création d'objet
        List<String> liste = new ArrayList<>();

        // ❌ INTERDIT - accès au champ d'instance
        // this.nom = valeur; // Erreur de compilation

        // ❌ INTERDIT - méthode d'instance
        // traiter(valeur); // Erreur de compilation

        super(valide);
    }
}

Cas d'utilisation pratiques

1. Validation des paramètres

Le cas le plus courant : valider les entrées avant de les passer au constructeur parent.

public class Commande extends Document {
    public Commande(List<Article> articles, Client client) {
        // Validations complètes avant super()
        Objects.requireNonNull(articles, "La liste d'articles ne peut pas être null");
        Objects.requireNonNull(client, "Le client ne peut pas être null");

        if (articles.isEmpty()) {
            throw new IllegalArgumentException("Une commande doit avoir au moins un article");
        }

        if (!client.estActif()) {
            throw new IllegalStateException("Impossible de créer une commande pour un client inactif");
        }

        super(genererNumero(), LocalDateTime.now());
        // Initialisation des champs après super()
    }

    private static String genererNumero() {
        return "CMD-" + System.currentTimeMillis();
    }
}

2. Transformation des paramètres

Préparer les données avant de les passer au constructeur parent.

public class ConfigurationSecurisee extends Configuration {
    public ConfigurationSecurisee(String motDePasse, Map<String, String> options) {
        // Hash du mot de passe avant stockage
        String hash = BCrypt.hashpw(motDePasse, BCrypt.gensalt(12));

        // Filtrage des options sensibles
        Map<String, String> optionsFiltrees = options.entrySet().stream()
            .filter(e -> !e.getKey().startsWith("secret_"))
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

        super(hash, optionsFiltrees);
    }
}

3. Calculs complexes d'initialisation

public class Rectangle extends Forme {
    public Rectangle(Point coin1, Point coin2) {
        // Calcul des dimensions
        double largeur = Math.abs(coin2.getX() - coin1.getX());
        double hauteur = Math.abs(coin2.getY() - coin1.getY());

        // Calcul du coin supérieur gauche
        double x = Math.min(coin1.getX(), coin2.getX());
        double y = Math.min(coin1.getY(), coin2.getY());
        Point coinNormalise = new Point(x, y);

        super(coinNormalise, largeur, hauteur);
    }
}

4. Logging et audit

public class EntiteAuditee extends Entite {
    private static final Logger LOGGER = LoggerFactory.getLogger(EntiteAuditee.class);

    public EntiteAuditee(String type, Map<String, Object> donnees) {
        // Logging avant création
        LOGGER.info("Création d'entité de type '{}' avec {} propriétés",
            type, donnees.size());

        // Validation du type
        if (!TypeEntite.estValide(type)) {
            LOGGER.error("Type d'entité invalide: {}", type);
            throw new IllegalArgumentException("Type invalide: " + type);
        }

        super(type, donnees);
    }
}

Utilisation avec this()

Les Flexible Constructor Bodies fonctionnent également avec this() pour le chaînage de constructeurs dans la même classe.

public class Produit {
    private final String code;
    private final String nom;
    private final BigDecimal prix;

    public Produit(String code, String nom, BigDecimal prix) {
        this.code = code;
        this.nom = nom;
        this.prix = prix;
    }

    // Constructeur avec prix String
    public Produit(String code, String nom, String prixStr) {
        // Conversion et validation avant this()
        BigDecimal prix;
        try {
            prix = new BigDecimal(prixStr);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("Prix invalide: " + prixStr);
        }

        if (prix.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Le prix ne peut pas être négatif");
        }

        this(code, nom, prix);
    }

    // Constructeur avec prix int (centimes)
    public Produit(String code, String nom, int prixCentimes) {
        // Conversion avant this()
        BigDecimal prix = BigDecimal.valueOf(prixCentimes)
            .divide(BigDecimal.valueOf(100));

        this(code, nom, prix);
    }
}

Interaction avec les Records

Les Flexible Constructor Bodies sont particulièrement utiles avec les Records Java introduits dans Java 16. Le constructeur canonique d'un Record peut maintenant inclure du code avant l'initialisation des composants.

public record Email(String adresse) {
    public Email {
        // Validation avant l'affectation automatique
        if (adresse == null || adresse.isBlank()) {
            throw new IllegalArgumentException("L'adresse email ne peut pas être vide");
        }

        // Normalisation
        adresse = adresse.toLowerCase().trim();

        // Validation du format
        if (!adresse.matches("^[\\w.-]+@[\\w.-]+\\.[a-z]{2,}$")) {
            throw new IllegalArgumentException("Format d'email invalide: " + adresse);
        }

        // L'affectation this.adresse = adresse est implicite
    }
}

public record Intervalle(int debut, int fin) {
    public Intervalle {
        // Normalisation : s'assurer que debut <= fin
        if (debut > fin) {
            int temp = debut;
            debut = fin;
            fin = temp;
        }
    }
}

Comparaison avec d'autres langages

D'autres langages permettent déjà ce type de flexibilité dans les constructeurs :

  • Kotlin : Les blocs init s'exécutent dans l'ordre de déclaration
  • C# : Permet du code avant l'appel au constructeur de base via des initialiseurs
  • Scala : Le corps de la classe est le constructeur, très flexible

Java 25 rattrape ce retard tout en maintenant la sécurité du modèle objet grâce aux restrictions sur l'accès à this.

Migration et compatibilité

Cette fonctionnalité est entièrement rétrocompatible. Le code Java existant continue de fonctionner sans modification. Vous pouvez adopter progressivement les Flexible Constructor Bodies dans les nouveaux constructeurs ou lors de la refactorisation.

Refactorisation d'un code existant

// Avant Java 25
public class Service {
    public Service(Config config) {
        super(validerConfig(config));
    }

    private static Config validerConfig(Config config) {
        if (config == null) throw new NullPointerException();
        if (!config.isValid()) throw new IllegalArgumentException();
        return config.normalize();
    }
}

// Après Java 25 - plus lisible
public class Service {
    public Service(Config config) {
        if (config == null) {
            throw new NullPointerException("Config requise");
        }
        if (!config.isValid()) {
            throw new IllegalArgumentException("Config invalide");
        }
        Config configNormalisee = config.normalize();

        super(configNormalisee);
    }
}

Bonnes pratiques

  • Utilisez cette fonctionnalité pour la validation : c'est son cas d'usage principal
  • Gardez le code pré-super() court : validation et préparation uniquement
  • Évitez les effets de bord complexes : le constructeur parent pourrait échouer
  • Documentez les validations : utilisez Javadoc pour expliquer les contraintes
  • Préférez Objects.requireNonNull() pour les vérifications de null

Conclusion

Les Flexible Constructor Bodies de Java 25 (JEP 513) sont une amélioration bienvenue qui simplifie l'écriture de constructeurs robustes. Cette fonctionnalité élimine le besoin de méthodes statiques auxiliaires et rend le code plus lisible et maintenable.

Pour les développeurs Java, c'est une fonctionnalité à adopter dès que possible, particulièrement pour la validation des paramètres et la préparation des données avant l'appel au constructeur parent.

Ressources