Ensayo sobre el estereotipo del contenedor de colección 2023: preguntas de la entrevista

contenedor de recogida

Descripción general del contenedor de recolección

¿Qué es la colección?

**Marco de colección:** Un contenedor para almacenar datos.

El marco de colección es una arquitectura estándar unificada para expresar y manipular colecciones. Cualquier marco de recopilación incluye tres partes principales: interfaz externa, implementación de la interfaz y algoritmo para las operaciones de recopilación.

**Interfaz:** representa el tipo de datos abstractos de la colección. La interfaz nos permite operar la colección sin tener que prestar atención a la implementación específica, logrando así "polimorfismo". En los lenguajes de programación orientados a objetos, las interfaces se utilizan a menudo para formar especificaciones.

**Implementación: **La implementación específica de la interfaz de recopilación es una estructura de datos altamente reutilizable.
**Algoritmo:** Un método para realizar algún cálculo útil en un objeto que implementa una interfaz en un determinado marco de colección, como búsqueda, clasificación, etc. Estos algoritmos suelen ser polimórficos, porque el mismo método puede comportarse de manera diferente cuando varias clases implementan la misma interfaz. De hecho, los algoritmos son funciones reutilizables. Reduce el esfuerzo de programación.

El marco de colecciones le permite centrarse en las partes importantes de su programa al proporcionar estructuras de datos y algoritmos útiles, en lugar de centrarse en el diseño de bajo nivel para que el programa funcione correctamente.

Con esta sencilla interoperabilidad entre API no relacionadas, se ahorra tener que escribir mucho código para adaptar objetos o convertir código para combinar estas API. Mejora la velocidad y la calidad del programa.

Características de la colección

Hay dos características principales de las colecciones:

  • Los objetos encapsulan datos y es necesario almacenar más objetos. Las colecciones se utilizan para almacenar objetos.
  • Si se determina el número de objetos, se puede utilizar una matriz y, si el número de objetos es incierto, se puede utilizar una colección. Porque las colecciones tienen una longitud variable.

La diferencia entre colecciones y matrices.

  • Las matrices son de longitud fija; las colecciones son de longitud variable.
  • Las matrices pueden almacenar tipos de datos básicos así como tipos de datos de referencia; las colecciones solo pueden almacenar tipos de datos de referencia.
  • Los elementos almacenados en una matriz deben ser del mismo tipo de datos; los objetos almacenados en una colección pueden ser de diferentes tipos de datos.

**Estructura de datos:** es la forma de almacenar datos en el contenedor.

Hay muchos tipos de contenedores de recogida. Debido a que las características de cada contenedor son diferentes, el principio es que la estructura de datos interna de cada contenedor es diferente.

En el proceso de extracción continua hacia arriba del recipiente de recogida aparece un sistema de recogida. Principios en el uso de un sistema: consulte el contenido de nivel superior. Crea objetos subyacentes.

Beneficios de utilizar el marco de colecciones

  1. capacidad de autocrecimiento;
  2. Proporciona estructuras de datos y algoritmos de alto rendimiento, lo que facilita la codificación y mejora la velocidad y la calidad del programa; 3
  3. Permitir la interoperabilidad entre diferentes API y las colecciones se pueden pasar de una API a otra;
  4. La colección se puede ampliar o reescribir fácilmente para mejorar la reutilización y operatividad del código.
  5. Al utilizar las clases de colección que vienen con el JDK, puede reducir el costo de mantenimiento del código y el aprendizaje de nuevas API.

¿Cuáles son las clases de colección más utilizadas?

La interfaz de Mapa y la interfaz de Colección son las interfaces principales de todos los marcos de colección:

  1. Las subinterfaces de la interfaz Colección incluyen: Interfaz Establecer e interfaz Lista
  2. Las clases de implementación de la interfaz Map incluyen principalmente: HashMap, TreeMap, Hashtable, ConcurrentHashMap y Properties, etc.
  3. Las clases de implementación de la interfaz Set incluyen principalmente: HashSet, TreeSet, LinkedHashSet, etc.
  4. Las clases de implementación de la interfaz List incluyen principalmente: ArrayList, LinkedList, Stack y Vector, etc.

¿Cuál es la diferencia entre Lista, Conjunto y Mapa? ¿La lista, el conjunto y el mapa heredan de la interfaz de colección? ¿Cuáles son las características de cada una de las tres interfaces de Lista, Mapa y Conjunto al acceder a elementos?

[Error en la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo anti-limpieza, se recomienda guardar la imagen y cargarla directamente (img-nX090D2S-1692508006551) (02-Preguntas de la entrevista sobre el contenedor de recopilación de Java (última versión de 2020) - punto clave.assets/Collection.png)]

[Error en la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo anti-limpieza, se recomienda guardar la imagen y cargarla directamente (img-4o9DbADS-1692508006552) (02-Preguntas de la entrevista del contenedor de recopilación de Java (última versión de 2020) - Activos.clave/Mapa.png)]

Los contenedores Java se dividen en dos categorías: Colección y Mapa. ​​Las subinterfaces de Colección incluyen tres subinterfaces: Conjunto, Lista y Cola. Comúnmente usamos Set y List, y la interfaz Map no es una subinterfaz de colección.

La colección de colección tiene principalmente dos interfaces: Lista y Conjunto.

  • Lista: un contenedor ordenado (el orden en que se almacenan los elementos en la colección es el mismo que el orden en que se extraen), los elementos se pueden repetir, se pueden insertar varios elementos nulos y los elementos tienen índices. Las clases de implementación más utilizadas son ArrayList, LinkedList y Vector.
  • Conjunto: un contenedor desordenado (el orden de almacenamiento y retiro puede ser inconsistente), que no puede almacenar elementos duplicados, solo permite almacenar un elemento nulo y se debe garantizar la unicidad de los elementos. Las clases de implementación comunes de la interfaz Set son HashSet, LinkedHashSet y TreeSet.

Map es una colección de pares clave-valor que almacena el mapeo entre claves, valores y valores. La clave está desordenada y es única; el valor no requiere orden y permite la duplicación. El mapa no hereda de la interfaz de la Colección. Al recuperar elementos de la colección del Mapa, siempre que se proporcione el objeto clave, se devolverá el objeto de valor correspondiente.

Clases de implementación comunes de Map: HashMap, TreeMap, HashTable, LinkedHashMap, ConcurrentHashMap

La estructura de datos subyacente del marco de recopilación.

Recopilación

**Lista **

  • Lista de matrices: matriz de objetos
  • Vector: matriz de objetos
  • LinkedList: lista enlazada circular bidireccional

**Colocar **

  • HashSet (desordenado, único): implementado en base a HashMap, la capa subyacente usa HashMap para almacenar elementos
  • LinkedHashSet: LinkedHashSet hereda de HashSet y su interior se realiza a través de LinkedHashMap. Es un poco similar al LinkedHashMap que dijimos antes, que se implementa internamente en base a Hashmap, pero todavía hay una pequeña diferencia.
  • TreeSet (ordenado, único): árbol rojo-negro (árbol binario ordenado autoequilibrado). Mapa
  • HashMap: antes de JDK1.8, HashMap se compone de matriz + lista vinculada. La matriz es el cuerpo principal de HashMap, y la lista vinculada existe principalmente para resolver conflictos hash ("método de cremallera" para resolver conflictos). Después de JDK1.8, los conflictos de hash se
    resuelven Cuando la longitud de la lista vinculada es mayor que el umbral (8 de forma predeterminada), la lista vinculada se convierte en un árbol rojo-negro para reducir el tiempo de búsqueda.
  • LinkedHashMap: LinkedHashMap hereda de HashMap, por lo que su capa inferior todavía se basa en la estructura hash de cremallera, que se compone de matrices y listas vinculadas o árboles rojo-negro. Además, LinkedHashMap agrega una lista doblemente vinculada sobre la base de la estructura anterior, de modo que la estructura anterior pueda mantener el orden de inserción de los pares clave-valor. Al mismo tiempo, al realizar las operaciones correspondientes en la lista vinculada, se realiza la lógica relacionada con la secuencia de acceso.
  • HashTable: compuesto por matriz + lista vinculada, la matriz es el cuerpo principal de HashMap y la lista vinculada existe principalmente para resolver conflictos de hash.
  • TreeMap: árbol rojo-negro (árbol binario ordenado autoequilibrado)

¿Qué clases de colección son seguras para subprocesos?

  • vector: Tiene un mecanismo de sincronización más (seguridad de subprocesos) que arraylist, debido a su baja eficiencia, no se recomienda usarlo ahora. En las aplicaciones web, especialmente en las páginas de primer plano, a menudo se da prioridad a la eficiencia (velocidad de respuesta de la página).
  • statck: clase de pila, primero en entrar, último en salir.
  • hashtable: es más seguro para subprocesos que hashmap.
  • enumeración: enumeración, equivalente a iterador.

¿El mecanismo de falla rápida de la colección Java es "rápido"?

