Código hash de ArrayList que se contiene a sí mismo como elemento

38

¿Podemos encontrar el hashcodede un listque se contiene como element?

Sé que esta es una mala práctica, pero esto es lo que preguntó el entrevistador.

Cuando ejecuté el siguiente código arroja un StackOverflowError:

public class Main {
    public static void main(String args[]) {
        ArrayList<ArrayList> a = new ArrayList();
        a.add(a);
        a.hashCode();
    }
}

Ahora aquí tengo dos preguntas:

  1. ¿Por qué hay un StackOverflowError?
  2. ¿Es posible encontrar el código hash de esta manera?
bufón
fuente
77
Porque agrega la Lista a sí mismo. pruebe a.hashCode () sin la instrucción add
Jens
Cuando coloca un objeto en una lista de arrays, está almacenando la referencia al objeto. En su caso, está colocando una bruja ArrayList que es en sí misma referencia.
Vishwa Ratna
Ok, entiendo por qué hay stackoverflow, ¿alguien puede ayudarme a explicar el problema número 2? ¿Cómo encontrar esto?
Joker
99
Como han respondido otros, esto no es posible, por la definición misma de la Listinterfaz, la hashCodelista depende de sus miembros. Dado que la lista es su propio miembro, su código hash depende de su hashCode, que depende de su hashCode... y así sucesivamente, causando una recursión infinita y el StackOverflowErrorque se está encontrando. Ahora la pregunta es: ¿por qué necesita una lista para contenerse? Puedo garantizarle que puede lograr lo que sea que esté tratando de hacer, de una mejor manera, sin requerir una membresía recursiva como esta.
Alexander - Restablece a Mónica el

Respuestas:

36

El código hash para Listimplementaciones conformes se ha especificado en la interfaz :

Devuelve el valor del código hash para esta lista. El código hash de una lista se define como el resultado del siguiente cálculo:

 int hashCode = 1;
 for (E e : list)
     hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());

Esto asegura que eso list1.equals(list2)implica que list1.hashCode()==list2.hashCode()para cualquiera de las dos listas, list1y list2, según lo requerido por el contrato general de Object.hashCode().

Esto no requiere que la implementación se vea exactamente así (consulte Cómo calcular el código hash para una secuencia de la misma manera que List.hashCode () para una alternativa), pero el código hash correcto para una lista que solo se contiene ser un número para el cual x == 31 + xdebe ser true, en otras palabras, es imposible calcular un número conforme.

Holger
fuente
1
@ Holger, Eirc quiere reemplazar el código de toda la función hashCode()para devolver 0. Esto resuelve técnicamente x == 31 + xpero ignora el requisito de que x debe ser al menos 1.
bxk21
44
@EricDuminil el punto de mi respuesta es que el contrato describe una lógica que se ArrayListimplementa literalmente, lo que lleva a una recursión, pero tampoco hay una implementación alternativa conforme. Tenga en cuenta que publiqué mi respuesta en un momento en que el OP ya entendía por qué esta implementación en particular conduce a una StackOverflowError, que se ha abordado en otras respuestas. Así que me concentré en la imposibilidad general de que una implementación conforme se complete en un tiempo finito con un valor.
Holger
2
@pdem, no importa si la especificación utiliza una descripción verbal de un algoritmo, una fórmula, un pseudocódigo o un código real. Como se dijo en la respuesta, el código dado en la especificación no excluye implementaciones alternativas en general. La forma de la especificación no dice nada acerca de si el análisis se realizó o no. La oración de la documentación de la interfaz " Si bien es permisible que las listas se contengan a sí mismas como elementos, se recomienda precaución extrema: los métodos de igualdad y hashCode ya no están bien definidos en dicha lista " sugiere que tal análisis sí sucedió.
Holger
2
@pdem lo estás invirtiendo. Nunca dije que la lista tiene que ser igual debido al código hash. Las listas son iguales, por definición. Arrays.asList("foo")es igual a Collections.singletonList("foo"), es igual a List.of("foo")es igual a new ArrayList<>(List.of("foo")). Todas estas listas son iguales, punto. Entonces, dado que estas listas son iguales, deben tener el mismo código hash. No al revés. Como deben tener el mismo código hash, debe estar bien definido. Independientemente de cómo lo definieron (en Java 2), debe estar bien definido, para ser acordado por todas las implementaciones.
Holger
2
@pdem exactamente, una implementación personalizada que, bien, no se implementa Listo tiene una gran señal de advertencia "no mezclar con listas comunes", se compara con IdentityHashMapsu "¡ Esta clase no es una implementación de Mapa de propósito general! " advertencia. En el primer caso, ya está bien con la implementación, Collectionpero también agrega los métodos de acceso basados ​​en el índice de estilo de lista. Entonces, no existe restricción de igualdad, pero aún funciona sin problemas con otros tipos de colección.
Holger
23

Echa un vistazo a la implementación esquelética del hashCodemétodo en AbstractListclase.

public int hashCode() {
    int hashCode = 1;
    for (E e : this)
        hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());
    return hashCode;
}

Para cada elemento de la lista, esto llama hashCode. En su caso, la lista se tiene como único elemento. Ahora esta llamada nunca termina. El método se llama a sí mismo de forma recursiva y la recursión se sigue enrollando hasta que encuentra el StackOverflowError. Entonces no puedes encontrar el hashCodecamino de esta manera.

