Pourquoi préférer les classes internes non statiques aux classes statiques?

37

Cette question concerne l’opportunité de transformer une classe imbriquée en Java en classe imbriquée statique ou en classe imbriquée interne. J'ai cherché ici et sur Stack Overflow, mais je n'ai trouvé aucune question concernant les implications de cette décision sur la conception.

Les questions que j'ai trouvées portent sur la différence entre les classes imbriquées statique et interne, ce qui est clair pour moi. Cependant, je n'ai pas encore trouvé de raison convaincante d'utiliser une classe imbriquée statique en Java - à l'exception des classes anonymes, que je ne considère pas pour cette question.

Voici ma compréhension de l'effet de l'utilisation de classes imbriquées statiques:

  • Moins de couplage: Nous obtenons généralement moins de couplage, car la classe ne peut pas accéder directement aux attributs de sa classe externe. Moins de couplage signifie généralement une meilleure qualité de code, des tests plus faciles, une refactorisation, etc.
  • Single Class: le chargeur de classe n'a pas besoin de s'occuper d'une nouvelle classe à chaque fois, nous créons un objet de la classe externe. Nous obtenons simplement de nouveaux objets pour la même classe, encore et encore.

Pour une classe interne, je trouve généralement que les gens considèrent l'accès aux attributs de la classe externe comme un pro. Je me démarquerai à cet égard du point de vue de la conception, car cet accès direct signifie que nous avons un couplage élevé et que si nous voulons jamais extraire la classe imbriquée dans sa classe de premier niveau séparée, nous ne pourrons le faire qu'après avoir tourné. dans une classe imbriquée statique.

Ma question se résume donc à ceci: ai-je tort de supposer que l'accès aux attributs disponible pour les classes internes non statiques conduit à un couplage élevé, donc à une qualité de code inférieure, et j'en déduis que des classes imbriquées (non anonymes) devraient généralement être statique?

Ou en d'autres termes: existe-t-il une raison convaincante de préférer une classe interne imbriquée?

Franc
la source
2
du didacticiel de Java : "Utilisez une classe imbriquée non statique (ou une classe interne) si vous souhaitez accéder aux champs et méthodes non publics d'une instance englobante. Utilisez une classe imbriquée statique si vous n'avez pas besoin de cet accès."
moucher
5
Merci pour la citation du didacticiel, mais cela ne fait que rappeler la différence, et non la raison pour laquelle vous souhaitez accéder aux champs d'instance.
Frank
c'est plutôt une orientation générale, suggérant ce qu'il faut préférer. J'ai ajouté une explication plus détaillée sur la façon dont je l'interprète dans cette réponse
Gnat,
1
Si la classe interne n'est pas étroitement liée à la classe englobante, elle ne devrait probablement pas être une classe interne en premier lieu.
Kevin Krumwiede

Réponses:

23

Dans l’article 22 de son livre "Effective Java Second Edition", Joshua Bloch explique quand utiliser quel type de classe imbriquée et pourquoi. Il y a quelques citations ci-dessous:

Une utilisation courante d'une classe de membre statique est une classe d'assistance publique, utile uniquement en conjonction avec sa classe externe. Par exemple, considérons une énumération décrivant les opérations prises en charge par une calculatrice. Operation enum doit être une classe de membre statique publique de la Calculatorclasse. Les clients de Calculatorpourraient alors faire référence à des opérations utilisant des noms tels que Calculator.Operation.PLUSet Calculator.Operation.MINUS.

Une utilisation courante d'une classe membre non statique consiste à définir un adaptateur permettant à une instance de la classe externe d'être considérée comme une instance d'une classe non liée. Par exemple, les implémentations de l' Mapinterface de utilisent généralement des classes de membres non statiques à mettre en œuvre leurs vues sur la collecte , qui sont renvoyés par Map« s keySet, entrySetet des valuesméthodes. De même, les implémentations des interfaces de collection, telles que Setet List, utilisent généralement des classes membres non statiques pour implémenter leurs itérateurs:

// Typical use of a nonstatic member class
public class MySet<E> extends AbstractSet<E> {
    ... // Bulk of the class omitted

    public Iterator<E> iterator() {
        return new MyIterator();
    }

    private class MyIterator implements Iterator<E> {
        ...
    }
}

Si vous déclarez une classe de membre qui ne nécessite pas l'accès à une instance englobante, insérez toujours le staticmodificateur dans sa déclaration, ce qui en fera une classe de membre statique plutôt que non statique.