Es un mecanismo de detección de errores para colecciones de Java. Cuando varios subprocesos realizan cambios estructurales en la colección, puede ocurrir un mecanismo de falla rápida.

Por ejemplo: supongamos que hay dos subprocesos (hilo 1, subproceso 2), el subproceso 1 atraviesa los elementos de la colección A a través del Iterador y, en algún momento, el subproceso 2 modifica la estructura de la colección A (es una modificación de la estructura, no una simple Modifique el contenido del elemento de la colección), luego el programa arrojará una ConcurrentModificationException en este momento, lo que dará como resultado un mecanismo de falla rápida.

Motivo: el iterador accede directamente al contenido de la colección durante el recorrido y utiliza una variable modCount durante el recorrido. Si el contenido de la colección cambia durante el recorrido, se cambiará el valor de modCount. Siempre que el iterador utilice hashNext()/next() antes de atravesar el siguiente elemento, comprobará si la variable modCount es el valor esperado de modCount y, de ser así, volverá al recorrido; de lo contrario, lanzará una excepción y finalizará el recorrido.

Solución:

  1. Durante el proceso transversal, se agrega sincronizado a todos los lugares que implican cambiar el valor de modCount.
  2. Utilice CopyOnWriteArrayList para reemplazar ArrayList

¿Cómo garantizar que una colección no se pueda modificar?

Puede utilizar el método Collections.unmodifiableCollection(Collection c) para crear una colección de solo lectura, de modo que cualquier operación que cambie la colección genere una excepción Java.lang.UnsupportedOperationException. El código de muestra es el siguiente:

 List<String> list = new ArrayList<>(); 
 list. add("x"); 
 Collection<String> clist = Collections. unmodifiableCollection(list); 
 clist. add("y"); // 运行时此行报错 
 System. out. println(list. size()); 

Interfaz de colección

Interfaz de lista

¿Qué es el iterador?

La interfaz Iterator proporciona una interfaz para atravesar cualquier colección. Podemos obtener instancias de iterador de una Colección usando el método iterador. Los iteradores reemplazan a la enumeración en el marco de colección de Java y los iteradores permiten a la persona que llama eliminar elementos durante la iteración.

¿Cómo utilizar el iterador? ¿Cuáles son las características?

El código de uso del iterador es el siguiente:

1	List<String> list = new ArrayList
2	Iterator<String> it = list. iterator
3	while(it. hasNext()){
4	String obj = it. next();
5	System. out. println(obj);
6	}

La característica de Iterator es que solo puede atravesar en una dirección, pero es más seguro porque puede garantizar que en el recorrido actual

Se lanza una excepción ConcurrentModificationException cuando se cambian los elementos de la colección.

¿Cómo eliminar elementos de la Colección mientras se recorre?

La única forma correcta de modificar una Colección mientras se itera es utilizar el método Iterator.remove(), de la siguiente manera:

1   Iterator<Integer> it = list.iterator();
2	while(it.hasNext()){
3	*// do something*
4	it.remove();5	}

Un código de error común es el siguiente:

1	for(Integer i : list){
2	list.remove(i)
3	}

La ejecución del código de error anterior informará una excepción ConcurrentModificationException. Esto se debe a que cuando se utiliza la instrucción foreach(for(Integer i: list)), se genera automáticamente un iterador para recorrer la lista, pero al mismo tiempo Iterator.remove() modifica la lista. Java generalmente no permite que un hilo modifique una colección mientras otro hilo la atraviesa.

¿Cuál es la diferencia entre Iterador y ListIterator?

  • Iterator puede atravesar colecciones Set y List, mientras que ListIterator solo puede atravesar List.
  • Iterator solo puede atravesar en una dirección, mientras que ListIterator puede atravesar en ambas direcciones (recorrido hacia adelante/atrás).
  • ListIterator implementa la interfaz Iterator y luego agrega algunas funciones adicionales, como agregar un elemento, reemplazar un elemento y obtener la posición de índice del elemento anterior o posterior.

¿Cuáles son las diferentes formas de iterar sobre una Lista? ¿Cuál es el principio de implementación de cada método? ¿Cuáles son las mejores prácticas para el recorrido de listas en Java?

Los métodos transversales son los siguientes:

  1. El bucle for atraviesa basándose en un contador. Mantenga un contador fuera de la colección, luego lea los elementos en cada posición por turno y deténgase cuando se lea el siguiente elemento.

  2. Recorrido del iterador, Iterador. Iterator es un patrón de diseño orientado a objetos, cuyo propósito es proteger las características de diferentes colecciones de datos y unificar la interfaz de las colecciones atravesadas. Java admite el patrón Iterador en Colecciones.

  3. foreach recorre. Foreach también se implementa en forma de Iterador, y no es necesario declarar explícitamente Iterador o contador al usarlo. La ventaja es que el código es conciso y propenso a errores; la desventaja es que solo puede realizar un recorrido simple y no puede manipular conjuntos de datos durante el proceso de recorrido, como la eliminación y el reemplazo.

Mejores prácticas: el marco de colecciones de Java proporciona una interfaz RandomAccess para marcar si la implementación de List admite acceso aleatorio.

  • Si una colección de datos implementa esta interfaz, significa que admite acceso aleatorio y la complejidad de tiempo promedio de lectura de elementos por posición es O (1), como ArrayList.
  • Si esta interfaz no está implementada, significa que no se admite el acceso aleatorio, como LinkedList. El método recomendado es que la lista que admite acceso aleatorio se pueda recorrer mediante un bucle for; de lo contrario, se recomienda atravesar mediante Iterator o foreach.

Hable sobre las ventajas y desventajas de ArrayList.

Las ventajas de ArrayList son las siguientes:

  • La capa inferior de ArrayList se implementa como una matriz, que es un modo de acceso aleatorio. ArrayList implementa la interfaz RandomAccess, por lo que la búsqueda es muy rápida.
  • ArrayList es muy conveniente al agregar elementos uno por uno de forma secuencial.

Las desventajas de ArrayList son las siguientes:

  • Al eliminar un elemento, se requiere una operación de copia del elemento. Si hay muchos elementos para copiar, consumirá rendimiento.
  • Al insertar elementos, también es necesario realizar una operación de copia de elementos, y las desventajas son las mismas que las anteriores.

ArrayList es más adecuado para escenarios de suma secuencial y acceso aleatorio.

¿Cómo realizar la conversión entre matriz y lista?

Array a lista: utilice Arrays.asList(array) para convertir.

De lista a matriz: utilice el método toArray() que viene con List. Ejemplo de código:

1	// list to array
2	List<String> list = new ArrayList<String>();
3	list.add("123");
4	list.add("456");
5	list.toArray();
6
7	// array to list
8	String[] array = new String[]{"123","456"};
9	Arrays.asList(array);

¿Cuál es la diferencia entre ArrayList y LinkedList?

  • Implementación de la estructura de datos: ArrayList es la implementación de la estructura de datos de una matriz dinámica y LinkedList es la implementación de la estructura de datos de una lista doblemente enlazada.
  • Eficiencia de acceso aleatorio: ArrayList es más eficiente que LinkedList en acceso aleatorio, porque LinkedList es un método de almacenamiento de datos lineal, por lo que debe mover el puntero para buscar de adelante hacia atrás.
  • Eficiencia de adición y eliminación: LinkedList es más eficiente que ArrayList en operaciones de adición y eliminación sin encabezado y cola, porque las operaciones de adición y eliminación de ArrayList afectarán los subíndices de otros datos en la matriz.
  • Ocupación del espacio de memoria: LinkedList ocupa más memoria que ArrayList, porque los nodos LinkedList almacenan dos referencias además de almacenar datos, una que apunta al elemento anterior y otra que apunta al siguiente elemento.
  • Seguridad de subprocesos: tanto ArrayList como LinkedList no están sincronizados, es decir, la seguridad de subprocesos no está garantizada;

En general, se recomienda ArrayList cuando los elementos de la colección deben leerse con frecuencia y LinkedList cuando hay muchas operaciones de inserción y eliminación.

Suplemento: lista doblemente enlazada basada en estructura de datos

Una lista doblemente enlazada también se llama lista doblemente enlazada, que es una especie de lista enlazada. Cada nodo de datos tiene dos punteros, que apuntan al sucesor directo y al predecesor directo, respectivamente. Por lo tanto, comenzando desde cualquier nodo en la lista doblemente enlazada, puede acceder fácilmente a su nodo predecesor y a su nodo sucesor.

¿Cuál es la diferencia entre ArrayList y Vector?

Ambas clases implementan la interfaz Lista (la interfaz Lista hereda la interfaz Colección) y ambas son colecciones ordenadas.

Seguridad de subprocesos: Vector utiliza Synchronized para lograr la sincronización de subprocesos, que es segura para subprocesos, y

ArrayList no es seguro para subprocesos.

Rendimiento: ArrayList es mejor que Vector en términos de rendimiento.

Expansión: Tanto ArrayList como Vector pueden ajustar dinámicamente la capacidad de acuerdo con las necesidades reales, pero en

La expansión del vector se duplicará cada vez, mientras que ArrayList solo aumentará en un 50%.

Todos los métodos de la clase Vector son sincrónicos. Se puede acceder de forma segura a un par de vectores mediante dos subprocesos

Me gusta, pero si un hilo accede a Vector, el código dedicará mucho tiempo a operaciones sincrónicas.

Arraylist no está sincronizado, por lo que se recomienda utilizar Arraylist cuando no se requiere seguridad para subprocesos.

Al insertar datos, ¿cuál es más rápido, ArrayList, LinkedList o Vector? ¿Explique el rendimiento del almacenamiento y las características de ArrayList, Vector, LinkedList?

Las implementaciones subyacentes de ArrayList, LinkedList y Vector utilizan matrices para almacenar datos. formación

La cantidad de elementos es mayor que los datos almacenados reales para agregar e insertar elementos. Todos permiten la indexación directa de elementos por número de serie, pero la inserción de elementos implica operaciones de memoria, como el movimiento de elementos de la matriz, por lo que la indexación de datos es rápida y la inserción de datos es lenta. .

Debido a que los métodos en Vector se modifican con sincronización, Vector es un contenedor seguro para subprocesos, pero su rendimiento es peor que el de ArrayList.

LinkedList utiliza una lista doblemente vinculada para el almacenamiento. La indexación de datos por número de serie requiere un recorrido hacia adelante o hacia atrás, pero al insertar datos, solo necesita registrar los elementos anteriores y posteriores del elemento actual, por lo que LinkedList se inserta más rápido.

¿Cómo utilizar ArrayList en un escenario multiproceso?

ArrayList no es seguro para subprocesos. Si encuentra un escenario de subprocesos múltiples, puede usar Colecciones

El método sincronizadoList lo convierte en un contenedor seguro para subprocesos antes de usarlo. Por ejemplo así:

1	List<String> synchronizedList = Collections.synchronizedList(list);
2	synchronizedList.add("aaa");
3	synchronizedList.add("bbb");
4
5	for (int i = 0; i < synchronizedList.size(); i++) {
6	System.out.println(synchronizedList.get(i));
7	}

¿Por qué elementData de ArrayList está decorado con transitorio? Las matrices en ArrayList se definen de la siguiente manera:

1 private transient Object[] elementData;

Mire la definición de ArrayList nuevamente:

1	public class ArrayList<E> extends AbstractList<E>
2	implements List<E>, RandomAccess, Cloneable, java.io.Serializable

Puede ver que ArrayList implementa la interfaz Serializable, lo que significa que ArrayList admite secuencias.

cambiar. La función de transitorio es decir que no se espera que la matriz elementData se serialice y se reescribe la implementación de writeObject:

