Preguntas de la entrevista sobre colecciones de Java

Este artículo es parte de una serie: • Preguntas de la entrevista de las colecciones de Java (artículo actual) • Preguntas de la entrevista del sistema de tipo Java

• Preguntas de la entrevista de simultaneidad de Java (+ respuestas)

• Preguntas de la entrevista de inicialización y estructura de clases de Java

• Preguntas de la entrevista de Java 8 (+ respuestas)

• Gestión de la memoria en preguntas de la entrevista de Java (+ respuestas)

• Preguntas de la entrevista de Java Generics (+ Respuestas)

• Preguntas de la entrevista de control de flujo de Java (+ respuestas)

• Preguntas de la entrevista sobre excepciones de Java (+ respuestas)

• Preguntas de la entrevista de anotaciones de Java (+ respuestas)

• Preguntas principales de la entrevista del Spring Framework

1. Introducción

Colecciones de Java es un tema que a menudo se plantea en entrevistas técnicas para desarrolladores de Java. Este artículo revisa algunas preguntas importantes que se hacen con mayor frecuencia y que pueden ser difíciles de responder.

2. Preguntas

Q1. Describa la jerarquía de tipos de colecciones. ¿Cuáles son las interfaces principales y cuáles son las diferencias entre ellas?

La interfaz Iterable representa cualquier colección que se pueda iterar utilizando el bucle for-each . La interfaz de Colección hereda de Iterable y agrega métodos genéricos para verificar si un elemento está en una colección, agregar y eliminar elementos de la colección, determinar su tamaño, etc.

Las interfaces List , Set y Queue heredan de la interfaz Collection .

La lista es una colección ordenada y se puede acceder a sus elementos por su índice en la lista.

Conjunto es una colección desordenada con elementos distintos, similar a la noción matemática de conjunto.

Queue es una colección con métodos adicionales para agregar, eliminar y examinar elementos, útil para retener elementos antes del procesamiento.

La interfaz del mapa también es parte del marco de la colección, pero no extiende la colección . Esto es por diseño, para enfatizar la diferencia entre colecciones y mapeos que son difíciles de reunir bajo una abstracción común. Lainterfaz del mapa representa una estructura de datos clave-valor con claves únicas y no más de un valor para cada clave.

Q2. Describa varias implementaciones de la interfaz de mapa y sus diferencias de casos de uso.

Una de las implementaciones más utilizadas de la interfaz Map es HashMap . Es una estructura de datos de mapa hash típica que permite acceder a elementos en tiempo constante, u O (1), pero no conserva el orden y no es seguro para subprocesos .

Para preservar el orden de inserción de los elementos, puede usar la clase LinkedHashMap que extiende el HashMap y además vincula los elementos en una lista vinculada, con una sobrecarga previsible.

La clase TreeMap almacena sus elementos en una estructura de árbol rojo-negro, lo que permite acceder a los elementos en tiempo logarítmico u O (log (n)). Es más lento que el HashMap en la mayoría de los casos, pero permite mantener los elementos en orden según algún Comparador .

El ConcurrentHashMap es una aplicación segura para los subprocesos de un mapa hash. Proporciona una concurrencia completa de recuperaciones (ya que la operación de obtención no implica bloqueo) y una alta concurrencia esperada de actualizaciones.

La clase Hashtable ha estado en Java desde la versión 1.0. No está desaprobado, pero en su mayoría se considera obsoleto. Es un mapa hash seguro para subprocesos, pero a diferencia de ConcurrentHashMap , todos sus métodos están simplemente sincronizados , lo que significa que todas las operaciones en este mapa bloquean, incluso la recuperación de valores independientes.

Q3. Explique la diferencia entre Linkedlist y Arraylist.

ArrayList es una implementación de lainterfaz List que se basa en una matriz. ArrayList maneja internamente el cambio de tamaño de esta matriz cuando los elementos se agregan o eliminan. Puede acceder a sus elementos en tiempo constante por su índice en la matriz. Sin embargo, insertar o eliminar un elemento infiere el desplazamiento de todos los elementos consecuentes, lo que puede ser lento si la matriz es enorme y el elemento insertado o eliminado está cerca del principio de la lista.

