JavaRush /Blog Java /Random-ES /Núcleo de Java. Preguntas de la entrevista, parte 2
Andrey
Nivel 26

Núcleo de Java. Preguntas de la entrevista, parte 2

Publicado en el grupo Random-ES
Para quienes escuchan la palabra Java Core por primera vez, estos son los fundamentos fundamentales del lenguaje. Con este conocimiento, puede realizar con seguridad una pasantía/prácticas.
Núcleo de Java.  Preguntas para una entrevista, parte 2 - 1
Estas preguntas le ayudarán a actualizar sus conocimientos antes de la entrevista o a aprender algo nuevo por sí mismo. Para adquirir habilidades prácticas, estudie en JavaRush . Artículo original Enlaces a otras partes: Java Core. Preguntas de la entrevista, parte 1 Java Core. Preguntas para una entrevista, parte 3

¿Por qué debería evitarse el método finalize()?

Todos conocemos la afirmación de que finalize()el recolector de basura llama a un método antes de liberar la memoria ocupada por un objeto. A continuación se muestra un programa de ejemplo que demuestra que finalize()no se garantiza una llamada a un método:
public class TryCatchFinallyTest implements Runnable {

	private void testMethod() throws InterruptedException
	{
		try
		{
			System.out.println("In try block");
			throw new NullPointerException();
		}
		catch(NullPointerException npe)
		{
			System.out.println("In catch block");
		}
		finally
		{
			System.out.println("In finally block");
		}
	}

	@Override
	protected void finalize() throws Throwable {
		System.out.println("In finalize block");
		super.finalize();
	}

	@Override
	public void run() {
		try {
			testMethod();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
public class TestMain
{
	@SuppressWarnings("deprecation")
	public static void main(String[] args) {
	for(int i=1;i< =3;i++)
	{
		new Thread(new TryCatchFinallyTest()).start();
	}
	}
}
Salida: En el bloque de prueba En el bloque de captura En el bloque de finalmente En el bloque de prueba En el bloque de captura En el bloque de finalmente En el bloque de prueba En el bloque de captura En el bloque de finalmente Sorprendentemente, el método finalizeno se ejecutó para ningún hilo. Esto prueba mis palabras. Creo que la razón es que los finalizadores son ejecutados por un subproceso recolector de basura separado. Si la máquina virtual Java finaliza demasiado pronto, el recolector de basura no tiene tiempo suficiente para crear y ejecutar finalizadores. Otras razones para no utilizar el método finalize()pueden ser:
  1. El método finalize()no funciona con cadenas como constructores. Esto significa que cuando llamas a un constructor de clase, los constructores de superclase se llamarán incondicionalmente. Pero en el caso del método finalize(), esto no sucederá. El método de la superclase finalize()debe llamarse explícitamente.
  2. Cualquier excepción lanzada por el método finalizees ignorada por el subproceso del recolector de basura y no se propagará más, lo que significa que el evento no se registrará en sus registros. Esto es muy malo, ¿no?
  3. También obtienes una penalización de rendimiento significativa si el método finalize()está presente en tu clase. En Programación efectiva (2ª ed.), Joshua Bloch dijo:
    “Sí, y una cosa más: hay una gran penalización en el rendimiento cuando se utilizan finalizadores. En mi máquina, el tiempo para crear y destruir objetos simples es de aproximadamente 5,6 nanosegundos.
    Agregar un finalizador aumenta el tiempo a 2400 nanosegundos. En otras palabras, es aproximadamente 430 veces más lento crear y eliminar un objeto con un finalizador”.

¿Por qué no debería utilizarse HashMap en un entorno de subprocesos múltiples? ¿Podría esto causar un bucle infinito?

Sabemos que HashMapse trata de una colección no sincronizada, cuya contraparte sincronizada es HashTable. Por lo tanto, cuando accede a una colección y se encuentra en un entorno de subprocesos múltiples donde todos los subprocesos tienen acceso a una única instancia de la colección, es más seguro usarlo HashTablepor razones obvias, como evitar lecturas sucias y garantizar la coherencia de los datos. En el peor de los casos, este entorno de subprocesos múltiples provocará un bucle infinito. Sí, es verdad. HashMap.get()puede causar un bucle infinito. ¿Veamos cómo? Si observa el código fuente del método HashMap.get(Object key), se ve así:
public Object get(Object key) {
    Object k = maskNull(key);
    int hash = hash(k);
    int i = indexFor(hash, table.length);
    Entry e = table[i];
    while (true) {
        if (e == null)
            return e;
        if (e.hash == hash && eq(k, e.key))
            return e.value;
        e = e.next;
    }
}
while(true)siempre puede ser víctima de un bucle infinito en un entorno de ejecución de subprocesos múltiples si por alguna razón e.nextpuede apuntar a sí mismo. Esto provocará un bucle sin fin, pero ¿cómo e.nextapuntará a sí mismo (es decir, a e)? Esto puede suceder en un método void transfer(Entry[] newTable)que se llama mientras se HashMapcambia su tamaño.
do {
    Entry next = e.next;
    int i = indexFor(e.hash, newCapacity);
    e.next = newTable[i];
    newTable[i] = e;
    e = next;
} while (e != null);
Este fragmento de código es propenso a crear un bucle infinito si el cambio de tamaño ocurre al mismo tiempo que otro hilo intenta cambiar la instancia del mapa ( HashMap). La única forma de evitar este escenario es utilizar la sincronización en su código o, mejor aún, utilizar una colección sincronizada.

Explicar la abstracción y la encapsulación. ¿Cómo están conectados?

En palabras simples , " la abstracción muestra sólo aquellas propiedades de un objeto que son importantes para la vista actual " . En la teoría de la programación orientada a objetos, la abstracción implica la capacidad de definir objetos que representan "actores" abstractos que pueden realizar trabajos, cambiar e informar cambios en su estado e "interactuar" con otros objetos en el sistema. La abstracción en cualquier lenguaje de programación funciona de muchas maneras. Esto se puede ver en la creación de rutinas para definir interfaces para comandos de lenguaje de bajo nivel. Algunas abstracciones intentan limitar la amplitud de la representación general de las necesidades de un programador ocultando por completo las abstracciones sobre las que se basan, como los patrones de diseño. Normalmente, la abstracción se puede ver de dos maneras: la abstracción de datos es una forma de crear tipos de datos complejos y exponer solo operaciones significativas para interactuar con el modelo de datos, mientras que al mismo tiempo oculta todos los detalles de implementación del mundo exterior. La abstracción de ejecución es el proceso de identificar todas las declaraciones significativas y exponerlas como una unidad de trabajo. Generalmente usamos esta característica cuando creamos un método para realizar algún trabajo. Confinar datos y métodos dentro de clases en combinación con ocultar (usando control de acceso) a menudo se denomina encapsulación. El resultado es un tipo de datos con características y comportamiento. Básicamente, la encapsulación también implica ocultar datos y ocultar implementaciones. "Resumir todo lo que puede cambiar" . Esta cita es un principio de diseño bien conocido. De hecho, en cualquier clase, los cambios de datos pueden ocurrir en tiempo de ejecución y los cambios de implementación pueden ocurrir en versiones futuras. Por tanto, la encapsulación se aplica tanto a los datos como a la implementación. Entonces se pueden conectar así:
  • La abstracción es principalmente lo que puede hacer una clase [Idea]
  • La encapsulación es más Cómo lograr esta funcionalidad [Implementación]

¿Diferencias entre interfaz y clase abstracta?

Las principales diferencias se pueden enumerar de la siguiente manera:
  • Una interfaz no puede implementar ningún método, pero una clase abstracta sí.
  • Una clase puede implementar muchas interfaces, pero sólo puede tener una superclase (abstracta o no abstracta)
  • Una interfaz no forma parte de una jerarquía de clases. Las clases no relacionadas pueden implementar la misma interfaz.
Lo que hay que recordar es esto: “Cuando se puede describir completamente un concepto en términos de “qué hace” sin tener que especificar “cómo lo hace”, entonces se debe utilizar una interfaz. Si necesita incluir algunos detalles de implementación, entonces necesita representar su concepto en una clase abstracta". Además, para decirlo de otra manera: ¿hay muchas clases que se pueden "agrupar" y describir con un solo sustantivo? Si es así, cree una clase abstracta con el nombre de este sustantivo y herede las clases de él. Por ejemplo, Caty Dogpuede heredar de la clase abstracta Animal, y esta clase base abstracta implementará el método void Breathe()- respirar, que todos los animales realizarán de la misma manera. ¿Qué verbos se pueden aplicar a mi clase y se pueden aplicar a otras? Crea una interfaz para cada uno de estos verbos. Por ejemplo, todos los animales pueden comer, así que crearé una interfaz IFeedabley haré Animalque la implemente. Sólo lo suficientemente bueno para implementar una interfaz Dog( capaz de gustarme), pero no todo. Alguien dijo: la principal diferencia es dónde desea su implementación. Cuando crea una interfaz, puede mover la implementación a cualquier clase que implemente su interfaz. Al crear una clase abstracta, puedes compartir la implementación de todas las clases derivadas en un solo lugar y evitar muchas cosas malas como la duplicación de código. HorseILikeable

¿Cómo ahorra memoria StringBuffer?

La clase Stringse implementa como un objeto inmutable, lo que significa que cuando inicialmente decides poner algo en el objeto String, la máquina virtual asigna una matriz de longitud fija exactamente del tamaño de tu valor original. Luego, esto se tratará como una constante dentro de la máquina virtual, lo que proporciona una mejora significativa del rendimiento si el valor de la cadena no cambia. Sin embargo, si decide cambiar el contenido de una cadena de alguna manera, lo que realmente hace la máquina virtual es copiar el contenido de la cadena original en un espacio temporal, realizar los cambios y luego guardarlos en una nueva matriz de memoria. Por tanto, realizar cambios en el valor de una cadena después de la inicialización es una operación costosa. StringBuffer, por otro lado, se implementa como una matriz que se expande dinámicamente dentro de la máquina virtual, lo que significa que cualquier operación de modificación puede ocurrir en una celda de memoria existente y se asignará nueva memoria según sea necesario. Sin embargo, no hay forma de que la máquina virtual realice la optimización StringBufferporque su contenido se considera inconsistente en cada instancia.

¿Por qué los métodos de espera y notificación se declaran en la clase Objeto en lugar de en Hilo?

Los métodos wait, notify, notifyAllsolo son necesarios cuando desea que sus subprocesos tengan acceso a recursos compartidos y el recurso compartido podría ser cualquier objeto Java en el montón. Por lo tanto, estos métodos se definen en la clase base Objectpara que cada objeto tenga un control que permita a los subprocesos esperar en su monitor. Java no tiene ningún objeto especial que se utilice para compartir un recurso compartido. No se define tal estructura de datos. Por lo tanto, es responsabilidad de la clase Objectpoder convertirse en un recurso compartido y proporcionar métodos auxiliares como wait(), notify(), notifyAll(). Java se basa en la idea de monitores de Charles Hoare. En Java, todos los objetos tienen un monitor. Los hilos esperan en los monitores, por lo que para realizar la espera necesitamos dos parámetros:
  • un hilo
  • monitor (cualquier objeto).
En el diseño de Java, un hilo no se puede definir con precisión; siempre es el hilo actual el que ejecuta el código. Sin embargo, podemos definir un monitor (que es un objeto sobre el que podemos llamar a un método wait). Este es un buen diseño porque si podemos forzar a cualquier otro hilo a esperar en un monitor específico, resultará en una "invasión", dificultando el diseño/programación de programas paralelos. Recuerde que en Java, todas las operaciones que interfieren con otros subprocesos están en desuso (por ejemplo, stop()).

Escriba un programa para crear un punto muerto en Java y solucionarlo

En Java deadlock, esta es una situación en la que al menos dos subprocesos mantienen un bloque en diferentes recursos y ambos están esperando que el otro recurso esté disponible para completar su tarea. Y ninguno de ellos es capaz de dejar un candado sobre el recurso que se retiene. Núcleo de Java.  Preguntas para una entrevista, parte 2 - 2 Programa de ejemplo:
package thread;

public class ResolveDeadLockTest {

	public static void main(String[] args) {
		ResolveDeadLockTest test = new ResolveDeadLockTest();

		final A a = test.new A();
		final B b = test.new B();

		// Thread-1
		Runnable block1 = new Runnable() {
			public void run() {
				synchronized (a) {
					try {
					// Добавляем задержку, чтобы обе нити могли начать попытки
					// блокирования ресурсов
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					// Thread-1 заняла A но также нуждается в B
					synchronized (b) {
						System.out.println("In block 1");
					}
				}
			}
		};

		// Thread-2
		Runnable block2 = new Runnable() {
			public void run() {
				synchronized (b) {
					// Thread-2 заняла B но также нуждается в A
					synchronized (a) {
						System.out.println("In block 2");
					}
				}
			}
		};

		new Thread(block1).start();
		new Thread(block2).start();
	}

	// Resource A
	private class A {
		private int i = 10;

		public int getI() {
			return i;
		}

		public void setI(int i) {
			this.i = i;
		}
	}

	// Resource B
	private class B {
		private int i = 20;

		public int getI() {
			return i;
		}

		public void setI(int i) {
			this.i = i;
		}
	}
}
La ejecución del código anterior provocará un punto muerto por razones muy obvias (explicadas anteriormente). Ahora necesitamos resolver este problema. Creo que la solución a cualquier problema está en la raíz del problema mismo. En nuestro caso, el modelo de acceso a A y B es el principal problema. Por tanto, para solucionarlo simplemente cambiamos el orden de los operadores de acceso a recursos compartidos. Después del cambio quedará así:
// Thread-1
Runnable block1 = new Runnable() {
	public void run() {
		synchronized (b) {
			try {
				// Добавляем задержку, чтобы обе нити могли начать попытки
				// блокирования ресурсов
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			// Thread-1 заняла B но также нуждается в А
			synchronized (a) {
				System.out.println("In block 1");
			}
		}
	}
};

// Thread-2
Runnable block2 = new Runnable() {
	public void run() {
		synchronized (b) {
			// Thread-2 заняла B но также нуждается в А
			synchronized (a) {
				System.out.println("In block 2");
			}
		}
	}
};
Ejecute esta clase nuevamente y ahora no verá el punto muerto. Espero que esto le ayude a evitar puntos muertos y deshacerse de ellos si los encuentra.

¿Qué sucede si su clase que implementa la interfaz Serializable contiene un componente no serializable? ¿Cómo arreglar esto?

En este caso, será arrojado NotSerializableExceptiondurante la ejecución. Para solucionar este problema, existe una solución muy sencilla: marque estas casillas transient. Esto significa que los campos marcados no se serializarán. Si también desea almacenar el estado de estos campos, debe considerar las variables de referencia, que ya implementan el archivo Serializable. Es posible que también necesite utilizar los métodos readResolve()y writeResolve(). Resumamos:
  • Primero, haga que su campo no sea serializable transient.
  • Primero writeObject, llame defaultWriteObjectal hilo para guardar todos los campos que no sean transientcampos, luego llame a los métodos restantes para serializar las propiedades individuales de su objeto no serializable.
  • En readObject, primero llame defaultReadObjecta la secuencia para leer todos transientlos campos que no son, luego llame a otros métodos (correspondientes a los que agregó writeObject) para deserializar su no transientobjeto.

Explicar las palabras clave transitorias y volátiles en Java.

"La palabra clave transientse utiliza para indicar campos que no se serializarán". Según la especificación del lenguaje Java: las variables se pueden marcar con el indicador transitorio para indicar que no forman parte del estado persistente del objeto. Por ejemplo, puede contener campos derivados de otros campos y es preferible obtenerlos mediante programación en lugar de restaurar su estado mediante la serialización. Por ejemplo, en una clase, BankPayment.javacampos como principal(director) y rate(tasa) se pueden serializar y interest(intereses acumulados) se pueden calcular en cualquier momento, incluso después de la deserialización. Si recordamos, cada hilo en Java tiene su propia memoria local y realiza operaciones de lectura/escritura en esta memoria local. Cuando se realizan todas las operaciones, escribe el estado modificado de la variable en la memoria compartida, desde donde todos los subprocesos acceden a la variable. Normalmente, se trata de un hilo normal dentro de una máquina virtual. Pero el modificador volátil le dice a la máquina virtual que el acceso de un hilo a esa variable siempre debe hacer coincidir su propia copia de esa variable con la copia maestra de la variable en la memoria. Esto significa que cada vez que un hilo quiera leer el estado de una variable, debe borrar el estado de la memoria interna y actualizar la variable desde la memoria principal. Volatilemás útil en algoritmos sin bloqueo. Marca una variable que almacena datos compartidos como volátil, luego no usa bloqueos para acceder a esa variable y todos los cambios realizados por un hilo serán visibles para los demás. O si desea crear una relación de "sucedió después" para garantizar que los cálculos no se repitan, nuevamente para garantizar que los cambios sean visibles en tiempo real. Volatile debe usarse para publicar de forma segura objetos inmutables en un entorno de subprocesos múltiples. La declaración de campo public volatile ImmutableObjectgarantiza que todos los subprocesos siempre vean la referencia disponible actualmente a la instancia.

¿Diferencia entre Iterador y ListIterator?

Podemos usar , o Iteratorpara iterar sobre elementos . Pero sólo se puede utilizar para iterar sobre elementos . Otras diferencias se describen a continuación. Puede: SetListMapListIteratorList
  1. iterar en orden inverso.
  2. obtener índice en cualquier lugar.
  3. agregue cualquier valor en cualquier lugar.
  4. establezca cualquier valor en la posición actual.
¡¡Buena suerte con sus estudios!! Autor del artículo Lokesh Gupta Artículo original Java Core. Preguntas de la entrevista, parte 1 Java Core. Preguntas para una entrevista, parte 3
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION