¿Por qué preferir las clases internas no estáticas sobre las estáticas?

37

Esta pregunta es sobre si hacer que una clase anidada en Java sea una clase anidada estática o una clase anidada interna. Busqué por aquí y en Stack Overflow, pero realmente no pude encontrar ninguna pregunta sobre las implicaciones de diseño de esta decisión.

Las preguntas que encontré son sobre la diferencia entre clases anidadas estáticas e internas, lo cual es claro para mí. Sin embargo, todavía no he encontrado una razón convincente para usar una clase anidada estática en Java, con la excepción de las clases anónimas, que no considero para esta pregunta.

Aquí entiendo el efecto del uso de clases anidadas estáticas:

  • Menos acoplamiento: generalmente obtenemos menos acoplamiento, ya que la clase no puede acceder directamente a los atributos de su clase externa. Menos acoplamiento generalmente significa mejor calidad de código, pruebas más fáciles, refactorización, etc.
  • Individual Class: el cargador de clases no necesita ocuparse de una nueva clase cada vez, creamos un objeto de la clase externa. Simplemente obtenemos nuevos objetos para la misma clase una y otra vez.

Para una clase interna, generalmente encuentro que las personas consideran el acceso a los atributos de la clase externa como un profesional. Ruego diferir en este aspecto desde el punto de vista del diseño, ya que este acceso directo significa que tenemos un alto acoplamiento y si alguna vez queremos extraer la clase anidada en su clase separada de nivel superior, solo podemos hacerlo después de esencialmente girar en una clase anidada estática.

Entonces, mi pregunta se reduce a esto: ¿Me equivoco al suponer que el acceso al atributo disponible para las clases internas no estáticas conduce a un alto acoplamiento, por lo tanto, a una menor calidad del código, y que deduzco de esto que las clases anidadas (no anónimas) deberían generalmente ser estático?

O en otras palabras: ¿hay alguna razón convincente por la que uno preferiría una clase interna anidada?

Franco
fuente
2
del tutorial de Java : "Use una clase anidada no estática (o clase interna) si necesita acceso a los campos y métodos no públicos de una instancia de cierre. Use una clase anidada estática si no requiere este acceso".
mosquito
55
Gracias por la cita del tutorial, pero esto solo reitera cuál es la diferencia, no por qué querría acceder a los campos de instancia.
Frank
es más bien una guía general, insinuando qué preferir. Agregué una explicación más detallada sobre cómo lo interpreto en esta respuesta
mosquito
1
Si la clase interna no está estrechamente acoplada a la clase que la encierra, probablemente no debería ser una clase interna en primer lugar.
Kevin Krumwiede

Respuestas:

23

Joshua Bloch en el ítem 22 de su libro "Effective Java Second Edition" le dice cuándo usar qué clase de clase anidada y por qué. Hay algunas citas a continuación:

Un uso común de una clase miembro estática es como una clase auxiliar pública, útil solo en combinación con su clase externa. Por ejemplo, considere una enumeración que describe las operaciones admitidas por una calculadora. La enumeración de la operación debe ser una clase miembro estática pública de la Calculatorclase Los clientes de Calculatorpodrían referirse a operaciones usando nombres como Calculator.Operation.PLUSy Calculator.Operation.MINUS.

Un uso común de una clase de miembro no estático es definir un Adaptador que permita que una instancia de la clase externa se vea como una instancia de una clase no relacionada. Por ejemplo, las implementaciones de la Mapinterfaz suelen utilizar clases de miembros no estáticos para implementar sus vistas de recogida , que son devueltos por Map's keySet, entrySety valuesmétodos. Del mismo modo, las implementaciones de las interfaces de recopilación, como Sety List, generalmente usan clases de miembros no estáticos para implementar sus iteradores:

// 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 declara una clase miembro que no requiere acceso a una instancia adjunta, coloque siempre el staticmodificador en su declaración, convirtiéndola en una clase miembro estática en lugar de no estática.