LinkedList es una lista doblemente enlazada: los elementos individuales se colocan enobjetos de nodo que tienen referencias al nodo anterior y siguiente. Esta implementación puede parecer más eficiente que ArrayList si tiene muchas inserciones o eliminaciones en diferentes partes de la lista, especialmente si la lista es grande.

En la mayoría de los casos, sin embargo, ArrayList supera a LinkedList . Incluso los elementos que cambian en ArrayList , aunque son una operación O (n), se implementan como una llamada muy rápida a System.arraycopy () . Incluso puede parecer más rápido que la inserción O (1) de LinkedList que requiere instanciar un objeto Node y actualizar múltiples referencias bajo el capó. LinkedList también puede tener una gran sobrecarga de memoria debido a la creación de varios objetos Node pequeños .

Q4. ¿Cuál es la diferencia entre Hashset y Treeset?

Tanto las clases HashSet como TreeSet implementan la interfaz Set y representan conjuntos de elementos distintos. Además, TreeSet implementa el NavigableSet interfaz. Esta interfaz define métodos que aprovechan el orden de los elementos.

HashSet se basa internamente en un HashMap , y TreeSet está respaldado por una instancia de TreeMap , que define sus propiedades: HashSet no mantiene los elementos en ningún orden en particular. La iteración sobre los elementos en un HashSet los produce en un orden aleatorio . TreeSet , por otro lado, produce elementos en orden de acuerdo con algún Comparador predefinido .

Q5. ¿Cómo se implementa Hashmap en Java? ¿Cómo utiliza su implementación Hashcode y métodos iguales de objetos? ¿Cuál es la complejidad temporal de colocar y obtener un elemento de dicha estructura?

La clase HashMap representa una estructura de datos de mapa hash típica con ciertas opciones de diseño.

El HashMap está respaldado por una matriz redimensionable que tiene un tamaño de potencia de dos. Cuando el elemento se agrega a un HashMap , primero se calcula su hashCode (un valor int ). Entonces, un cierto número de bits más bajos de este valor se utilizan como índice de matriz. Este índice apunta directamente a la celda de la matriz (denominada depósito) donde se debe colocar este par clave-valor. Acceder a un elemento por su índice en una matriz es una operación O (1) muy rápida, que es la característica principal de una estructura de mapa hash.

A hashCode is not unique, however, and even for different hashCodes, we may receive the same array position. This is called a collision. There is more than one way of resolving collisions in the hash map data structures. In Java's HashMap, each bucket actually refers not to a single object, but to a red-black tree of all objects that landed in this bucket (prior to Java 8, this was a linked list).

So when the HashMap has determined the bucket for a key, it has to traverse this tree to put the key-value pair in its place. If a pair with such key already exists in the bucket, it is replaced with a new one.

To retrieve the object by its key, the HashMap again has to calculate the hashCode for the key, find the corresponding bucket, traverse the tree, call equals on keys in the tree and find the matching one.

HashMap has O(1) complexity, or constant-time complexity, of putting and getting the elements. Of course, lots of collisions could degrade the performance to O(log(n)) time complexity in the worst case, when all elements land in a single bucket. This is usually solved by providing a good hash function with a uniform distribution.

When the HashMap internal array is filled (more on that in the next question), it is automatically resized to be twice as large. This operation infers rehashing (rebuilding of internal data structures), which is costly, so you should plan the size of your HashMap beforehand.

Q6. What Is the Purpose of the Initial Capacity and Load Factor Parameters of a Hashmap? What Are Their Default Values?

The initialCapacity argument of the HashMap constructor affects the size of the internal data structure of the HashMap, but reasoning about the actual size of a map is a bit tricky. The HashMap‘s internal data structure is an array with the power-of-two size. So the initialCapacity argument value is increased to the next power-of-two (for instance, if you set it to 10, the actual size of the internal array will be 16).