1 private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOE xception{ 
2  *// Write out element count, and any hidden stuff* 
3  int expectedModCount = modCount; 
4  s.defaultWriteObject(); 
5  *// Write out array length* 
6  s.writeInt(elementData.length); 
7  *// Write out all elements in the proper order.* 
8  for (int i=0; i<size; i++) 
9  s.writeObject(elementData[i]); 
10  if (modCount != expectedModCount) { 
11  throw new ConcurrentModificationException();
12 }

Para cada serialización, primero llame al método defaultWriteObject () para serializar los elementos no transitorios en ArrayList, luego recorra elementData y solo serialice los elementos almacenados, lo que no solo acelera la velocidad de serialización, sino que también reduce la velocidad de serialización. tamaño.

La diferencia entre Lista y Conjunto

Lista y Conjunto se heredan de la interfaz de Colección

Características de la lista: un contenedor ordenado (el orden en que se almacenan los elementos en la colección es el mismo que el orden en que se extraen), los elementos se pueden repetir, se pueden insertar varios elementos nulos y los elementos tienen índices. Las clases de implementación más utilizadas son ArrayList,

LinkedList y Vector.

Características del conjunto: un contenedor desordenado (el orden de almacenamiento y retiro puede ser inconsistente), que no puede almacenar elementos duplicados, solo permite almacenar un elemento nulo y se debe garantizar la unicidad de los elementos. Las clases de implementación comunes de la interfaz Set son

HashSet, LinkedHashSet y TreeSet.

Además, List admite bucles for, es decir, el recorrido a través de subíndices, y también se pueden usar iteradores, pero set solo puede usar iteración, porque está desordenado y los subíndices no se pueden usar para obtener el valor deseado.

Comparación entre conjunto y lista

Conjunto: la recuperación de elementos es ineficiente, la eliminación y la inserción son eficientes, y la inserción y eliminación no harán que cambien las posiciones de los elementos.

Lista: similar a una matriz, una Lista puede crecer dinámicamente, es eficiente para encontrar elementos y es ineficiente para insertar y eliminar elementos, porque hará que la posición de otros elementos cambie

Establecer interfaz

¿Cuéntame sobre el principio de implementación de HashSet?

HashSet se implementa en base a HashMap y el valor de HashSet se almacena en la clave de HashMap.

El valor de HashMap está unificado como PRESENTE, por lo que la implementación de HashSet es relativamente simple y la operación del HashSet relacionado se completa básicamente llamando directamente a los métodos relevantes del HashMap subyacente.

HashSet no permite valores duplicados.

¿Cómo comprueba HashSet si hay duplicados? ¿Cómo garantiza HashSet que los datos no sean repetibles?

Al agregar elementos () al HashSet, la base para juzgar si los elementos existen no es solo comparar el valor hash, sino también combinar la comparación con el método equles.

El método add() en HashSet utilizará el método put() de HashMap.

La clave de HashMap es única. Se puede ver en el código fuente que el valor agregado a HashSet se usa como clave de HashMap, y si el K/V es el mismo en HashMap, el anterior se sobrescribirá con el nuevo. V

V, y devuelve la antigua V. Por lo tanto, no se repetirá (HashMap compara si las claves son iguales comparando primero el código hash y luego las iguales).

Lo siguiente es parte del código fuente de HashSet:

1	private static final Object PRESENT = new Object();
2	private transient HashMap<E,Object> map;
3
4	public HashSet() {
<>
5	map = new HashMap ();
6	}
7
8	public boolean add(E e) {
9	// 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
10	return map.put(e, PRESENT)==null;
11	}

Disposiciones relevantes de hashCode() y equals():

  1. Si dos objetos son iguales, el código hash también debe ser el mismo

  2. Dos objetos son iguales, devuelve verdadero para ambos métodos iguales

  3. Dos objetos tienen el mismo valor de código hash, no son necesariamente iguales

  4. En resumen, si se ha anulado el método igual, también se debe anular el método hashCode.

  5. El comportamiento predeterminado de hashCode() es generar valores únicos para los objetos en el montón. Si no se anula hashCode(), no habrá dos objetos de esta clase iguales de todos modos (incluso si los dos objetos apuntan a los mismos datos).

La diferencia entre == e igual

  1. == es juzgar si dos variables o instancias apuntan al mismo espacio de memoria igual es juzgar si los valores de los espacios de memoria apuntados por dos variables o instancias son los mismos

  2. Significa comparar la dirección de memoria igual () es comparar el contenido de la cadena 3.Si la guía es igual o no es igual() se refiere a si los valores son iguales

La diferencia entre HashSet y HashMap

mapa hash Conjunto de hash
Implementada la interfaz del mapa. Implementa la interfaz Set.
Almacenar pares clave-valor almacenar solo objetos
Llame a put() para agregar elementos al mapa. Llame al método add() para agregar elementos al conjunto
HashMap usa la clave (Key) para calcular Hashcode HashSet utiliza objetos miembro para calcular el valor del código hash. El código hash puede ser el mismo para dos objetos, por lo que el método equals() se utiliza para juzgar la igualdad de los objetos. Si los dos objetos son diferentes, entonces devuelve false
HashMap es más rápido que HashSet porque usa claves únicas para obtener objetos HashSet es más lento que HashMap

Cola

¿Qué es BlockingQueue?

Un java.util.concurrent.BlockingQueue es una cola que espera a que la cola no esté vacía al recuperar o eliminar un elemento, y espera espacio libre en la cola al agregar un elemento. La interfaz BlockingQueue es parte del marco de colección de Java y se utiliza principalmente para implementar el patrón productor-consumidor. No necesitamos preocuparnos por esperar a que el productor tenga espacio disponible, o que el consumidor tenga objetos disponibles, porque todo se maneja en la clase de implementación BlockingQueue. Java proporciona la implementación de BlockingQueue centralizado, como ArrayBlockingQueue,

LinkedBlockingQueue, PriorityBlockingQueue, SynchronousQueue, etc. ¿Cuál es la diferencia entre poll() y remove() en la cola?

  • El mismo punto: ambos devuelven el primer elemento y eliminan el objeto devuelto en la cola.
  • La diferencia: si no hay ningún elemento, poll() devolverá nulo y remove() arrojará directamente NoSuchElementException.

Ejemplo de código:

1	Queue<String> queue = new LinkedList<String>();
2	queue. offer("string"); // add
3	System. out. println(queue. poll());
4	System. out. println(queue. remove());
5	System. out. println(queue. size());

Interfaz de mapa

¿Cuénteme sobre el principio de implementación de HashMap?

Descripción general de HashMap: HashMap es una implementación asincrónica de la interfaz Map basada en una tabla hash. Esta implementación proporciona todas las operaciones de mapas opcionales y permite valores nulos y claves nulas. Esta clase no garantiza el orden del mapeo y, en particular, no garantiza que este orden sea inmutable.

La estructura de datos de HashMap: en el lenguaje de programación Java, hay dos estructuras básicas, una es una matriz y la otra es un puntero analógico (referencia). Todas las estructuras de datos se pueden construir utilizando estas dos estructuras básicas, y HashMap no. excepción. HashMap es en realidad una estructura de datos de "hash de lista vinculada", que es una combinación de una matriz y una lista vinculada.

HashMap se implementa en base al algoritmo Hash

  1. Cuando colocamos elementos en Hashmap, usamos el código hash de la clave para volver a calcular el subíndice del elemento del objeto actual en la matriz.

  2. Al almacenar, si aparece una clave con el mismo valor hash, existen dos situaciones en este momento. (1) Si la fase clave

Si la clave es la misma, sobrescriba el valor original; (2) Si la clave es diferente (se produce un conflicto), coloque la clave-valor actual en la lista vinculada

  1. Al obtenerlo, busque directamente el subíndice correspondiente al valor hash y determine además si las claves son las mismas para encontrar el valor correspondiente.

  2. Después de comprender el proceso anterior, no es difícil comprender cómo HashMap resuelve el problema de los conflictos de hash. El núcleo es utilizar el método de almacenamiento de matriz y luego colocar el objeto de la clave en conflicto en la lista vinculada. Una vez que se encuentra un conflicto , se realizan más comparaciones en la lista vinculada.

Cabe señalar que la implementación de HashMap se ha optimizado en Jdk 1.8. Cuando los datos de nodos en la lista vinculada superan los ocho

Después de eso, la lista vinculada se convertirá en un árbol rojo-negro para mejorar la eficiencia de la consulta, del O (n) original al O (logn)

¿Cuáles son las diferencias entre HashMap en JDK1.7 y JDK1.8? La implementación subyacente de HashMap

En Java, existen dos estructuras de datos relativamente simples para almacenar datos: matrices y listas vinculadas. Las características de las matrices son: fáciles de abordar, difíciles de insertar y eliminar; las características de las listas vinculadas son: difíciles de abordar, pero fáciles de insertar y eliminar; por eso combinamos matrices y listas vinculadas para aprovechar sus respectivas ventajas, utilizando un método llamado método zip puede resolver conflictos de hash.

Antes de JDK1.8

Antes de JDK1.8, se utilizaba el método de cremallera. Método de cremallera: combina lista enlazada y matriz. Es decir, cree una matriz de listas vinculadas y cada celda de la matriz es una lista vinculada. Si hay un conflicto de hash, simplemente agregue el valor en conflicto a la lista vinculada.

[Error en la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo anti-limpieza, se recomienda guardar la imagen y cargarla directamente (img-ry5BaX0U-1692508006553) (02-Preguntas de la entrevista sobre el contenedor de recopilación de Java (última versión de 2020) - punto clave.assets/image- 20201109162525820.png)]

Después de JDK1.8

En comparación con la versión anterior, jdk1.8 ha realizado grandes cambios en la resolución de conflictos de hash. Cuando la longitud de la lista vinculada es mayor que el umbral (8 por defecto), la lista vinculada se convierte en un árbol rojo-negro para reducir el tiempo de búsqueda.

[Error en la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo anti-limpieza, se recomienda guardar la imagen y cargarla directamente (img-LtupEEp7-1692508006553) (02-Preguntas de la entrevista sobre el contenedor de recopilación de Java (última versión de 2020) - punto clave.assets/clip_image001.jpg)]

Comparación entre JDK1.7 y JDK1.8

JDK1.8 resuelve u optimiza principalmente los siguientes problemas:

  1. optimización de expansión de cambio de tamaño

  2. Se introduce el árbol rojo-negro, el propósito es evitar que la lista única vinculada sea demasiado larga y afecte la eficiencia de la consulta; consulte el algoritmo del árbol rojo-negro

  3. Se resolvió el problema del bucle infinito de subprocesos múltiples, pero aún no es seguro para subprocesos y puede causar pérdida de datos cuando se ejecuta en subprocesos múltiples.

diferente JDK 1.7 JDK 1.8
estructura de almacenamiento matriz + lista enlazada Matriz + lista enlazada + árbol rojo-negro
método de inicialización Función separada: inflarTable() Integrado directamente en la función de expansión resize()
Método de cálculo del valor hash Procesamiento de perturbaciones = 9 perturbaciones = operaciones de 4 bits + 5 operaciones XOR Procesamiento de perturbaciones = 2 perturbaciones = operación de 1 bit + 1 operación XOR
Reglas para almacenar datos. Cuando no hay conflicto, almacena una matriz; cuando hay un conflicto, almacena una lista vinculada Cuando no hay conflicto, almacene una matriz; conflicto y longitud de lista enlazada < 8: almacene lista enlazada única; conflicto y longitud de lista enlazada > 8: árbol y almacene árbol rojo-negro
Insertar método de datos Método de inserción del encabezado (primero mueva los datos en la posición original al último bit y luego inserte los datos en esta posición) Método de inserción de cola (insertar directamente al final de la lista vinculada/árbol rojo-negro)
Método de cálculo de la ubicación de almacenamiento después de la expansión de capacidad. Todos los cálculos se realizan de acuerdo con el método original (es decir, hashCode ->> función de perturbación ->> (h&length h-1)) Calcule de acuerdo con la ley después de la expansión de capacidad (es decir, posición después de la expansión de capacidad = posición original o posición original + capacidad anterior)

¿El proceso específico del método put de HashMap?

Cuando lo ponemos, primero calculamos el valor hash de la clave. Aquí se llama al método hash. El método hash en realidad hace que key.hashCode() y key.hashCode() >>> 16 realicen la operación XOR, y los 16 bits altos son lleno de 0, uno El número y 0 son XOR sin cambios, por lo que la función aproximada de la función hash es: los 16 bits altos permanecen sin cambios, los 16 bits bajos y los 16 bits altos permanecen sin cambios.

16 bits realiza un XOR para reducir las colisiones. Según el comentario de la función, debido a que el tamaño de la matriz del depósito es

Potencia de 2, calcule el índice de subíndice = (table.length - 1) & hash, si no se realiza ningún procesamiento de hash, equivale a solo unos pocos bits bajos para que el hash surta efecto. Para reducir la colisión de los hash, el diseñador consideró exhaustivamente Después de la velocidad, la función y la calidad, use XOR alto de 16 bits y bajo de 16 bits para manejar y reducir las colisiones de manera simple, y

En JDK8, se utiliza una estructura de árbol con complejidad O (logn) para mejorar el rendimiento en caso de colisión.

Diagrama de flujo de ejecución del método putVal

[Falló la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo anti-sanguijuela, se recomienda guardar la imagen y cargarla directamente (img-6T3PqxSu-1692508006554) (02-Preguntas de la entrevista del contenedor de recopilación de Java (última versión de 2020) - focus.assets/image- 20201109162901394.png)]

1 public V put(K key, V value) {
2 return putVal(hash(key), key, value, false, true);
3 }
4
5 static final int hash(Object key) {
6 int h;
7 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
8 }
9
10 //实现Map.put和相关方法
11 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
12 boolean evict) {
13 Node<K,V>[] tab; Node<K,V> p; int n, i;
14 // 步骤①:tab为空则创建
15 // table未初始化或者长度为0,进行扩容
16 if ((tab = table) == null || (n = tab.length) == 0)
17 n = (tab = resize()).length;
18 // 步骤②:计算index,并对null做处理
19 // (n ‐ 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这
个结点是放在数组中)
20 if ((p = tab[i = (n ‐ 1) & hash]) == null)
21 tab[i] = newNode(hash, key, value, null);
22 // 桶中已经存在元素
23 else {
24 Node<K,V> e; K k;
25 // 步骤③:节点key存在,直接覆盖value
26 // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
27 if (p.hash == hash &&
28 ((k = p.key) == key || (key != null && key.equals(k))))
29 // 将第一个元素赋值给e,用e来记录
30 e = p;
31 // 步骤④:判断该链为红黑树
32 // hash值不相等,即key不相等;为红黑树结点
33 // 如果当前元素类型为TreeNode,表示为红黑树,putTreeVal返回待存放的node, e可
能为null
34 else if (p instanceof TreeNode)
35 // 放入树中
36 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
37 // 步骤⑤:该链为链表
38 // 为链表结点
39 else {
40 // 在链表最末插入结点
41 for (int binCount = 0; ; ++binCount) {
42 // 到达链表的尾部
43
44 //判断该链表尾部指针是不是空的
45 if ((e = p.next) == null) {
46 // 在尾部插入新结点
47 p.next = newNode(hash, key, value, null);
48 //判断链表的长度是否达到转化红黑树的临界值,临界值为8
49 if (binCount >= TREEIFY_THRESHOLD ‐ 1) // ‐1 for 1st
50 //链表结构转树形结构
51 treeifyBin(tab, hash);
52 // 跳出循环
53 break;
54 }
55 // 判断链表中结点的key值与插入的元素的key值是否相等
56 if (e.hash == hash &&
57 ((k = e.key) == key || (key != null && key.equals(k))))
58 // 相等,跳出循环
59 break;
60 // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
61 p = e;
62 }
63 }
64 //判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,返回新来的val
ue这个值
65 if (e != null) {
66 // 记录e的value
67 V oldValue = e.value;
68 // onlyIfAbsent为false或者旧值为null
69 if (!onlyIfAbsent || oldValue == null)
70 //用新值替换旧值
71 e.value = value;
72 // 访问后回调
73 afterNodeAccess(e);
74 // 返回旧值
75 return oldValue;
76 }
77 }
78 // 结构性修改
79 ++modCount;
80 // 步骤⑥:超过最大容量就扩容
81 // 实际大小大于阈值则扩容
82 if (++size > threshold)
83 resize();
84 // 插入后回调
85 afterNodeInsertion(evict);
86 return null;
87 }