erakitin
fuente
1
No estoy muy seguro de cómo / si este adaptador cambia la vista de las MySetinstancias de clase externa . Podría imaginar que algo como un tipo dependiente de la ruta sea una diferencia, es decir myOneSet.iterator.getClass() != myOtherSet.iterator.getClass(), pero de nuevo, en Java esto en realidad no es posible, ya que la clase sería la misma para cada uno.
Frank
@Frank Es una clase de adaptador bastante típica: le permite ver el conjunto como una secuencia de sus elementos (un iterador).
user253751
@immibis gracias, soy muy consciente de lo que hace el iterador / adaptador, pero esta pregunta fue sobre cómo se implementa.
Frank
@Frank Estaba diciendo cómo cambia la vista de la instancia externa. Eso es exactamente lo que hace un adaptador: cambia la forma de ver algún objeto. En este caso, ese objeto es la instancia externa.
user253751
10

Tiene razón al suponer que el acceso al atributo disponible para las clases internas no estáticas conduce a un alto acoplamiento, por lo tanto, a una menor calidad del código, y las clases internas (no anónimas y no locales ) generalmente deberían ser estáticas.

Las implicaciones de diseño de la decisión de hacer que la clase interna no sea estática se presentan en Java Puzzlers , Puzzle 90 (la fuente en negrita en la cita a continuación es mía):

Cada vez que escriba una clase miembro, pregúntese: ¿Esta clase realmente necesita una instancia de cierre? Si la respuesta es no, hazlo static. Las clases internas a veces son útiles, pero pueden introducir fácilmente complicaciones que hacen que un programa sea difícil de entender. Tienen interacciones complejas con genéricos (Puzzle 89), reflexión (Puzzle 80) y herencia (este rompecabezas) . Si declaras Inner1ser static, el problema desaparece. Si también declaras Inner2serlo static, puedes entender lo que hace el programa: una buena ventaja.

En resumen, rara vez es apropiado que una clase sea tanto una clase interna como una subclase de otra. De manera más general, rara vez es apropiado extender una clase interna; si debe hacerlo, piense mucho sobre la instancia que lo incluye. Además, prefiera staticclases anidadas a no static. La mayoría de las clases miembro pueden y deben declararse static.

Si está interesado, se proporciona un análisis más detallado de Puzzle 90 en esta respuesta en Stack Overflow .


Vale la pena señalar que lo anterior es esencialmente una versión extendida de la guía dada en el tutorial de Java Classes and Objects :

Utilice una clase anidada no estática (o clase interna) si necesita acceso a los campos y métodos no públicos de una instancia de cierre. Use una clase anidada estática si no requiere este acceso.

Por lo tanto, la respuesta a la pregunta que hizo en otras palabras por tutorial es que la única razón convincente para usar no estática es cuando se requiere acceso a los campos y métodos no públicos de una instancia adjunta .

La redacción del tutorial es algo amplia (esta puede ser la razón por la cual los Java Puzzlers intentan fortalecerla y reducirla). En particular, el acceso directo a los campos de instancia de cierre nunca ha sido realmente requerido en mi experiencia, en el sentido de que formas alternativas como pasarlas como parámetros de constructor / método siempre resultaron más fáciles de depurar y mantener.


En general, mis encuentros (bastante dolorosos) con la depuración de las clases internas que acceden directamente a los campos de la instancia de cierre causaron una fuerte impresión de que esta práctica se asemeja al uso del estado global, junto con los males conocidos asociados con él.

Por supuesto, Java hace que el daño de un "cuasi global" esté contenido dentro de la clase adjunta, pero cuando tuve que depurar una clase interna particular, sentí que una curita no ayudaba a reducir el dolor: todavía tenía tener en cuenta la semántica "extranjera" y los detalles en lugar de centrarse completamente en el análisis de un objeto problemático en particular.


En aras de la exhaustividad, puede haber casos en los que no se aplique el razonamiento anterior. Por ejemplo, según mi lectura de map.keySet javadocs , esta característica sugiere un acoplamiento estrecho y, como resultado, invalida los argumentos contra las clases no estáticas:

Devuelve una Setvista de las claves contenidas en este mapa. El conjunto está respaldado por el mapa, por lo que los cambios en el mapa se reflejan en el conjunto, y viceversa ...