The load factor of a HashMap is the ratio of the element count divided by the bucket count (i.e. internal array size). For instance, if a 16-bucket HashMap contains 12 elements, its load factor is 12/16 = 0.75. A high load factor means a lot of collisions, which in turn means that the map should be resized to the next power of two. So the loadFactor argument is a maximum value of the load factor of a map. When the map achieves this load factor, it resizes its internal array to the next power-of-two value.

The initialCapacity is 16 by default, and the loadFactor is 0.75 by default, so you could put 12 elements in a HashMap that was instantiated with the default constructor, and it would not resize. The same goes for the HashSet, which is backed by a HashMap instance internally.

Consequently, it is not trivial to come up with initialCapacity that satisfies your needs. This is why the Guava library has Maps.newHashMapWithExpectedSize() and Sets.newHashSetWithExpectedSize() methods that allow you to build a HashMap or a HashSet that can hold the expected number of elements without resizing.

Q7. Describe Special Collections for Enums. What Are the Benefits of Their Implementation Compared to Regular Collections?

EnumSet and EnumMap are special implementations of Set and Map interfaces correspondingly. You should always use these implementations when you're dealing with enums because they are very efficient.

An EnumSet is just a bit vector with “ones” in the positions corresponding to ordinal values of enums present in the set. To check if an enum value is in the set, the implementation simply has to check if the corresponding bit in the vector is a “one”, which is a very easy operation. Similarly, an EnumMap is an array accessed with enum's ordinal value as an index. In the case of EnumMap, there is no need to calculate hash codes or resolve collisions.

Q8. What Is the Difference Between Fail-Fast and Fail-Safe Iterators?

Iterators for different collections are either fail-fast or fail-safe, depending on how they react to concurrent modifications. The concurrent modification is not only a modification of collection from another thread but also modification from the same thread but using another iterator or modifying the collection directly.

Fail-fast iterators (those returned by HashMap, ArrayList, and other non-thread-safe collections) iterate over the collection's internal data structure, and they throw ConcurrentModificationException as soon as they detect a concurrent modification.

Fail-safe iterators (returned by thread-safe collections such as ConcurrentHashMap, CopyOnWriteArrayList) create a copy of the structure they iterate upon. They guarantee safety from concurrent modifications. Their drawbacks include excessive memory consumption and iteration over possibly out-of-date data in case the collection was modified.

Q9. How Can You Use Comparable and Comparator Interfaces to Sort Collections?

The Comparable interface is an interface for objects that can be compared according to some order. Its single method is compareTo, which operates on two values: the object itself and the argument object of the same type. For instance, Integer, Long, and other numeric types implement this interface. String also implements this interface, and its compareTo method compares strings in lexicographical order.

The Comparable interface allows to sort lists of corresponding objects with the Collections.sort() method and uphold the iteration order in collections that implement SortedSet and SortedMap. If your objects can be sorted using some logic, they should implement the Comparable interface.

The Comparable interface usually is implemented using natural ordering of the elements. For instance, all Integer numbers are ordered from lesser to greater values. But sometimes you may want to implement another kind of ordering, for instance, to sort the numbers in descending order. The Comparator interface can help here.

The class of the objects you want to sort does not need to implement this interface. You simply create an implementing class and define the compare method which receives two objects and decides how to order them. You may then use the instance of this class to override the natural ordering of the Collections.sort() method or SortedSet and SortedMap instances.

As the Comparator interface is a functional interface, you may replace it with a lambda expression, as in the following example. It shows ordering a list using a natural ordering (Integer‘s Comparable interface) and using a custom iterator (Comparator interface).

List list1 = Arrays.asList(5, 2, 3, 4, 1); Collections.sort(list1); assertEquals(new Integer(1), list1.get(0)); List list1 = Arrays.asList(5, 2, 3, 4, 1); Collections.sort(list1, (a, b) -> b - a); assertEquals(new Integer(5), list1.get(0));
Siguiente » Preguntas de la entrevista del sistema de tipo Java