Ravindra Ranwala
fuente
Entonces, ¿la respuesta es que no hay forma de encontrar el código hash de esta manera?
Joker
3
Sí, debido a la condición recursiva
springe
No solo eso, se define de esta manera.
Chrylis -en huelga-
14

Ha definido una lista (patológica) que se contiene a sí misma.

¿Por qué hay StackOverflowError?

Según los javadocs (es decir, la especificación), el código hash de a Listse define como una función del código hash de cada uno de sus elementos. Dice:

"El código hash de una lista se define como el resultado del siguiente cálculo:"

int hashCode = 1;
    for (E e : list)
         hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());

Entonces, para calcular el código hash de a, primero debes calcular el código hash de a. Eso es infinitamente recursivo y conduce rápidamente a un desbordamiento de pila.

¿Es posible encontrar código hash de esta manera?

No. Si considera la especificación algorítmica anterior en términos matemáticos, el código hash de un Listque se contiene es una función no computable . No es posible calcularlo de esta manera (usando el algoritmo anterior) ni de ninguna otra manera .

Stephen C
fuente
No sé por qué esta respuesta es más baja que las otras dos, ya que en realidad responde a las preguntas de OP con explicaciones.
Neyt
1
@Neyt algunos usuarios solo leen las respuestas en la parte superior.
Holger
8

No, la documentación tiene una respuesta.

La documentación de la estructura de la Lista establece explícitamente:

Nota: Si bien es permisible que las listas se contengan a sí mismas como elementos, se recomienda precaución extrema: los métodos igual y hashCode ya no están bien definidos en dicha lista.

No hay mucho más que decir más allá de eso: según la especificación de Java, no podrá calcular hashCode para una lista que se contiene a sí misma; otras respuestas entran en detalles por qué es así, pero el punto es que es conocido e intencional.

Pedro es
fuente
1
Usted dijo por qué está fuera de la especificación, por lo que explica que no es un error. Esa era la parte que faltaba en las otras respuestas.
pdem
3

La respuesta de Ravindra da una buena explicación para el punto 1. Para comentar sobre la pregunta 2:

¿Es posible encontrar código hash de esta manera?

Algo es circular aquí. Al menos uno de estos 2 debe estar equivocado en el contexto de este error de desbordamiento de pila:

  • que el código hash de la lista debe tener en cuenta los de sus elementos
  • que está bien que una lista sea su propio elemento

Ahora, debido a que estamos tratando con un ArrayList, el primer punto es fijo. En otras palabras, tal vez necesite una implementación diferente para poder calcular significativamente un código hash de una lista recursiva ... Se podría extender ArrayListy omitir la adición de códigos hash de elementos, algo así como

for (E e : this)
  if(e == this) continue; //contrived rules
  hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());

Usando tal clase en lugar de ArrayList, podrías.

Con ArrayList, el segundo punto está mal. Entonces, si el entrevistador quería decir "¿Es posible encontrar código hash de esta manera (con una lista de matriz)?" , entonces la respuesta es no, porque eso es absurdo.

ernest_k
fuente
1
El cálculo código hash es un mandato de la Listcontrato . Ninguna implementación válida puede saltarse a sí misma. A partir de la especificación, puede deducir que si encuentra un intnúmero para el cual x == 31 + xes true, entonces puede implementar un atajo válido ...
Holger
No entendí muy bien lo que @Holger estaba diciendo. Pero hay 2 problemas con la solución: primero: esto resuelve el problema solo cuando esta lista es un elemento en sí mismo y no si la lista es un elemento de un elemento en sí mismo (capas más profundas de recursión) Segundo: si la lista se omitió podría ser igual a una lista vacía.
Jonas Michel
1
@JonasMichel No propuse una solución. Solo estoy debatiendo filosóficamente sobre lo absurdo de la pregunta 2. Si debemos calcular un código hash para dicha lista, entonces no funcionará a menos que eliminemos la restricción de una lista de matriz (y Holger lo refuerza diciendo que Listdicta formalmente la lógica del código hash que deben cumplir las implementaciones)
ernest_k
Mi comprensión (limitada) es que Listproporciona un cálculo de código hash, pero que podríamos anularlo. El único requisito real es el de los códigos hash generales: si los objetos son iguales, entonces los códigos hash deben ser iguales. Esta implementación sigue ese requisito.
Teepeemm
1
@Teepeemm Listes una interfaz. Define un contrato , no proporciona una implementación. Por supuesto, el contrato cubre tanto la igualdad como el código hash. Dado que el contrato general de igualdad es que tiene que ser simétrico, si cambia la implementación de una lista, tendría que cambiar todas las implementaciones de listas existentes, de lo contrario, incluso rompería el contrato fundamental java.lang.Object.
Holger
1

Porque cuando llamas a la misma función con la misma función, creará una condición de recursión que nunca termina. Y para evitar esta operación JAVA regresarájava.lang.StackOverflowError

A continuación se muestra un código de ejemplo que explica un escenario similar:

public class RefTest {

    public static void main(String... strings) {
        RefTest rf = new RefTest();
        rf.message(message("test"));
    }

    public static String message2(String s){
        return message(s);
    }

    public static String message(String s){
        return message2(s);
    }

}   
Simmant
fuente