①. Determine si la tabla de matriz de pares clave-valor [i] está vacía o nula; de lo contrario, ejecute resize() para expandir;

②. Calcule el valor hash de acuerdo con la clave de valor clave para obtener el índice de matriz i insertado, si la tabla [i] == nula, cree directamente un nuevo nodo para agregar, pase a ⑥, si la tabla [i] no está vacía, pasa a ③;

③. Determine si el primer elemento de la tabla [i] es el mismo que la clave, si es el mismo, sobrescribe directamente el valor; de lo contrario, pase a

④, lo mismo aquí se refiere a hashCode y es igual;

4. Determine si la tabla [i] es un nodo de árbol, es decir, si la tabla [i] es un árbol rojo-negro, si es un árbol rojo-negro, inserte directamente pares clave-valor en el árbol; de lo contrario, pase a ⑤ ;

⑤.Atravesando la tabla [i], juzgando si la longitud de la lista vinculada es mayor que 8, si es mayor que 8, convierta la lista vinculada en un árbol rojo-negro y realice una operación de inserción en el árbol rojo-negro De lo contrario, realice una operación de inserción de la lista vinculada; si se descubre que la clave ya existe durante el proceso transversal, simplemente anule el valor directamente;

⑥. Después de que la inserción sea exitosa, juzgue si el tamaño real del par clave-valor excede el umbral de gran capacidad. Si lo excede, expanda la capacidad.

¿Cómo se realiza la operación de expansión de HashMap?

①.En jdk1.8, el método de cambio de tamaño es llamar al método de cambio de tamaño para expandir la capacidad cuando el par clave-valor en el mapa hash es mayor que el umbral o cuando se inicializa;

②. Cada vez que se expande, se expande 2 veces;

③ Después de la expansión, la posición del objeto Nodo está en la posición original o se mueve a una posición dos veces el desplazamiento original. En putVal(), vemos que el método resize() se usa dos veces en esta función. El método resize() indica que se expandirá cuando se inicialice por primera vez, o cuando el tamaño real de la matriz sea mayor. que su El valor crítico (la primera vez es 12), en este momento, los elementos en el depósito se redistribuirán durante la expansión. Esto también es una optimización de la versión JDK1.8. En 1.7, es necesario redistribuirlo después la expansión Calcule su valor Hash y distribúyalo de acuerdo con el valor Hash, pero en la versión 1.8, se juzga en función de si (e.hash y oldCap) es 0 en la misma posición del depósito. Después de volver a distribuir el hash, el elemento La posición permanece en la posición original o se mueve a la posición de la posición original + el tamaño de matriz aumentado.

1 final Node<K,V>[] resize() {
2 Node<K,V>[] oldTab = table;//oldTab指向hash桶数组
3 int oldCap = (oldTab == null) ? 0 : oldTab.length;
4 int oldThr = threshold;
5 int newCap, newThr = 0;
6 if (oldCap > 0) {//如果oldCap不为空的话,就是hash桶数组不为空
7 if (oldCap >= MAXIMUM_CAPACITY) {//如果大于最大容量了,就赋值为整数最大的阀
值
8 threshold = Integer.MAX_VALUE;
9 return oldTab;//返回
10 }//如果当前hash桶数组的长度在扩容后仍然小于最大容量 并且oldCap大于默认值16
11 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
12 oldCap >= DEFAULT_INITIAL_CAPACITY)
13 newThr = oldThr << 1; // double threshold 双倍扩容阀值threshold
14 }
15 // 旧的容量为0,但threshold大于零,代表有参构造有cap传入,threshold已经被初
始化成最小2的n次幂
16 // 直接将该值赋给新的容量
17 else if (oldThr > 0) // initial capacity was placed in threshold
18 newCap = oldThr;
19 // 无参构造创建的map,给出默认容量和threshold 16, 16*0.75
20 else { // zero initial threshold signifies using defaults
21 newCap = DEFAULT_INITIAL_CAPACITY;
22 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
23 }
24 // 新的threshold = 新的cap * 0.75
25 if (newThr == 0) {
26 float ft = (float)newCap * loadFactor;
27 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
28 (int)ft : Integer.MAX_VALUE);
29 }
30 threshold = newThr;
31 // 计算出新的数组长度后赋给当前成员变量table
32 @SuppressWarnings({"rawtypes","unchecked"})
33 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//新建hash桶数组
34 table = newTab;//将新数组的值复制给旧的hash桶数组
35 // 如果原先的数组没有初始化,那么resize的初始化工作到此结束,否则进入扩容元素
重排逻辑,使其均匀的分散
36 if (oldTab != null) {
37 // 遍历新数组的所有桶下标
38 for (int j = 0; j < oldCap; ++j) {
39 Node<K,V> e;
40 if ((e = oldTab[j]) != null) {
41 // 旧数组的桶下标赋给临时变量e,并且解除旧数组中的引用,否则就数组无法被GC回收
42 oldTab[j] = null;
43 // 如果e.next==null,代表桶中就一个元素,不存在链表或者红黑树
44 if (e.next == null)
45 // 用同样的hash映射算法把该元素加入新的数组
46 newTab[e.hash & (newCap ‐ 1)] = e;
47 // 如果e是TreeNode并且e.next!=null,那么处理树中元素的重排
48 else if (e instanceof TreeNode)
49 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
50 // e是链表的头并且e.next!=null,那么处理链表中元素重排
51 else { // preserve order
52 // loHead,loTail 代表扩容后不用变换下标,见注1
53 Node<K,V> loHead = null, loTail = null;
54 // hiHead,hiTail 代表扩容后变换下标,见注1
55 Node<K,V> hiHead = null, hiTail = null;
56 Node<K,V> next;
57 // 遍历链表
58 do {
59 next = e.next;
60 if ((e.hash & oldCap) == 0) {
61 if (loTail == null)
62 // 初始化head指向链表当前元素e,e不一定是链表的第一个元素,初始化后loHead
63 // 代表下标保持不变的链表的头元素
64 loHead = e;
65 else
66 // loTail.next指向当前e
67 loTail.next = e;
68 // loTail指向当前的元素e
69 // 初始化后,loTail和loHead指向相同的内存,所以当loTail.next指向下一个元素
时,
70 // 底层数组中的元素的next引用也相应发生变化,造成lowHead.next.next.....
71 // 跟随loTail同步,使得lowHead可以链接到所有属于该链表的元素。
72 loTail = e;
73 }
74 else {
75 if (hiTail == null)
76 // 初始化head指向链表当前元素e, 初始化后hiHead代表下标更改的链表头元素
77 hiHead = e;
78 else
79 hiTail.next = e;
80 hiTail = e;
81 }
82 } while ((e = next) != null);
83 // 遍历结束, 将tail指向null,并把链表头放入新数组的相应下标,形成新的映射。
84 if (loTail != null) {
85 loTail.next = null;
86 newTab[j] = loHead;
87 }
88 if (hiTail != null) {
89 hiTail.next = null;
90 newTab[j + oldCap] = hiHead;
91 }
92 }
93 }
94 }
95 }
96 return newTab;
97 }

¿Cómo resuelve HashMap los conflictos de hash?

Respuesta: Antes de resolver este problema, primero necesitamos saber qué es una colisión de hash, y antes de entender una colisión de hash, necesitamos saber qué es un hash; ¿qué es un hash?

