En un proyecto mío reciente, definí una clase con el siguiente encabezado:
public class Node extends ArrayList<Node> {
...
}
Sin embargo, después de discutir con mi profesor de CS, declaró que la clase sería "horrible para la memoria" y "mala práctica". No he encontrado que el primero sea particularmente cierto, y el segundo sea subjetivo.
Mi razonamiento para este uso es que tuve una idea para un objeto que debía definirse como algo que podría tener una profundidad arbitraria, donde el comportamiento de una instancia podría definirse ya sea por una implementación personalizada o por el comportamiento de varios objetos similares que interactúan . Esto permitiría la abstracción de objetos cuya implementación física estaría compuesta por muchos subcomponentes que interactúan.
Por otro lado, veo cómo esto podría ser una mala práctica. La idea de definir algo como una lista de sí misma no es simple ni físicamente implementable.
¿Hay alguna razón válida por la que no debería usar esto en mi código, considerando mi uso?
¹ Si necesito explicar esto más a fondo, estaría encantado de hacerlo; Solo intento mantener esta pregunta concisa.
fuente
ArrayList<Node>
, en comparación con implementarList<Node>
?Respuestas:
Francamente, no veo la necesidad de herencia aquí. No tiene sentido
Node
es unArrayList
deNode
?Si esto es solo una estructura de datos recursiva, simplemente escribiría algo como:
Lo que tiene sentido; El nodo tiene una lista de hijos o nodos descendientes.
fuente
Item
está, yItem
opera independientemente de las listas.Node
esArrayList
deNode
comoNode
contiene 0 a muchosNode
objetos. Su función es la misma, esencialmente, pero en lugar de ser una lista de objetos similares, tiene una lista de objetos similares. El diseño original de la clase escrita fue la creencia de que cualquieraNode
puede expresarse como un conjunto de subNode
s, lo que hacen ambas implementaciones, pero este es ciertamente menos confuso. +1Children
solo una o dos veces, y generalmente es preferible escribirlo en tales casos por razones de claridad del código.El argumento "horrible para la memoria" es completamente erróneo, pero es objetivamente una "mala práctica". Cuando hereda de una clase, no solo hereda los campos y métodos que le interesan. En cambio, hereda todo . Cada método que declara, incluso si no es útil para usted. Y lo más importante, también hereda todos sus contratos y garantías que ofrece la clase.
El acrónimo SOLID proporciona algunas heurísticas para un buen diseño orientado a objetos. Aquí, el que Nterface Segregación Principio (ISP) y la L iskov Sustitución Pricinple (LSP) tiene algo que decir.
El ISP nos dice que mantengamos nuestras interfaces lo más pequeñas posible. Pero al heredar de
ArrayList
, obtienes muchos, muchos métodos. ¿Tiene algún sentidoget()
,remove()
,set()
(sustituir), oadd()
(insertar) un nodo hijo en un índice en particular? ¿Es sensible aensureCapacity()
la lista subyacente? ¿Qué significa parasort()
un nodo? ¿Realmente se supone que los usuarios de tu clase obtienen unsubList()
? Como no puede ocultar los métodos que no desea, la única solución es tener ArrayList como una variable miembro y reenviar todos los métodos que realmente desee:Si realmente desea todos los métodos que ve en la documentación, podemos pasar al LSP. El LSP nos dice que debemos poder usar la subclase donde se espera la clase principal. Si una función toma un
ArrayList
parámetro como y pasamos nuestroNode
en su lugar, se supone que nada cambiará.La compatibilidad de las subclases comienza con cosas simples como firmas de tipo. Cuando anula un método, no puede hacer que los tipos de parámetros sean más estrictos, ya que eso podría excluir los usos que eran legales con la clase principal. Pero eso es algo que el compilador nos busca en Java.
Pero el LSP es mucho más profundo: tenemos que mantener la compatibilidad con todo lo que promete la documentación de todas las clases e interfaces principales. En su respuesta , Lynn ha encontrado un caso en el que la
List
interfaz (que ha heredado a través deArrayList
) garantiza cómo se supone que funcionan los métodosequals()
yhashCode()
. ParahashCode()
que se les da incluso un algoritmo particular que debe ser implementada con exactitud. Supongamos que ha escrito estoNode
:Esto requiere que el
value
no pueda contribuir alhashCode()
y no pueda influirequals()
. LaList
interfaz, que promete honrar heredando de ella, debenew Node(0).equals(new Node(123))
ser verdadera.Debido a que heredar de las clases hace que sea demasiado fácil romper accidentalmente una promesa que hizo una clase principal, y debido a que generalmente expone más métodos de los que pretendía, generalmente se sugiere que prefiera la composición sobre la herencia . Si debe heredar algo, se sugiere heredar solo las interfaces. Si desea reutilizar el comportamiento de una clase en particular, puede mantenerlo como un objeto separado en una variable de instancia, de esa manera no se le imponen todas sus promesas y requisitos.
A veces, nuestro lenguaje natural sugiere una relación de herencia: un automóvil es un vehículo. Una motocicleta es un vehículo. ¿Debo definir clases
Car
yMotorcycle
que hereden de unaVehicle
clase? El diseño orientado a objetos no se trata de reflejar el mundo real exactamente en nuestro código. No podemos codificar fácilmente las ricas taxonomías del mundo real en nuestro código fuente.Un ejemplo de ello es el problema de modelado empleado-jefe. Tenemos varios correos
Person
electrónicos, cada uno con un nombre y una dirección. UnEmployee
es unPerson
y tiene unBoss
. ABoss
es también aPerson
. Entonces, ¿debería crear unaPerson
clase que sea heredada porBoss
yEmployee
? Ahora tengo un problema: el jefe también es un empleado y tiene otro superior. Entonces parece queBoss
debería extenderseEmployee
. Pero elCompanyOwner
es unBoss
pero no es unEmployee
? Cualquier tipo de gráfico de herencia se descompondrá de alguna manera aquí.OOP no se trata de jerarquías, herencia y reutilización de clases existentes, se trata de generalizar el comportamiento . OOP se trata de "Tengo un montón de objetos y quiero que hagan algo en particular, y no me importa cómo". Para eso están las interfaces . Si implementa la
Iterable
interfaz para ustedNode
porque desea que sea iterable, está perfectamente bien. Si implementa laCollection
interfaz porque desea agregar / eliminar nodos secundarios, etc., está bien. Pero heredar de otra clase porque te da todo lo que no es, o al menos no a menos que lo hayas pensado detenidamente como se describe anteriormente.fuente
Extender un contenedor en sí mismo generalmente se considera una mala práctica. Realmente hay muy pocas razones para extender un contenedor en lugar de solo tener uno. Extender un contenedor de ti mismo solo lo hace extra extraño.
fuente
Además de lo que se ha dicho, hay una razón algo específica de Java para evitar este tipo de estructuras.
El contrato del
equals
método para listas requiere que una lista se considere igual a otro objetoFuente: https://docs.oracle.com/javase/7/docs/api/java/util/List.html#equals(java.lang.Object)
Específicamente, esto significa que tener una clase diseñada como una lista de sí misma puede hacer que las comparaciones de igualdad sean costosas (y los cálculos hash también si las listas son mutables), y si la clase tiene algunos campos de instancia, estos deben ignorarse en la comparación de igualdad .
fuente
En cuanto a la memoria:
Yo diría que esto es una cuestión de perfeccionismo. El constructor predeterminado de se
ArrayList
ve así:Fuente . Este constructor también se usa en Oracle-JDK.
Ahora considere construir una Lista enlazada individualmente con su código. Acaba de aumentar con éxito su consumo de memoria por el factor 10x (para ser precisos, incluso marginalmente más alto). Para los árboles, esto también puede ser bastante malo sin requisitos especiales en la estructura del árbol. Usar una
LinkedList
u otra clase alternativamente debería resolver este problema.Para ser sincero, en la mayoría de los casos esto no es más que un mero perfeccionismo, incluso si ignoramos la cantidad de memoria disponible. A
LinkedList
ralentizaría un poco el código como alternativa, por lo que es una compensación entre el rendimiento y el consumo de memoria de cualquier manera. Aún así, mi opinión personal sobre esto sería no desperdiciar tanta memoria de una manera que se pueda eludir tan fácilmente como esta.EDITAR: aclaración sobre los comentarios (@amon para ser precisos). Esta sección de la respuesta no trata el tema de la herencia. La comparación del uso de la memoria se realiza con una Lista vinculada individualmente y con el mejor uso de la memoria (en implementaciones reales, el factor puede cambiar un poco, pero aún es lo suficientemente grande como para sumar un poco de memoria desperdiciada).
En cuanto a "Mala práctica":
Seguro. Esta no es la forma estándar de implementar un gráfico por una simple razón: un nodo de gráfico tiene nodos secundarios, no es como una lista de nodos secundarios. Expresar con precisión lo que quieres decir en código es una habilidad importante. Ya sea en nombres de variables o estructuras de expresión como esta. Siguiente punto: mantener las interfaces mínimas: ha hecho que todos los métodos
ArrayList
estén disponibles para el usuario de la clase a través de la herencia. No hay forma de alterar esto sin romper toda la estructura del código. En su lugar, almacene laList
variable interna y ponga a disposición los métodos necesarios a través de un método adaptador. De esta manera, puede agregar y eliminar fácilmente la funcionalidad de la clase sin estropear todo.fuente
new Object[0]
usa 4 palabras en total, anew Object[10]
solo usaría 14 palabras de memoria.ArrayList
están eliminando bastante memoria.Esto parece parecerse a la semántica del patrón compuesto . Se utiliza para modelar estructuras de datos recursivas.
fuente
public class Node extends ArrayList<Node> { public string Item; }
es la versión de herencia de tu respuesta. El suyo es más sencillo de razonar, pero ambos logran algo: definen árboles no binarios.