erakitin
la source
1
Je ne sais pas trop si / comment cet adaptateur modifie la vue des MySetinstances de classe externes ? Je pourrais envisager que quelque chose comme un type dépendant du chemin soit une différence, c'est-à-dire myOneSet.iterator.getClass() != myOtherSet.iterator.getClass(), mais encore une fois, en Java, cela n'est en fait pas possible, car la classe serait la même pour chacun.
Frank
@Frank C'est une classe d'adaptateur assez typique - elle vous permet de voir l'ensemble comme un flux de ses éléments (un itérateur).
user253751
@immibis merci, je sais très bien ce que fait l'itérateur / adaptateur, mais cette question portait sur la façon dont elle est mise en œuvre.
Frank
@ Frank, je disais comment cela change la vue de l'instance externe. C'est exactement ce que fait un adaptateur - cela change votre façon de voir un objet. Dans ce cas, cet objet est l'instance externe.
user253751
10

Vous avez raison de supposer que l'accès aux attributs disponible pour les classes internes non statiques entraîne un couplage élevé, donc une qualité de code inférieure, et que les classes internes (non anonymes et non locales ) doivent généralement être statiques.

Les implications de la décision de rendre les classes internes non statiques sont décrites dans Java Puzzlers , Puzzle 90 (la police en gras ci-dessous est le mien):

Chaque fois que vous écrivez une classe membre, demandez-vous si cette classe a vraiment besoin d'une instance englobante? Si la réponse est non, faites-le static. Les classes internes sont parfois utiles, mais elles peuvent facilement introduire des complications qui rendent un programme difficile à comprendre. Ils ont des interactions complexes avec les génériques (Puzzle 89), la réflexion (Puzzle 80) et l'héritage (ce puzzle) . Si vous déclarez Inner1être static, le problème disparaît. Si vous déclarez également l' Inner2être static, vous pouvez réellement comprendre ce que fait le programme: un bonus vraiment intéressant.

En résumé, il est rarement approprié qu'une classe soit à la fois une classe interne et une sous-classe d'une autre. Plus généralement, il est rarement approprié d'étendre une classe interne; si vous devez, réfléchissez longuement à l'instance englobante. Aussi, préférez staticles classes imbriquées à non static. La plupart des classes membres peuvent et doivent être déclarées static.

Si vous êtes intéressé, une analyse plus détaillée de Puzzle 90 est fournie dans cette réponse à Stack Overflow .


Il est à noter que ce qui précède est essentiellement une version étendue des instructions données dans le tutoriel sur les classes et objets Java :

Utilisez une classe imbriquée (ou classe interne) non statique si vous souhaitez accéder aux champs et méthodes non publics d'une instance englobante. Utilisez une classe imbriquée statique si vous n'avez pas besoin de cet accès.

Ainsi, la réponse à la question que vous a demandé en d' autres termes par tutoriel est, la seule convaincre raison d'utiliser non-statique lorsque l' accès à des champs non publics d'une instance englobante et des méthodes est nécessaire .