Hash, generalmente traducido como "hash", también se transcribe directamente como "hash", que consiste en transformar una entrada de cualquier longitud en una salida de longitud fija a través de un algoritmo hash, y la salida es el valor hash (valor hash); Esto La transformación es un mapeo de compresión, es decir, el espacio del valor hash suele ser mucho más pequeño que el espacio de la entrada, y diferentes entradas pueden generar la misma salida, por lo que es imposible determinar de forma única el valor de entrada a partir del valor hash. . En pocas palabras, es una función para comprimir un mensaje de cualquier longitud en un resumen de mensaje de longitud fija.

Todas las funciones hash tienen la siguiente propiedad básica**: si los valores hash calculados según la misma función hash son diferentes, los valores de entrada también deben ser diferentes. Sin embargo, si los valores hash calculados a partir de la misma función hash son los mismos, los valores de entrada no son necesariamente los mismos**.

¿Qué son las colisiones de hash?

Cuando dos valores de entrada diferentes calculan el mismo valor hash de acuerdo con la misma función hash, lo llamamos colisión (colisión hash).

Estructura de datos HashMap

En Java, existen dos estructuras de datos relativamente simples para almacenar datos: matrices y listas vinculadas. Las características de las matrices son: fáciles de abordar, difíciles de insertar y eliminar; las características de las listas vinculadas son: difíciles de abordar, pero fáciles de insertar y eliminar; por eso combinamos matrices y listas vinculadas para aprovechar sus respectivas ventajas, utilizando un método llamado El método de dirección en cadena puede resolver el conflicto hash:

[Error en la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo anti-sanguijuela, se recomienda guardar la imagen y cargarla directamente (img-BnuhMgra-1692508006554) (02-Preguntas de la entrevista sobre el contenedor de recopilación de Java (última versión de 2020) - punto clave.assets/image- 20201109163448750.png)]

De esta manera, podemos organizar objetos (img) con el mismo valor hash en una lista vinculada y colocarlos debajo del depósito correspondiente al valor hash, pero en comparación con el tipo int devuelto por hashCode, la capacidad inicial de nuestro HashMap es DEFAULT_INITIAL_CAPACITY = 1 << 4 (es decir, 2 elevado a la cuarta potencia 16) es mucho menor que el rango del tipo int, por lo que si simplemente usamos el resto del hashCode para obtener el depósito correspondiente, esto aumentará en gran medida la probabilidad de colisiones de hash y, en el peor de los casos, HashMap se convertirá en una lista enlazada individualmente, por lo que también necesitamos optimizar la función hash() de hashCode

El problema mencionado anteriormente se debe principalmente a que si usa hashCode para tomar el resto, entonces es equivalente a que solo los bits bajos del hashCode participen en el cálculo, y los bits altos no tienen ningún efecto, por lo que nuestra idea es dejar que los bits altos Los bits del valor hashCode también participan en el cálculo, para reducir aún más la probabilidad de colisión hash y hacer que la distribución de datos sea más uniforme. A esta operación la llamamos perturbación. La función hash() en JDK 1.8 es la siguiente:

1	static final int hash(Object key) {
2	int h;
3	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或)
4	}

Esto es más conciso que en JDK 1.7, en comparación con operaciones de 4 bits, 5 operaciones XOR (9 perturbaciones) en 1.7, en 1.8, solo operación de 1 bit y 1 operación XOR (2 perturbaciones);

JDK1.8 agrega un árbol rojo-negro

[Error en la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo anti-limpieza, se recomienda guardar la imagen y cargarla directamente (img-xF4yqZAB-1692508006555) (02-Preguntas de la entrevista sobre el contenedor de recopilación de Java (última versión de 2020) - punto clave.assets/image- 20201109163526813.png)]

A través del método de dirección de cadena anterior (usando la tabla hash) y la función de perturbación (img), logramos que nuestra distribución de datos sea más uniforme y se redujeron las colisiones hash. Sin embargo, cuando hay una gran cantidad de datos en nuestro HashMap, agregue a uno de nuestros depósitos La lista vinculada correspondiente tiene n elementos, por lo que la complejidad del tiempo de recorrido es O (n). Para resolver este problema, JDK1.8 agrega una estructura de datos de árbol rojo-negro en HashMap, lo que reduce aún más el complejidad transversal a O(n) logn); resumen

Resuma brevemente qué métodos utiliza HashMap para resolver eficazmente los conflictos de hash:

  1. Utilice el método de dirección en cadena (usando una tabla hash) para vincular datos con el mismo valor hash;
  2. Utilice 2 funciones de perturbación (funciones hash) para reducir la probabilidad de colisiones hash y hacer que la distribución de datos sea más uniforme;
  3. La introducción del árbol rojo-negro reduce aún más la complejidad temporal del recorrido, lo que hace que el recorrido sea más rápido;

¿Se puede utilizar cualquier clase como clave de un mapa?

Puede usar cualquier clase como clave del Mapa, pero antes de usarla, debe considerar los siguientes puntos: Si la clase anula el método equals(), también debe anular el método hashCode(). Todas las instancias de la clase deben obedecer las reglas relacionadas con iguales () y hashCode ().

Si una clase no usa equals(), no debe usarse en hashCode().

La mejor práctica para las clases clave definidas por el usuario es hacerlas inmutables para que el valor hashCode() pueda almacenarse en caché para un mejor rendimiento. Las clases inmutables también pueden garantizar que hashCode() y equals() no cambien en el futuro, lo que resolverá los problemas asociados con las mutables.

¿Por qué las clases contenedoras como String e Integer son adecuadas como K en HashMap?

Respuesta: Las características de las clases de empaquetado, como String e Integer, pueden garantizar la inmutabilidad y la precisión del cálculo de los valores Hash y pueden reducir eficazmente la probabilidad de colisiones Hash.

\ 1. Todos son tipos finales, es decir, inmutabilidad, lo que garantiza la inmutabilidad de la clave, y no habrá caso de obtener diferentes valores hash.

Los métodos iguales (), hashCode () y otros se han reescrito internamente, cumpliendo con las especificaciones internas de HashMap (si no está seguro, puede ir a lo anterior para ver el proceso de putValue), y no es fácil calcular mal el valor Hash

¿Qué debo hacer si uso Object como clave de HashMap?

Respuesta: Reescribe los métodos hashCode() y equals()

\1. Reescribe hashCode() porque es necesario calcular la ubicación de almacenamiento de los datos almacenados. Debes tener cuidado de no intentar excluir una parte clave de un objeto del cálculo del código hash para mejorar el rendimiento. Aunque esto puede ser más rápido, puede provocar más colisiones de Hash;

\ 2. Para reescribir el método igual (), debe cumplir con la reflexividad, la simetría, la transitividad, la consistencia y

Para cualquier valor de referencia x no nulo, x.equals(null) debe devolver falso para garantizar la unicidad de la clave en la tabla hash.

¿Por qué HashMap no utiliza directamente el valor hash procesado por hashCode () como subíndice de la tabla?

Respuesta: El método hashCode() devuelve un tipo entero int, su rango es -(2 ^ 31)~(2 ^ 31 - 1), hay alrededor de 4 mil millones de espacios de mapeo y el rango de capacidad de HashMap es 16 (valor predeterminado inicial valor) ~ 2 ^ 30, HashMap generalmente no puede obtener un valor grande y es difícil proporcionar tanto espacio de almacenamiento en el dispositivo, por lo que el valor hash calculado por hashCode () puede no estar dentro del rango del tamaño de la matriz, por lo tanto , la ubicación de almacenamiento no puede coincidir;

¿Cómo resolverlo?

\ 1. HashMap implementa su propio método hash (). A través de dos perturbaciones, los bits altos y bajos de su propio valor hash se pueden aplicar XOR, lo que reduce la probabilidad de colisión hash y hace que la distribución de datos sea más uniforme;

\ 2. Cuando se garantiza que la longitud de la matriz es una potencia de 2, use la operación valor AND después de la operación hash()

(&) (longitud de la matriz - 1) para obtener el subíndice de la matriz para el almacenamiento, de modo que sea mejor que buscar

Las operaciones restantes son más eficientes y, en segundo lugar, solo cuando la longitud de la matriz es una potencia de 2, h&

(longitud-1) es equivalente a h%longitud, tres para resolver el problema de "el valor hash no coincide con el rango de tamaño de la matriz"

¿Por qué la longitud de HashMap es una potencia de 2?

Para que el acceso a HashMap sea eficiente, debe haber la menor cantidad de colisiones posible, es decir, los datos deben distribuirse lo más uniformemente posible y la longitud de cada lista vinculada/árbol rojo-negro debe ser aproximadamente la misma. Esta implementación es el algoritmo de en qué lista vinculada/árbol rojo-negro almacenar los datos.

¿Cómo debería diseñarse este algoritmo? En primer lugar, podemos pensar en utilizar la operación de tomar el resto para lograrlo. Sin embargo, aquí viene el punto: "Toma el resto (%)

