web-dev-qa-db-fra.com

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

Cette question est de savoir si faire une classe imbriquée dans Java pour être une classe imbriquée statique ou une classe imbriquée interne. J'ai cherché ici et sur Stack Overflow, mais je n'ai pas vraiment trouvé de questions concernant les implications de conception de cette décision.

Les questions que j'ai trouvées posent sur la différence entre les classes imbriquées statiques et internes, ce qui est clair pour moi. Cependant, je n'ai pas encore trouvé de raison convaincante d'utiliser jamais une classe imbriquée statique dans 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 prendre soin 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 prie de différer à cet égard du point de vue de la conception, car cet accès direct signifie que nous avons un couplage élevé et si jamais nous voulons extraire la classe imbriquée dans sa classe de niveau supérieur séparée, nous ne pouvons le faire qu'après avoir essentiellement 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 que j'en déduis que les classes imbriquées (non anonymes) devraient être généralement statique?

Ou en d'autres termes: y a-t-il une raison convaincante pour laquelle on préférerait une classe intérieure imbriquée?

39
Frank

Joshua Bloch dans le point 22 de son livre "Effective Java Second Edition" indique quand utiliser quel type de classe imbriquée et pourquoi. Il y a quelques citations ci-dessous:

Une utilisation courante d'une classe membre statique est en tant que 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. L'énumération Operation doit être une classe membre statique publique de la classe Calculator. Les clients de Calculator pourraient alors se référer à des opérations utilisant des noms comme Calculator.Operation.PLUS et Calculator.Operation.MINUS.

Une utilisation courante d'une classe membre non statique est de définir un Adapter qui permet à une instance de la classe externe d'être vue comme une instance d'une classe non liée. Par exemple, les implémentations de l'interface Map utilisent généralement des classes membres non statiques pour implémenter leurs vues de collection, qui sont renvoyées par Map, keySet, entrySet et values méthodes. De même, les implémentations des interfaces de collection, telles que Set et 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 membre qui ne nécessite pas d'accès à une instance englobante, toujours placez le modificateur static dans sa déclaration, ce qui en fait une classe membre statique plutôt que non statique.

26
erakitin

Vous avez raison 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 (non anonyme et non local ) les classes internes doivent généralement être statiques .

Les implications de la décision de rendre la classe interne non statique sont présentées dans Java Puzzlers , Puzzle 90 (police en gras ci-dessous citation est à moi):

Chaque fois que vous écrivez une classe membre, demandez-vous: cette classe a-t-elle 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 Inner2 être static, vous pouvez réellement comprendre ce que fait le programme: un joli bonus en effet.

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 le devez, réfléchissez longuement à l'instance englobante. De plus, préférez static les classes imbriquées aux 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 sur Stack Overflow .


Il convient de noter que ce qui précède est essentiellement une version étendue des instructions données dans Java Classes and Objects tutorial :

Utilisez une classe imbriquée non statique (ou classe interne) si vous avez besoin d'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.

Donc, la réponse à la question que vous avez posée en d'autres termes par tutoriel est, la seule raison convaincante d'utiliser non statique est lorsque l'accès à une instance englobante est les champs et méthodes non publics sont requis.

Le libellé du didacticiel est assez large (cela peut être la raison pour laquelle Java Puzzlers essaie de le renforcer et de le réduire). En particulier, l'accès direct aux champs d'instance inclus n'a jamais été vraiment required d'après mon expérience - dans le sens où des moyens alternatifs comme les passer en tant que paramètres constructeur/méthode se sont toujours révélés plus faciles à déboguer et à maintenir.


Dans l'ensemble, mes rencontres (assez douloureuses) avec le débogage de classes internes accédant directement aux champs d'instance englobante ont fait forte impression que cette pratique ressemble à l'utilisation de l'état global, avec maux connus qui lui sont associés.

Bien sûr, Java fait en sorte que les dommages d'un tel "quasi global" soient contenus dans la classe englobante, mais quand j'ai dû déboguer une classe interne particulière, c'était comme si un tel pansement ne faisait pas ' t aider à réduire la douleur: je devais toujours garder à l'esprit la sémantique et les détails "étrangers" au lieu de me concentrer entièrement sur l'analyse d'un objet particulièrement gênant.


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

Renvoie une vue Set des clés contenues dans cette carte. L'ensemble est soutenu par la carte, donc les modifications apportées à la carte sont reflétées dans l'ensemble, et vice-versa ...

Non pas que ce qui précède rendrait le code impliqué plus facile à maintenir, à tester et à déboguer, mais cela pourrait au moins permettre de faire valoir que la complication correspond/est justifiée par la fonctionnalité prévue.

10
gnat

Quelques idées fausses:

Moins de couplage: nous obtenons généralement moins de couplage, car la classe [statique 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 à regarder. Néanmoins, s'il obtient un tel objet de n'importe où, il peut 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 sais pas trop ce que tu veux dire ici. Le chargeur de classe n'est impliqué que deux fois au total, une fois pour chaque classe (lorsque la classe est chargée).


Randonnée suit:

Au Javaland, nous semblons avoir un peu peur de créer des classes. Je pense que cela doit ressembler à 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.

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

Nous pouvons ignorer les problèmes de conception concernant l'accès direct, en gardant la classe intérieure privée.

(... Si vous aviez demandé 'pourquoi une classe interne non statique public?'), C'est une très bonne question - je ne pense pas avoir encore trouvé de justification pour ceux-ci) .

Vous pouvez les utiliser chaque fois que cela aide à la lisibilité du code et à éviter DRY violations et passe-partout. Je n'ai pas d'exemple prêt car cela ne se produit pas très souvent dans mon expérience, mais il le fait se produire.


Les classes internes statiques sont toujours une bonne approche par défaut , btw. Non statique a un champ caché supplémentaire généré à travers lequel la classe interne se réfère à l'extérieur.

1
Iain

Il peut être utilisé pour créer un modèle d'usine. Un modèle d'usine est souvent utilisé lorsque les objets créés ont besoin de paramètres constructeurs 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, createFromSQL ou createFromTemplate. La même chose peut être faite ici également sous la forme de plusieurs classes internes:

public class QueryFactory {

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

}

Moins de passage de paramètres est nécessaire de l'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");
0
john16384