No es que lo anterior haga que el código involucrado sea más fácil de mantener, probar y depurar, pero al menos podría permitir a uno argumentar que las complicaciones coinciden / están justificadas por la funcionalidad prevista.

mosquito
fuente
Comparto su experiencia sobre los problemas causados ​​por este alto acoplamiento, pero no estoy de acuerdo con el uso del tutorial de require . Usted mismo dice que realmente nunca requirió el acceso al campo, por lo que desafortunadamente, esto tampoco responde completamente a mi pregunta.
Frank
@Frank, como puede ver, mi interpretación de la guía de tutoría (que parece ser compatible con Java Puzzlers) es como, usar clases no estáticas solo cuando puede demostrar que esto es necesario y, en particular, demostrar que las formas alternativas son peor . Vale la pena señalar que, en mi experiencia, las formas alternativas siempre resultaron mejores
mosquito
Ese es el punto. Si siempre es mejor, ¿por qué nos molestamos?
Frank
@Frank otra respuesta proporciona ejemplos convincentes de que esto no siempre es así. Según mi lectura de map.keySet javadocs , esta característica sugiere un acoplamiento estrecho y, como resultado, invalida los argumentos contra las clases no estáticas "El conjunto está respaldado por el mapa, por lo que los cambios en el mapa se reflejan en el conjunto, y viceversa ... "
mosquito
1

Algunas ideas falsas:

Menos acoplamiento: generalmente tenemos menos acoplamiento, ya que la clase [estática interna] no puede acceder directamente a los atributos de su clase externa.

IIRC - En realidad, puede. (Por supuesto, si son campos de objetos, no tendrá un objeto externo para mirar. Sin embargo, si obtiene dicho objeto de cualquier parte, entonces puede acceder directamente a esos campos).

Clase única: el cargador de clases no necesita ocuparse de una nueva clase cada vez, creamos un objeto de la clase externa. Simplemente obtenemos nuevos objetos para la misma clase una y otra vez.

No estoy muy seguro de lo que quieres decir aquí. El cargador de clases solo está involucrado dos veces en total, una vez para cada clase (cuando se carga la clase).


La divagación sigue:

En Javaland parece que tenemos un poco de miedo para crear clases. Creo que tiene que sentirse como un concepto significativo. Generalmente pertenece a su propio archivo. Se necesita una placa repetitiva significativa. Necesita atención a su diseño.

Frente a otros lenguajes, por ejemplo, Scala, o tal vez C ++: no hay absolutamente ningún drama crear un pequeño titular (clase, estructura, lo que sea) en cualquier momento que dos bits de datos se unan. Las reglas de acceso flexible lo ayudan a reducir el límite, lo que le permite mantener el código relacionado físicamente más cerca. Esto reduce su "distancia mental" entre las dos clases.

Podemos alejar las preocupaciones de diseño sobre el acceso directo, manteniendo la clase interna privada.

(... Si hubiera preguntado '¿por qué una clase interna pública no estática ?', Esa es una muy buena pregunta, no creo que haya encontrado un caso justificado para ellos).

Puede usarlos en cualquier momento que ayude con la legibilidad del código y evite violaciones DRY y repetitivo. No tengo un ejemplo listo porque no sucede con tanta frecuencia en mi experiencia, pero sucede.


Las clases internas estáticas siguen siendo un buen enfoque predeterminado , por cierto. No estático tiene un campo oculto adicional generado a través del cual la clase interna se refiere a la externa.

Iain
fuente
0

Se puede usar para crear un patrón de fábrica. A menudo se usa un patrón de fábrica cuando los objetos creados necesitan parámetros de constructor adicionales para funcionar, pero son tediosos para proporcionar cada vez:

Considerar:

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);
    }
}

Usado como:

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

q.performQuery();

Lo mismo podría lograrse con una clase interna no estática:

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);
        }
    }
}

Usar como:

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

q.performQuery();

Las fábricas se benefician de tener métodos nombrados específicamente, como create, createFromSQLo createFromTemplate. Aquí también se puede hacer lo mismo en forma de múltiples clases internas:

public class QueryFactory {

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

}

Se necesita menos paso de parámetros de fábrica a clase construida, pero la legibilidad puede sufrir un poco.

Comparar:

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

vs

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