En operación, si el divisor es una potencia de 2, equivale a la operación AND (&) con su divisor menos uno (es decir,

La premisa de hash%length==hash&(length-1) es que la longitud es 2 elevado a la enésima potencia;). "Y el uso de la operación de bits binarios &, en comparación con %, puede mejorar la eficiencia de la operación, lo que explica por qué la longitud de HashMap es la potencia de 2.

Entonces, ¿por qué dos perturbaciones? Respuesta: Esto es para aumentar la aleatoriedad de los bits bajos del valor hash para hacer que la distribución sea más uniforme, mejorando así la aleatoriedad y uniformidad de la posición del subíndice de almacenamiento de matriz correspondiente y, finalmente, reduciendo los conflictos de hash. Dos veces es suficiente y el alto y se han alcanzado bits bajos. El propósito de participar en la operación al mismo tiempo.

¿Cuál es la diferencia entre HashMap y HashTable?

  1. Seguridad de subprocesos: HashMap no es seguro para subprocesos y HashTable es seguro para subprocesos;

Los métodos dentro de HashTable se modifican básicamente mediante sincronización. (¡Si desea garantizar la seguridad de los subprocesos, utilice ConcurrentHashMap!);

  1. Eficiencia: debido a problemas de seguridad de subprocesos, HashMap es un poco más eficiente que HashTable. Además, HashTable está básicamente eliminado, no lo use en el código;

  2. Soporte para clave nula y valor nulo: en HashMap, nulo se puede usar como clave, solo existe una de esas claves y puede haber una o más claves cuyo valor correspondiente sea nulo. Pero cuando

Siempre que haya un valor nulo en la clave de colocación en HashTable, se arrojará directamente.

Excepción de puntero nulo.

  1. **La diferencia entre el tamaño de capacidad inicial y el tamaño de capacidad de cada expansión**: ①Si no se especifica el valor de capacidad inicial al crear, el tamaño inicial predeterminado de Hashtable es 11, y después de cada expansión, la capacidad pasa a ser el 2n original +1. El tamaño de inicialización predeterminado de HashMap es 16. Después de cada ampliación, la capacidad se convierte en el doble de la original. ②Si se proporciona el valor inicial de la capacidad al crear, Hashtable usará directamente el tamaño que usted proporcione y HashMap lo expandirá al tamaño de la potencia de 2. Es decir, HashMap siempre usa una potencia de 2 como tamaño de la tabla hash, y más adelante se explicará por qué es una potencia de 2.

  2. Estructura de datos subyacente: HashMap después de JDK1.8 ha experimentado cambios importantes en la resolución de conflictos de hash. Cuando la longitud de la lista vinculada es mayor que el umbral (8 por defecto), la lista vinculada se convierte en un árbol rojo-negro para reducir el tiempo de búsqueda. Hashtable no tiene tal mecanismo.

  3. Uso recomendado: como puede ver en los comentarios de clase de Hashtable, Hashtable es una clase reservada y no se recomienda su uso. Se recomienda usar HashMap en un entorno de un solo subproceso y usar ConcurrentHashMap en su lugar si se usa con múltiples subprocesos. se requiere.

¿Cómo decidir utilizar HashMap o TreeMap?

HashMap es una buena opción para operaciones como insertar, eliminar y ubicar elementos en el mapa. Sin embargo

Sin embargo, si necesita recorrer una colección ordenada de claves, TreeMap es una mejor opción. Según el tamaño de su colección, puede ser más rápido agregar elementos a HashMap y reemplazar el mapa con TreeMap para un recorrido ordenado de claves.

La diferencia entre HashMap y ConcurrentHashMap

  1. ConcurrentHashMap divide toda la matriz de depósitos en segmentos y luego usa un bloqueo para proteger cada segmento. En comparación con el bloqueo sincronizado de HashTable, la granularidad es más fina y el rendimiento de concurrencia es mejor, mientras que HashMap no tiene mecanismo de bloqueo, lo cual es no es seguro para subprocesos. (Después de JDK1.8, ConcurrentHashMap lanzó una nueva forma de implementarlo, utilizando el algoritmo CAS).

  2. Los pares clave-valor de HashMap permiten nulos, pero ConCurrentHashMap no lo permite.

¿La diferencia entre ConcurrentHashMap y Hashtable?

La diferencia entre ConcurrentHashMap y Hashtable se refleja principalmente en la forma de lograr la seguridad de los subprocesos.

La estructura de datos subyacente: ConcurrentHashMap de JDK1.7 usa una matriz segmentada en la parte inferior

+ Implementación de lista vinculada, la estructura de datos adoptada por JDK1.8 es la misma que la de HashMap1.8, matriz + lista vinculada/rojo y negro

árbol binario. La estructura de datos subyacente de Hashtable y HashMap antes de JDK1.8 es similar a la forma de matriz + lista vinculada: la matriz es el cuerpo principal de HashMap y la lista vinculada existe principalmente para resolver conflictos de hash;

​ Formas de lograr la seguridad de subprocesos (importante): ① En JDK1.7,

ConcurrentHashMap (bloqueo de segmento) divide toda la matriz del depósito en segmentos (Segmento), cada bloqueo solo bloquea parte de los datos en el contenedor y el acceso multiproceso a los datos en diferentes segmentos de datos en el contenedor evitará la competencia de bloqueos y mejorará el acceso concurrente. tasa. (De forma predeterminada, se asignan 16 segmentos, lo cual es 16 más eficiente que Hashtable

veces. ) En el momento de JDK1.8, el concepto de segmento se abandonó, pero se usó directamente

Se implementa la estructura de datos de matriz de nodos + lista vinculada + árbol rojo-negro y se utiliza el control de concurrencia.

sincronizado y CAS para operar. (JDK1.6 ha realizado muchas optimizaciones para los bloqueos sincronizados) Todo parece un HashMap optimizado y seguro para subprocesos, aunque todavía está en JDK1.8

Puede ver la estructura de datos de Segment, pero los atributos se han simplificado, solo para que sean compatibles con la versión anterior;②

Hashtable (mismo bloqueo): el uso de sincronizado para garantizar la seguridad de los subprocesos es muy ineficiente. Cuando un subproceso accede al método de sincronización, otros subprocesos también acceden al método de sincronización y pueden entrar en un estado de bloqueo o sondeo, como usar put para agregar elementos, otro subproceso no puede usar put para agregar elementos, ni puede usar get, y el La competencia será cada vez más intensa cuanto menos eficiente.

Un cuadro comparativo de los dos:

Tabla de picadillo:

[Error en la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo anti-sanguijuela, se recomienda guardar la imagen y cargarla directamente (img-nGpDytvk-1692508006555) (02-Preguntas de la entrevista sobre el contenedor de recopilación de Java (última versión de 2020) - punto clave.assets/image- 20201109165808848.png)]

JDK1.7 ConcurrentHashMap:

[Error en la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo anti-limpieza, se recomienda guardar la imagen y cargarla directamente (img-EcYg9xdw-1692508006555) (02-Preguntas de la entrevista sobre el contenedor de recopilación de Java (última versión de 2020) - focus.assets/image- 20201109165820466.png)]

ConcurrentHashMap de JDK1.8 (TreeBi (img) n: nodo de árbol binario rojo y negro Nodo: nodo de lista vinculada):

[Error en la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo anti-limpieza, se recomienda guardar la imagen y cargarla directamente (img-RC4PO3Fy-1692508006556) (02-Preguntas de la entrevista sobre el contenedor de recopilación de Java (última versión de 2020) - punto clave.assets/image- 20201109165840129.png)]

Respuesta: ConcurrentHashMap combina las ventajas de Hash(img)Map y HashTable. HashMap no considera la sincronización y HashTable considera la sincronización. Pero HashTable bloquea toda la estructura cada vez que se ejecuta sincrónicamente. La forma en que se bloquea ConcurrentHashMap es ligeramente detallada.

¿Conoce la implementación subyacente de ConcurrentHashMap? ¿Cuál es el principio de realización?

JDK1.7

Primero, los datos se dividen en secciones para su almacenamiento y luego se asigna un candado a cada sección de datos. Cuando un subproceso ocupa un candado para acceder a una sección de datos, otros subprocesos también pueden acceder a otras secciones de datos.

En JDK1.7, ConcurrentHashMap usa Segment + HashEntry para implementar

Ahora, la estructura es la siguiente:

Un ConcurrentHashMap contiene una matriz de segmentos. La estructura de Segment es similar a HashMap. Es una estructura de matriz y lista vinculada. Un segmento contiene una matriz HashEntry. Cada HashEntry es un elemento de una estructura de lista vinculada. Cada segmento protege los elementos en una matriz HashEntry. Cuando los datos de la matriz HashEntry Al modificar, primero debe obtener el bloqueo de segmento correspondiente.

[Error en la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo anti-limpieza, se recomienda guardar la imagen y cargarla directamente (img-K1gdrPKc-1692508006557) (02-Preguntas de la entrevista sobre el contenedor de recopilación de Java (última versión de 2020) - focus.assets/image- 20201109165916185.png)]

  1. Esta clase contiene dos clases internas estáticas HashE (img) ntry y Segment: la primera se usa para encapsular los pares clave-valor de la tabla de mapeo y la segunda se usa para actuar como un bloqueo;

  2. El segmento es un bloqueo reentrante ReentrantLock. Cada segmento protege un elemento en una matriz HashEntry. Al modificar los datos de la matriz HashEntry, primero se debe obtener el bloqueo de segmento correspondiente.

JDK1.8

En JDK1.8, se abandonó el diseño de segmento inflado y en su lugar se usó Nodo + CAS + Sincronizado para garantizar la seguridad de concurrencia. Sincronizado solo bloquea el primer nodo de la lista vinculada actual o el árbol binario rojo-negro, por lo que siempre que el hash no entra en conflicto, no se generará concurrencia y la eficiencia aumentará N veces.

La estructura es la siguiente:

[Error en la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo anti-limpieza, se recomienda guardar la imagen y cargarla directamente (img-FCdhnfGk-1692508006557) (02-Preguntas de la entrevista sobre el contenedor de recopilación de Java (última versión de 2020) - punto clave.assets/image- 20201109165939912.png)]

Mire el proceso de inserción de elementos (se recomienda mirar el código fuente):

Si el Nodo en la posición correspondiente no se ha inicializado, llame a CAS para insertar los datos correspondientes;

1	else if ((f = tabAt(tab, i = (n ‐ 1) & hash)) == null) {
2	if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
3	break; // no lock when adding to empty bin
4	}

Si el nodo en la posición correspondiente no está vacío y el nodo actual no está en estado de movimiento, agregue

Bloqueo sincronizado, si el hash del nodo no es inferior a 0, recorra la lista vinculada para actualizar el nodo o insertar un nuevo nodo;

1 if (fh >= 0) { 
2 binCount = 1;
3	for (Node<K,V> e = f;; ++binCount) {
4	K ek;
5	if (e.hash == hash &&
6	((ek = e.key) == key ||
7	(ek != null && key.equals(ek)))) {
8	oldVal = e.val;
9	if (!onlyIfAbsent)
10	e.val = value;
11	break;
12	}
13	Node<K,V> pred = e;
14	if ((e = e.next) == null) {
15	pred.next = new Node<K,V>(hash, key, value, null);
16	break;
17	}
18	}
19	}

\ 1. Si el nodo es un nodo de tipo TreeBin, lo que indica que es una estructura de árbol rojo-negro, luego inserte el nodo en el árbol rojo-negro a través del método putTreeVal; si binCount no es 0, significa que el nodo La operación ha afectado los datos, si la lista vinculada actual Si el número llega a 8, se transformará en un árbol rojo-negro a través del método treeifyBin. Si oldVal no está vacío, significa que es una operación de actualización que no afecta el número de elementos y el valor anterior se devolverán directamente;

\ 2. Si se inserta un nuevo nodo, ejecute el método addCount() para intentar actualizar el número de elementos baseCount;

herramientas auxiliares

¿Cuál es la diferencia entre Array y ArrayList?

  • Array puede almacenar objetos y tipos de datos básicos, mientras que ArrayList solo puede almacenar objetos.
  • La matriz se especifica con un tamaño fijo, mientras que el tamaño de ArrayList se expande automáticamente.
  • No hay tantos métodos integrados en Array como en ArrayList, como addAll, removeAll, iteration y otros métodos solo en ArrayList.

Para los tipos primitivos, las colecciones utilizan autoboxing para reducir el esfuerzo de codificación. Sin embargo, este enfoque es relativamente lento cuando se trata de tipos de datos primitivos de tamaño fijo.

¿Cómo realizar la conversión entre Array y List?

  • Matriz 转 Lista: Matrices. asList(matriz) ;
  • List to Array: método toArray() de la lista.

¿La diferencia entre comparable y comparador?

  • La interfaz comparable en realidad proviene del paquete java.lang, que tiene un método compareTo(Object obj) para ordenar.
  • La interfaz del comparador es en realidad del paquete java.util, que tiene un método de comparación (Objeto obj1, Objeto obj2) para ordenar.

Generalmente, cuando necesitamos usar una clasificación personalizada para una colección, necesitamos reescribir el método compareTo o el método de comparación. Cuando necesitamos implementar dos métodos de clasificación para una determinada colección, por ejemplo, el título de la canción y el nombre del cantante en una canción. objeto use a Si hay un método de clasificación, podemos reescribir el método compareTo y usar el método Comparador hecho por nosotros mismos o usar dos Comparadores para ordenar el título de la canción y el nombre del cantante. El segundo significa que solo podemos usar el método de dos parámetros. versión de Collections.sort() .

¿Cuál es la diferencia entre Colección y Colecciones?

  • java.util.Collection es una interfaz de colección (una interfaz de nivel superior de clases de colección). Proporciona métodos de interfaz comunes para operaciones básicas en objetos de colección. La interfaz Colección tiene muchas implementaciones concretas en la biblioteca de clases Java. La importancia de la interfaz Colección es proporcionar un método de operación unificado maximizado para varias colecciones específicas, y sus interfaces de herencia directa incluyen Lista y Conjunto.
  • Colecciones es una clase de herramienta/clase auxiliar de la clase de colección, que proporciona una serie de métodos estáticos para ordenar, buscar y realizar operaciones seguras para subprocesos en elementos de la colección.

¿Cómo comparan TreeMap y TreeSet los elementos al ordenar? ¿Cómo compara elementos el método sort() en la clase de herramienta Colecciones?

TreeSet requiere que la clase del objeto almacenado implemente la interfaz Comparable, que proporciona el método compareTo () para comparar elementos. Cuando se inserta un elemento, se volverá a llamar a este método para comparar el tamaño del elemento.

TreeMap requiere que las claves de mapeo de pares clave-valor almacenadas implementen la interfaz Comparable para que los elementos se puedan ordenar según las claves.

El método de clasificación de la clase de herramienta Colecciones tiene dos formas sobrecargadas:

El primer tipo requiere que los objetos almacenados en el contenedor a ordenar deben implementar la interfaz Comparable para comparar elementos;

  • List to Array: método toArray() de la lista.

¿La diferencia entre comparable y comparador?

  • La interfaz comparable en realidad proviene del paquete java.lang, que tiene un método compareTo(Object obj) para ordenar.
  • La interfaz del comparador es en realidad del paquete java.util, que tiene un método de comparación (Objeto obj1, Objeto obj2) para ordenar.

Generalmente, cuando necesitamos usar una clasificación personalizada para una colección, necesitamos reescribir el método compareTo o el método de comparación. Cuando necesitamos implementar dos métodos de clasificación para una determinada colección, por ejemplo, el título de la canción y el nombre del cantante en una canción. objeto use a Si hay un método de clasificación, podemos reescribir el método compareTo y usar el método Comparador hecho por nosotros mismos o usar dos Comparadores para ordenar el título de la canción y el nombre del cantante. El segundo significa que solo podemos usar el método de dos parámetros. versión de Collections.sort() .

¿Cuál es la diferencia entre Colección y Colecciones?

  • java.util.Collection es una interfaz de colección (una interfaz de nivel superior de clases de colección). Proporciona métodos de interfaz comunes para operaciones básicas en objetos de colección. La interfaz Colección tiene muchas implementaciones concretas en la biblioteca de clases Java. La importancia de la interfaz Colección es proporcionar un método de operación unificado maximizado para varias colecciones específicas, y sus interfaces de herencia directa incluyen Lista y Conjunto.
  • Colecciones es una clase de herramienta/clase auxiliar de la clase de colección, que proporciona una serie de métodos estáticos para ordenar, buscar y realizar operaciones seguras para subprocesos en elementos de la colección.

¿Cómo comparan TreeMap y TreeSet los elementos al ordenar? ¿Cómo compara elementos el método sort() en la clase de herramienta Colecciones?

TreeSet requiere que la clase del objeto almacenado implemente la interfaz Comparable, que proporciona el método compareTo () para comparar elementos. Cuando se inserta un elemento, se volverá a llamar a este método para comparar el tamaño del elemento.

TreeMap requiere que las claves de mapeo de pares clave-valor almacenadas implementen la interfaz Comparable para que los elementos se puedan ordenar según las claves.

El método de clasificación de la clase de herramienta Colecciones tiene dos formas sobrecargadas:

El primer tipo requiere que los objetos almacenados en el contenedor a ordenar deben implementar la interfaz Comparable para comparar elementos;

El segundo tipo no requiere necesariamente que los elementos en el contenedor sean comparables, pero requiere que se pase el segundo parámetro, que es un subtipo de la interfaz Comparator (es necesario anular el método de comparación para lograr la comparación de elementos), que es equivalente a una regla de clasificación definida temporalmente es en realidad un algoritmo para comparar tamaños de elementos mediante inyección de interfaz, y también es una aplicación del modo de devolución de llamada (soporte para programación funcional en Java).

Supongo que te gusta

Origin blog.csdn.net/leader_song/article/details/132390992
Recomendado
Clasificación