JavaRush /Blog Java /Random-ES /Análisis de preguntas y respuestas de entrevistas para de...

Análisis de preguntas y respuestas de entrevistas para desarrollador Java. parte 15

Publicado en el grupo Random-ES
Hola hola! ¿Cuánto necesita saber un desarrollador de Java? Puedes discutir durante mucho tiempo sobre este tema, pero la verdad es que en la entrevista te dejarás llevar por la teoría al máximo. Incluso en aquellas áreas del conocimiento que no tendrás la oportunidad de utilizar en tu trabajo. Análisis de preguntas y respuestas de entrevistas para desarrollador Java.  Parte 15 - 1Bueno, si eres principiante, tus conocimientos teóricos se tomarán muy en serio. Dado que todavía no hay experiencia ni grandes logros, solo queda comprobar la solidez de la base de conocimientos. Hoy continuaremos fortaleciendo esta misma base examinando las preguntas de entrevistas más populares para desarrolladores de Java. ¡Volemos!

Núcleo de Java

9. ¿Cuál es la diferencia entre enlace estático y dinámico en Java?

Esta pregunta ya la respondí en este artículo en la pregunta 18 sobre polimorfismo estático y dinámico, te aconsejo que lo leas.

10. ¿Es posible utilizar variables privadas o protegidas en una interfaz?

No, no puedes. Porque cuando declara una interfaz, el compilador de Java agrega automáticamente las palabras clave public y abstract antes de los métodos de la interfaz y las palabras clave public , static y final antes de los miembros de datos. En realidad, si agrega private o protected , surgirá un conflicto y el compilador se quejará del modificador de acceso con el mensaje: "El modificador '<modificador seleccionado>' no está permitido aquí". ¿Por qué el compilador agrega public , static y final? variables en la interfaz? Vamos a resolverlo:
  • público : la interfaz permite al cliente interactuar con el objeto. Si las variables no fueran públicas, los clientes no tendrían acceso a ellas.
  • estática : no se pueden crear interfaces (o más bien, sus objetos), por lo que la variable es estática.
  • final : dado que la interfaz se utiliza para lograr una abstracción del 100%, la variable tiene su forma final (y no se cambiará).

11. ¿Qué es Classloader y para qué se utiliza?

Classloader , o Class Loader, proporciona carga de clases Java. Más precisamente, la carga la proporcionan sus descendientes: cargadores de clases específicas, porque ClassLoader en sí es abstracto. Cada vez que se carga un archivo .class, por ejemplo, después de llamar a un constructor o método estático de la clase correspondiente, esta acción la realiza uno de los descendientes de la clase ClassLoader . Hay tres tipos de herederos:
  1. Bootstrap ClassLoader es un cargador básico, implementado a nivel de JVM y no tiene retroalimentación del entorno de ejecución, ya que es parte del kernel de JVM y está escrito en código nativo. Este cargador actúa como padre de todas las demás instancias de ClassLoader.

    Principalmente responsable de cargar las clases internas de JDK, generalmente rt.jar y otras bibliotecas principales ubicadas en el directorio $JAVA_HOME/jre/lib . Diferentes plataformas pueden tener diferentes implementaciones de este cargador de clases.

  2. Extension Classloader es un cargador de extensiones, un descendiente de la clase de cargador base. Se encarga de cargar la extensión de las clases base estándar de Java. Se carga desde el directorio de extensiones JDK, normalmente $JAVA_HOME/lib/ext o cualquier otro directorio mencionado en la propiedad del sistema java.ext.dirs (esta opción se puede utilizar para controlar la carga de extensiones).

  3. System ClassLoader es un cargador de sistema implementado en el nivel JRE que se encarga de cargar todas las clases a nivel de aplicación en la JVM. Carga archivos que se encuentran en la variable de entorno de clase -classpath o la opción de línea de comando -cp .