La formulation du didacticiel est quelque peu large (c'est peut-être pourquoi Java Puzzlers tente de le renforcer et de le réduire). En particulier, accéder directement aux champs d'instance englobants n'a jamais été réellement nécessaire , dans la mesure où d'autres méthodes, telles que leur transmission en tant que paramètres constructeur / méthode, devenaient toujours plus faciles à déboguer et à gérer.


Globalement, mes rencontres (assez pénibles) avec le débogage de classes internes qui accédaient directement à des champs d'instance englobante donnaient la forte impression que cette pratique ressemblait à l'utilisation d'un état global, avec les maux connus qui lui étaient associés.

Bien sûr, Java fait en sorte que les dommages d’un tel "quasi global" soient contenus dans la classe englobante, mais lorsque je devais déboguer une classe interne particulière, c’était comme si un tel bandeau ne permettait pas de réduire la douleur: j’avais encore garder à l'esprit la sémantique et les détails «étrangers» au lieu de se concentrer entièrement sur l'analyse d'un objet problématique particulier.


Par souci d'exhaustivité, il peut y avoir des cas où le raisonnement ci-dessus ne s'applique pas. Par exemple, à ma lecture des javadocs map.keySet , cette fonctionnalité suggère un couplage étroit et, par conséquent, invalide les arguments par rapport aux classes non statiques:

Renvoie une Setvue des clés contenues dans cette carte. L'ensemble étant sauvegardé par la carte, les modifications apportées à la carte sont répercutées dans l'ensemble, et inversement ...

Cela ne faciliterait pas la maintenance, le test et le débogage du code impliqué, mais cela pourrait au moins permettre de dire que la complication correspond / est justifiée par la fonctionnalité voulue.

moucheron
la source
Je partage votre expérience sur les problèmes causés par ce couplage élevé, mais je ne suis pas d'accord avec l'utilisation du besoin faite par le tutoriel . Vous dites vous-même que vous n'avez jamais vraiment besoin de l'accès au champ. Malheureusement, cela ne répond pas pleinement à ma question.
Frank
@Frank, comme vous pouvez le constater, mon interprétation des instructions du tutoriel (qui semblent être supportées par Java Puzzlers) est la suivante: utilisez des classes non statiques uniquement lorsque vous pouvez prouver que cela est nécessaire, et en particulier, prouver que d'autres méthodes sont disponibles. pire . A noter que , dans mon expérience d' autres moyens toujours mieux tourné
moucheron
C'est le but. Si c'est toujours mieux, alors pourquoi on se donne la peine?
Frank
@Frank une autre réponse fournit des exemples convaincants que ce n'est pas toujours le cas. Selon mes lectures des javadocs map.keySet , cette fonctionnalité suggère un couplage étroit et, par conséquent, invalide les arguments des classes non statiques "Le jeu étant sauvegardé par la carte, les modifications apportées à la carte sont reflétées dans le jeu, et inversement. ... "
Gnat
1

Quelques idées fausses:

Moins de couplage: Nous obtenons généralement moins de couplage, car la classe [static interne] ne peut pas accéder directement aux attributs de sa classe externe.

IIRC - En fait, c'est possible. (Bien sûr, s’il s’agit de champs d’objet, il n’aura pas d’objet externe à examiner. Néanmoins, s’il récupère un tel objet de n’importe où, il pourra accéder directement à ces champs).

Classe unique: le chargeur de classe n'a pas besoin de prendre en charge une nouvelle classe à chaque fois, nous créons un objet de la classe externe. Nous obtenons simplement de nouveaux objets pour la même classe, encore et encore.

Je ne suis pas tout à fait sûr de ce que vous voulez dire ici. Le chargeur de classes n'est impliqué que deux fois au total, une fois pour chaque classe (lorsque la classe est chargée).


Randonnée suit:

En Javaland, il semble que nous ayons un peu peur de créer des classes. Je pense que cela doit se sentir comme un concept important. Il appartient généralement à son propre fichier. Il a besoin d'un passe-partout important. Il a besoin d'attention à sa conception.

Vers d'autres langages - par exemple Scala, ou peut-être C ++: Il n'y a absolument aucun drame créant un détenteur minuscule (classe, struct, peu importe) à chaque fois que deux bits de données appartiennent ensemble. Les règles d'accès flexibles vous aident en coupant le passe-partout, vous permettant de garder le code associé physiquement plus près. Cela réduit votre "distance mentale" entre les deux classes.

Nous pouvons éviter les problèmes de conception liés à l’accès direct en maintenant la classe privée privée.

(... Si vous avez demandé « pourquoi un non-statique publique classe intérieure ?», C'est une très bonne question - je ne pense pas avoir encore trouvé de cas justifié à leur place).

Vous pouvez les utiliser à tout moment, car cela aide à la lisibilité du code et permet d’éviter les violations de DRY et le standard. Je n'ai pas un exemple concret parce que cela ne se produit pas très souvent dans mon expérience, mais cela se produit.


Les classes internes statiques sont toujours une bonne approche par défaut , au fait. Non statique a un champ caché supplémentaire généré par lequel la classe interne fait référence à la classe extérieure.

Iain
la source
0

Il peut être utilisé pour créer un motif d'usine. Un modèle d'usine est souvent utilisé lorsque les objets créés nécessitent des paramètres de constructeur supplémentaires pour fonctionner, mais sont fastidieux à fournir à chaque fois:

Considérer:

public class QueryFactory {
    @Inject private Database database;

    public Query create(String sql) {
        return new Query(database, sql);
    }
}

public class Query {
    private final Database database;
    private final String sql;

    public Query(Database database, String sql) {
        this.database = database;
        this.sql = sql;
    } 

    public List performQuery() {
        return database.query(sql);
    }
}

Utilisé comme:

Query q = queryFactory.create("SELECT * FROM employees");

q.performQuery();

La même chose pourrait être réalisée avec une classe interne non statique:

public class QueryFactory {
    @Inject private Database database;

    public class Query {
        private final String sql;

        public Query(String sql) {
            this.sql = sql;
        }

        public List performQuery() {
            return database.query(sql);
        }
    }
}

Utilisé comme:

Query q = queryFactory.new Query("SELECT * FROM employees");

q.performQuery();

Les usines bénéficient de méthodes spécifiquement nommées, comme create, createFromSQLou createFromTemplate. La même chose peut être faite ici aussi sous la forme de plusieurs classes internes:

public class QueryFactory {

    public class SQL { ... }
    public class Native { ... }
    public class Builder { ... }

}

Moins de passages de paramètres sont nécessaires de la classe d'usine à la classe construite, mais la lisibilité peut en souffrir un peu.

Comparer:

Queries.Native q = queries.new Native("select * from employees");

contre

Query q = queryFactory.createNative("select * from employees");
john16384
la source