web-dev-qa-db-fra.com

Est-ce une bonne pratique de faire en sorte que le constructeur lève une exception?

Est-ce une bonne pratique de faire en sorte que le constructeur lève une exception? Par exemple, j'ai une classe Person et j'ai age comme unique attribut. Maintenant, je fournis la classe comme

class Person{
  int age;
  Person(int age) throws Exception{
   if (age<0)
       throw new Exception("invalid age");
   this.age = age;
  }

  public void setAge(int age) throws Exception{
  if (age<0)
       throw new Exception("invalid age");
   this.age = age;
  }
}
145
ako

Lancer des exceptions dans un constructeur n'est pas une mauvaise pratique. En fait, c’est le moyen niquement raisonnable pour un constructeur d’indiquer qu’il ya un problème; par exemple. que les paramètres ne sont pas valides.

Je pense aussi que jeter coché exceptions est OK1en supposant que l'exception vérifiée est 1) déclarée et 2) spécifique au problème que vous signalez.

Cependant, déclarer ou lancer explicitement Java.lang.Exception est presque toujours une mauvaise pratique.

Vous devez choisir une classe d'exception qui correspond à la condition exceptionnelle qui s'est produite. Si vous lancez Exception, il est difficile pour l'appelant de séparer cette exception de tout autre nombre d'exceptions déclarées et non déclarées possibles. Cela rend la reprise sur erreur difficile et si l'appelant choisit de propager l'exception, le problème se propage.


1 - Certaines personnes peuvent ne pas être d’accord, mais à l’OMI, il n’ya pas de différence entre le cas présent et le cas des exceptions exceptionnelles. Les conseils standard vérifiés par rapport aux conseils non vérifiés s'appliquent également aux deux cas.


Quelqu'un a suggéré d'utiliser assert pour vérifier les arguments. Le problème avec ceci est que la vérification des assertions assert peut être activée et désactivée via un paramètre de ligne de commande JVM. Utiliser des assertions pour vérifier les invariants internes est correct, mais les utiliser pour implémenter la vérification d'arguments spécifiée dans votre javadoc n'est pas une bonne idée ... car cela signifie que votre méthode implémentera uniquement la spécification lorsque la vérification d'assertions est activée.

Le deuxième problème avec assert est que si une assertion échoue, alors AssertionError sera projeté, et la sagesse reçue est qu'il est une mauvaise idée d'essayer d'attraper Error et n'importe lequel de ses sous-types.

153
Stephen C

J'ai toujours pensé que jeter des exceptions vérifiées dans le constructeur était une mauvaise pratique, ou du moins une chose à éviter.

La raison en est que vous ne pouvez pas faire ceci:

private SomeObject foo = new SomeObject();

Au lieu de cela, vous devez faire ceci:

private SomeObject foo;
public MyObject() {
    try {
        foo = new SomeObject()
    } Catch(PointlessCheckedException e) {
       throw new RuntimeException("ahhg",e);
    }
}

Au moment où je construis Un objet Je sais ce que sont les paramètres alors pourquoi devrais-je m'attendre à l'envelopper dans une prise d'essai? Ahh vous dites mais si je construis un objet à partir de paramètres dynamiques, je ne sais pas s'ils sont valides ou non. Eh bien, vous pouvez ... valider les paramètres avant de les transmettre au constructeur. Ce serait une bonne pratique. Et si tout ce qui vous préoccupe est de savoir si les paramètres sont valides, vous pouvez utiliser IllegalArgumentException.

Donc, au lieu de lancer des exceptions vérifiées,

public SomeObject(final String param) {
    if (param==null) throw new NullPointerException("please stop");
    if (param.length()==0) throw new IllegalArgumentException("no really, please stop");
}

Bien sûr, il y a des cas où il pourrait simplement être raisonnable de lancer une exception vérifiée

public SomeObject() {
    if (todayIsWednesday) throw new YouKnowYouCannotDoThisOnAWednesday();
}

Mais à quelle fréquence est-ce probable?

26
Richard

Comme mentionné dans autre réponse ici , dans la directive 7-3 du Java consignes de codage sécurisé , le lancement d'une exception dans le constructeur d'une version non finale La classe ouvre un vecteur d’attaque potentiel:

Directive 7-3/OBJECT-3: Défense contre les instances partiellement initialisées de classes non finales Lorsqu'un constructeur d'une classe non finale lève une exception, les attaquants peuvent tenter d'accéder aux instances partiellement initialisées de cette classe. Assurez-vous qu'une classe non finale reste totalement inutilisable jusqu'à la fin de son constructeur.

À partir de JDK 6, il est possible d'empêcher la construction d'une classe sous-classable en générant une exception avant la fin du constructeur Object. Pour ce faire, effectuez les vérifications dans une expression évaluée dans un appel à this () ou à super ().

    // non-final Java.lang.ClassLoader
    public abstract class ClassLoader {
        protected ClassLoader() {
            this(securityManagerCheck());
        }
        private ClassLoader(Void ignored) {
            // ... continue initialization ...
        }
        private static Void securityManagerCheck() {
            SecurityManager security = System.getSecurityManager();
            if (security != null) {
                security.checkCreateClassLoader();
            }
            return null;
        }
    }

Pour assurer la compatibilité avec les versions antérieures, une solution potentielle implique l’utilisation d’un indicateur initialisé. Définissez l'indicateur comme dernière opération dans un constructeur avant de retourner avec succès. Toutes les méthodes fournissant une passerelle vers des opérations sensibles doivent d'abord consulter l'indicateur avant de poursuivre:

    public abstract class ClassLoader {

        private volatile boolean initialized;

        protected ClassLoader() {
            // permission needed to create ClassLoader
            securityManagerCheck();
            init();

            // Last action of constructor.
            this.initialized = true;
        }
        protected final Class defineClass(...) {
            checkInitialized();

            // regular logic follows
            ...
        }

        private void checkInitialized() {
            if (!initialized) {
                throw new SecurityException(
                    "NonFinal not initialized"
                );
            }
        }
    }

En outre, toute utilisation de ces classes liée à la sécurité doit vérifier l'état de l'indicateur d'initialisation. Dans le cas de la construction ClassLoader, il convient de vérifier que son chargeur de classe parent est initialisé.

Les instances partiellement initialisées d'une classe non finale sont accessibles via une attaque de finaliseur. L'attaquant substitue la méthode de finalisation protégée dans une sous-classe et tente de créer une nouvelle instance de cette sous-classe. Cette tentative échoue (dans l'exemple ci-dessus, la vérification SecurityManager dans le constructeur de ClassLoader lève une exception de sécurité), mais l'attaquant ignore simplement toute exception et attend que la machine virtuelle procède à la finalisation de l'objet partiellement initialisé. Lorsque cela se produit, l'implémentation de la méthode de finalisation malveillante est invoquée, donnant à l'attaquant un accès à celle-ci, une référence à l'objet en cours de finalisation. Bien que l'objet ne soit que partiellement initialisé, l'attaquant peut toujours invoquer des méthodes dessus, contournant ainsi le contrôle SecurityManager. Bien que l'indicateur initialisé n'empêche pas l'accès à l'objet partiellement initialisé, il empêche les méthodes sur cet objet de faire quoi que ce soit d'utile pour l'attaquant.

L'utilisation d'un indicateur initialisé, bien que sécurisé, peut être fastidieuse. S'assurer simplement que tous les champs d'une classe publique non finale contient une valeur sûre (telle que null) jusqu'à la fin de l'initialisation de l'objet peut représenter une alternative raisonnable dans les classes non sensibles à la sécurité.

Une approche plus robuste, mais aussi plus verbeuse, consiste à utiliser un "pointeur vers une implémentation" (ou "pimpl"). Le noyau de la classe est déplacé dans une classe non publique avec les appels de méthode de transmission de classe d'interface. Toute tentative d'utilisation de la classe avant son initialisation complète entraînera une exception NullPointerException. Cette approche convient également aux attaques par clonage et par désérialisation.

    public abstract class ClassLoader {

        private final ClassLoaderImpl impl;

        protected ClassLoader() {
            this.impl = new ClassLoaderImpl();
        }
        protected final Class defineClass(...) {
            return impl.defineClass(...);
        }
    }

    /* pp */ class ClassLoaderImpl {
        /* pp */ ClassLoaderImpl() {
            // permission needed to create ClassLoader
            securityManagerCheck();
            init();
        }

        /* pp */ Class defineClass(...) {
            // regular logic follows
            ...
        }
    }
23
Hazok

Vous n'avez pas besoin de lancer une exception cochée. C'est un bogue au sein du contrôle du programme, vous voulez donc lancer une exception non contrôlée. Utilisez l'une des exceptions non vérifiées déjà fournies par le langage Java, telles que IllegalArgumentException, IllegalStateException ou NullPointerException.

Vous voudrez peut-être aussi vous débarrasser du passeur. Vous avez déjà fourni un moyen d'initier age via le constructeur. Doit-il être mis à jour une fois instancié? Sinon, ignorez le passeur. Une bonne règle, ne rendez pas les choses plus publiques que nécessaire. Commencez avec privé ou par défaut et sécurisez vos données avec final. Maintenant tout le monde sait que Person a été construit correctement et est immuable. Il peut être utilisé en toute confiance.

C'est probablement ce dont vous avez vraiment besoin:

class Person { 

  private final int age;   

  Person(int age) {    

    if (age < 0) 
       throw new IllegalArgumentException("age less than zero: " + age); 

    this.age = age;   
  }

  // setter removed
8
Spam Suppper

Ceci est totalement valide, je le fais tout le temps. J'utilise habituellement IllegalArguemntException s'il résulte de la vérification de paramètres.

Dans ce cas, je ne recommanderais pas les assertions car elles sont désactivées dans une version de déploiement et vous voulez toujours empêcher cela, mais elles sont valides si votre groupe effectue TOUT test, avec des assertions activées et si vous pensez que le risque de manquer Un problème de paramètre au moment de l'exécution est plus acceptable que le lancement d'une exception susceptible de provoquer un crash.

En outre, une assertion serait plus difficile à intercepter pour l'appelant, c'est facile.

Vous voudrez probablement le répertorier comme un "jeté" dans les javadocs de votre méthode avec la raison pour que les appelants ne soient pas surpris.

7
Bill K

Je n'ai jamais considéré comme une mauvaise pratique de lancer une exception dans le constructeur. Lorsque la classe est conçue, vous avez une certaine idée de ce que devrait être la structure de cette classe. Si quelqu'un d'autre a une idée différente et tente de l'exécuter, vous devez vous tromper en conséquence, en donnant à l'utilisateur des informations en retour sur l'erreur. Dans votre cas, vous pourriez envisager quelque chose comme

if (age < 0) throw new NegativeAgeException("The person you attempted " +
                       "to construct must be given a positive age.");

NegativeAgeException est une classe d'exception que vous avez construite vous-même, éventuellement en étendant une autre exception telle que IndexOutOfBoundsException ou quelque chose de similaire.

Les assertions ne semblent pas non plus être la solution, car vous n'essayez pas de détecter des bogues dans votre code. Je dirais que terminer avec une exception est absolument la bonne chose à faire ici.

4
ashays

C'est une mauvaise pratique de lancer Exception, car cela nécessite que toute personne appelant votre constructeur attrape Exception, ce qui est une mauvaise pratique.

C'est une bonne idée de demander à un constructeur (ou à une méthode) de lancer une exception, en général IllegalArgumentException, qui n'est pas cochée, le compilateur ne vous oblige donc pas à l'attraper.

Vous devez lancer les exceptions vérifiées (les éléments qui vont de Exception, mais pas RuntimeException) si vous voulez que l'appelant le détecte.

4
TofuBeer

Je ne suis pas pour avoir jeté Exceptions dans le constructeur puisque je considère cela comme non propre. Il y a plusieurs raisons pour mon opinion.

  1. Comme Richard l'a mentionné, vous ne pouvez pas initialiser une instance de manière simple. Surtout dans les tests, il est vraiment ennuyeux de construire un objet à l'échelle du test uniquement en l'entourant dans un try-catch lors de l'initialisation.

  2. Les constructeurs doivent être dépourvus de logique. Il n'y a aucune raison du tout d'encapsuler la logique dans un constructeur, puisque vous visez toujours le principe de séparation des préoccupations et de responsabilité unique. Puisque le constructeur a pour préoccupation de "construire un objet", il ne doit en aucun cas encapsuler la gestion des exceptions si vous suivez cette approche.

  3. Ça sent mauvais design. À mon humble avis, si je suis obligé de gérer les exceptions dans le constructeur, je me demande d’abord si j’ai des fraudes de conception dans ma classe. Il est parfois nécessaire, mais ensuite je confie cette tâche à un constructeur ou à une usine pour que le constructeur reste aussi simple que possible.

Ainsi, s’il est nécessaire de gérer certaines exceptions dans le constructeur, pourquoi ne pas externaliser cette logique vers un constructeur d’usine? Il peut s'agir de quelques lignes de code supplémentaires, mais vous donne la liberté d'implémenter une gestion des exceptions bien plus robuste et mieux adaptée, car vous pouvez externaliser davantage la logique de gestion des exceptions sans être collée au constructeur, ce qui encapsulera trop logique. Et le client n'a besoin de rien savoir de votre logique de construction si vous déléguez correctement la gestion des exceptions.

1
Vegaaaa