Análisis de preguntas y respuestas de entrevistas para desarrollador Java.  Parte 15 - 2Los cargadores de clases son parte del tiempo de ejecución de Java. En el momento en que la JVM solicita una clase, el cargador de clases intenta encontrar la clase y cargar la definición de clase en el tiempo de ejecución utilizando el nombre completo de la clase. El método java.lang.ClassLoader.loadClass() es responsable de cargar la definición de clase en tiempo de ejecución. Intenta cargar una clase según su nombre completo. Si la clase aún no se ha cargado, delega la solicitud al cargador de clases principal. Este proceso ocurre de forma recursiva y tiene este aspecto:
  1. System Classloader intenta encontrar la clase en su caché.

    • 1.1. Si se encuentra la clase, la carga se completa con éxito.

    • 1.2. Si no se encuentra la clase, la carga se delega al cargador de clases de extensión.

  2. Extension Classloader intenta encontrar la clase en su propio caché.

    • 2.1. Si se encuentra la clase, se completa con éxito.

    • 2.2. Si no se encuentra la clase, la carga se delega al cargador de clases Bootstrap.

  3. Bootstrap Classloader intenta encontrar la clase en su propio caché.

    • 3.1. Si se encuentra la clase, la carga se completa con éxito.

    • 3.2. Si no se encuentra la clase, el cargador de clases Bootstrap subyacente intentará cargarla.

  4. Si carga:

    • 4.1. Exitoso: se completó la carga de la clase.

    • 4.2. Si falla, el control se transfiere al cargador de clases de extensión.

  5. 5. Extension Classloader intenta cargar la clase y, si se carga:

    • 5.1. Exitoso: se completó la carga de la clase.

    • 5.2. Si no tiene éxito, el control se transfiere al System Classloader.

  6. 6. System Classloader intenta cargar la clase y, si se carga:

    • 6.1. Exitoso: se completó la carga de la clase.

    • 6.2. No se aprobó correctamente; se genera una excepción: ClassNotFoundException.

El tema de los cargadores de clases es muy amplio y no debe descuidarse. Para conocerlo con más detalle, le aconsejo que lea este artículo y no nos detendremos más y seguiremos adelante.

12. ¿Qué son las áreas de datos en tiempo de ejecución?

Ares de datos en tiempo de ejecución : áreas de datos en tiempo de ejecución de JVM. La JVM define algunas áreas de datos de tiempo de ejecución necesarias durante la ejecución del programa. Algunos de ellos se crean cuando se inicia la JVM. Otros son locales de subprocesos y se crean solo cuando se crea el subproceso (y se destruyen cuando se destruye el subproceso). Las áreas de datos del tiempo de ejecución de JVM tienen este aspecto: Análisis de preguntas y respuestas de entrevistas para desarrollador Java.  Parte 15 - 3
  • PC Register es local para cada subproceso y contiene la dirección de la instrucción JVM que el subproceso está ejecutando actualmente.

  • JVM Stack es un área de memoria que se utiliza como almacenamiento para variables locales y resultados temporales. Cada hilo tiene su propia pila separada: tan pronto como el hilo termina, esta pila también se destruye. Vale la pena señalar que la ventaja de la pila sobre el montón es el rendimiento, mientras que el montón ciertamente tiene una ventaja en la escala de almacenamiento.

  • Pila de métodos nativos: un área de datos por subproceso que almacena elementos de datos, similar a la pila JVM, para ejecutar métodos nativos (no Java).

  • Montón: utilizado por todos los subprocesos como almacenamiento que contiene objetos, metadatos de clases, matrices, etc., que se crean en tiempo de ejecución. Esta área se crea cuando se inicia la JVM y se destruye cuando se apaga.

  • Área de método: esta área de tiempo de ejecución es común para todos los subprocesos y se crea cuando se inicia la JVM. Almacena estructuras para cada clase, como Runtime Constant Pool, código para constructores y métodos, datos de métodos, etc.

13. ¿Qué es un objeto inmutable?

En esta parte del artículo, en las preguntas 14 y 15, ya hay respuesta a esta pregunta, así que echa un vistazo sin perder el tiempo.

14. ¿Qué tiene de especial la clase String?

Al principio del análisis, hablamos repetidamente sobre ciertas características de String (había una sección separada para esto). Ahora resumamos las características de String :
  1. Es el objeto más popular en Java y se utiliza para diversos fines. En términos de frecuencia de uso, no es inferior ni siquiera a los tipos primitivos.

  2. Se puede crear un objeto de esta clase sin utilizar la nueva palabra clave, directamente entre comillas String str = “string”; .

  3. String es una clase inmutable : al crear un objeto de esta clase, sus datos no se pueden cambiar (cuando agrega + "otra cadena" a una determinada cadena, como resultado obtendrá una nueva tercera cadena). La inmutabilidad de la clase String la hace segura para subprocesos.

  4. La clase String está finalizada (tiene el modificador final ), por lo que no se puede heredar.

  5. String tiene su propio grupo de cadenas, un área de memoria en el montón que almacena en caché los valores de cadena que crea. En esta parte de la serie , en la pregunta 62, describí el grupo de cadenas.

  6. Java tiene análogos de String , también diseñados para trabajar con cadenas: StringBuilder y StringBuffer , pero con la diferencia de que son mutables. Puedes leer más sobre ellos en este artículo .

Análisis de preguntas y respuestas de entrevistas para desarrollador Java.  Parte 15 - 4

15. ¿Qué es la covarianza de tipos?

Para comprender la covarianza, veremos un ejemplo. Digamos que tenemos una clase animal:
public class Animal {
 void voice() {
   System.out.println("*тишина*");
 }
}
Y alguna clase de Perro ampliándola :
public class Dog extends Animal {

 @Override
 public void voice() {
   System.out.println("Гав, гав, гав!!!");
 }
}
Como recordamos, podemos asignar fácilmente objetos del tipo heredero al tipo padre:
Animal animal = new Dog();
Esto no será más que polimorfismo. Cómodo y flexible, ¿verdad? Bueno, ¿qué pasa con la lista de animales? ¿ Podemos dar una lista con un Animal genérico y una lista con objetos Perro ?
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs;
En este caso, la línea para asignar la lista de perros a la lista de animales estará subrayada en rojo, es decir el compilador no pasará este código. A pesar de que esta asignación parece bastante lógica (después de todo, podemos asignar un objeto Perro a una variable de tipo Animal ), no se puede hacer. Esto se debe a que, si estuviera permitido, podríamos colocar un objeto Animal en una lista que originalmente estaba destinada a ser un Perro , mientras pensábamos que solo teníamos Perros en la lista . Y luego, por ejemplo, usaremos el método get() para tomar un objeto de esa lista de perros , pensando que es un perro, y llamaremos a algún método del objeto Perro que Animal no tiene . Y como comprenderá, esto es imposible: se producirá un error. Pero, afortunadamente, el compilador no pasa por alto este error lógico al asignar una lista de descendientes a una lista de padres (y viceversa). En Java, solo puede asignar objetos de lista a variables de lista con genéricos coincidentes. A esto se le llama invariación. Si pudieran hacer esto, se llamaría y se llama covarianza. Es decir, la covarianza es si pudiéramos establecer un objeto de tipo ArrayList<Dog> en una variable de tipo List<Animal> . ¿Resulta que la covarianza no es compatible con Java? ¡No importa cómo sea! Pero esto se hace a su manera especial. ¿ Para qué se utiliza el diseño ? extiende Animal . Se coloca con un genérico de la variable a la que queremos asignar el objeto de lista, con un genérico del descendiente. Esta construcción genérica significa que cualquier tipo que sea descendiente del tipo Animal servirá (y el tipo Animal también cae dentro de esta generalización). A su vez, Animal puede ser no sólo una clase, sino también una interfaz (no te dejes engañar por la palabra clave extends ). Podemos hacer nuestra tarea anterior así: Análisis de preguntas y respuestas de entrevistas para desarrollador Java.  Parte 15 - 5
List<Dog> dogs = new ArrayList<>();
List<? extends Animal> animals = dogs;
Como resultado, verá en el IDE que el compilador no se quejará de esta construcción. Comprobemos la funcionalidad de este diseño. Digamos que tenemos un método que hace que todos los animales que se le pasan emitan sonidos:
public static void animalsVoice(List<? extends Animal> animals) {
 for (Animal animal : animals) {
   animal.voice();
 }
}
Démosle una lista de perros:
List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new Dog());
dogs.add(new Dog());
animalsVoice(dogs);
En la consola veremos el siguiente resultado:
¡¡¡Guau, guau, guau!!! ¡¡¡Guau, guau, guau!!! ¡¡¡Guau, guau, guau!!!
Esto significa que este enfoque de la covarianza funciona con éxito. Déjame señalar que este genérico está incluido en la lista . extiende Animal no podemos insertar nuevos datos de ningún tipo: ni el tipo Perro , ni siquiera el tipo Animal :
List<Dog> dogs = new ArrayList<>();
List<? extends Animal> animals = dogs;
animals.add(new Dog());
dogs.add(new Animal());
De hecho, en las dos últimas líneas el compilador resaltará en rojo la inserción de objetos. Esto se debe a que no podemos estar cien por cien seguros de qué lista de objetos y de qué tipo se asignará a la lista con datos mediante el genérico <? extiende Animal> . Análisis de preguntas y respuestas de entrevistas para desarrollador Java.  Parte 15 - 6También me gustaría hablar de contravarianza , ya que normalmente este concepto siempre va de la mano de covarianza, y por regla general se preguntan por ellos juntos. Este concepto es algo opuesto a la covarianza, ya que esta construcción utiliza el tipo heredero. Digamos que queremos una lista a la que se le pueda asignar una lista de objetos de tipo que no sean ancestros del objeto Perro . Sin embargo, no sabemos de antemano qué tipos específicos serán. En este caso, ¿una construcción de la forma ? super Perro , para el cual todos los tipos son adecuados - los progenitores de la clase Perro :
List<Animal> animals = new ArrayList<>();
List<? super Dog> dogs = animals;
dogs.add(new Dog());
dogs.add(new Dog());
Podemos agregar de forma segura objetos de tipo Perro a la lista con un genérico de este tipo, porque en cualquier caso tiene todos los métodos implementados de cualquiera de sus antepasados. Pero no podremos agregar un objeto de tipo Animal , ya que no hay certeza de que dentro haya objetos de este tipo, y no, por ejemplo, Perro . Después de todo, podemos solicitar de un elemento de esta lista un método de la clase Perro , que Animal no tendrá . En este caso, se producirá un error de compilación. Además, si quisiéramos implementar el método anterior, pero con este genérico:
public static void animalsVoice(List<? super Dog> dogs) {
 for (Dog dog : dogs) {
   dog.voice();
 }
}
obtendríamos un error de compilación en el bucle for , ya que no podemos estar seguros de que la lista devuelta contenga objetos de tipo Perro y podamos usar sus métodos libremente. Si llamamos al método dogs.get(0) en esta lista . - obtendremos un objeto de tipo Objeto . Es decir, para que el método animalesVoice() funcione , al menos necesitamos agregar pequeñas manipulaciones para limitar el tipo de datos:
public static void animalsVoice(List<? super Dog> dogs) {
 for (Object obj : dogs) {
   if (obj instanceof Dog) {
     Dog dog = (Dog) obj;
     dog.voice();
   }
 }
}
Análisis de preguntas y respuestas de entrevistas para desarrollador Java.  Parte 15 - 7

16. ¿Cómo existen los métodos en la clase Objeto?

En esta parte de la serie, en el párrafo 11, ya respondí esta pregunta, por lo que te recomiendo encarecidamente que la leas si aún no lo has hecho. Ahí es donde terminaremos por hoy. ¡Nos vemos en la siguiente parte! Análisis de preguntas y respuestas de entrevistas para desarrollador Java.  Parte 15 - 8
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION