Preguntas de la entrevista de Complete Works of Java (3)

Preguntas de la entrevista de Complete Works of Java (3)

Luo Hao es jaja

21. Describa el principio y mecanismo de los archivos de clases de carga de JVM.

Respuesta: La carga de clases en la JVM es implementada por el cargador de clases (ClassLoader) y sus subclases. El cargador de clases en Java es un componente importante del sistema en tiempo de ejecución de Java, que es responsable de encontrar y cargar clases en tiempo de ejecución La clase en el archivo.
Debido a la naturaleza multiplataforma de Java, el programa fuente Java compilado no es un programa ejecutable, sino uno o más archivos de clase. Cuando un programa Java necesita utilizar una determinada clase, la JVM se asegurará de que esta clase se haya cargado, conectado (verificado, preparado y analizado) e inicializado. La carga de clases se refiere a leer los datos en el archivo .class de la clase en la memoria, generalmente creando una matriz de bytes para leer en el archivo .class y luego generando el objeto Class correspondiente a la clase cargada. Después de la carga, el objeto Clase no está completo, por lo que la clase en este momento aún no está disponible. Cuando la clase está cargada, entra en la fase de conexión, fase que incluye tres pasos: verificación, preparación (asignación de memoria para variables estáticas y configuración del valor inicial por defecto) y resolución (sustitución de referencias de símbolos por referencias directas). Finalmente, la JVM inicializa la clase, incluyendo: 1) Si la clase tiene una clase principal directa y la clase no se ha inicializado, entonces inicialice la clase principal primero; 2) Si hay declaraciones de inicialización en la clase, estas declaraciones de inicialización se ejecutan en secuencia.
La carga de clases la realiza el cargador de clases, que incluye: cargador raíz (BootStrap), cargador de extensión (Extensión), cargador del sistema (Sistema) y cargador de clases definido por el usuario (java.lang.ClassLoader) Subclase). A partir de Java 2 (JDK 1.2), se ha adoptado el mecanismo de delegación principal (PDM) para el proceso de carga de clases. PDM garantiza mejor la seguridad de la plataforma Java En este mecanismo, el Bootstrap que viene con la JVM es el cargador raíz, y otros cargadores tienen y solo tienen un cargador padre. La carga de la clase primero solicita al cargador de clases principal que se cargue, y el cargador de clases secundario lo cargará cuando el cargador de clases principal no pueda hacer nada. La JVM no proporciona una referencia a Bootstrap para programas Java. La siguiente es una descripción de varios cargadores de clases:

  • Bootstrap: generalmente implementado con código local, responsable de cargar la biblioteca básica de clases de JVM (rt.jar);
  • Extensión: cargue la biblioteca de clases desde el directorio especificado por la propiedad del sistema java.ext.dirs, y su cargador principal es Bootstrap;
  • Sistema: también llamado cargador de clases de aplicaciones, su clase principal es Extension. Es el cargador de clases más utilizado. Registra clases del directorio especificado por la variable de entorno classpath o la propiedad del sistema java.class.path, y es el cargador principal predeterminado del cargador definido por el usuario.

22. ¿Se puede almacenar un carácter chino en una variable char?

Respuesta: El tipo char puede almacenar un carácter chino, porque la codificación utilizada en Java es Unicode (no seleccione ninguna codificación específica, use el número del carácter en el juego de caracteres directamente, este es el único método unificado), un tipo char ocupa 2 caracteres Sección (16 bits), por lo que está bien poner un chino.

Suplemento: El uso de Unicode significa que los caracteres tienen diferentes manifestaciones dentro y fuera de la JVM. Dentro de la JVM, todos son Unicode. Cuando el carácter se transfiere desde la JVM al exterior (como se almacena en el sistema de archivos), se requiere la conversión de codificación. Así que hay flujos de bytes y flujos de caracteres en Java, así como flujos de conversión que convierten entre flujos de caracteres y flujos de bytes, como InputStreamReader y OutputStreamReader. Estas dos clases son clases adaptadoras entre flujos de bytes y flujos de caracteres. La tarea de conversión de código; para los programadores de C, para completar dicha conversión de código, me temo que debo confiar en las características de la unión (unión / unión) de memoria compartida para lograrlo.

23. ¿Cuáles son las similitudes y diferencias entre la clase abstracta y la interfaz?

Respuesta: No se pueden crear instancias de clases e interfaces abstractas, pero se pueden definir referencias a clases abstractas y tipos de interfaz. Si una clase hereda una clase abstracta o implementa una interfaz, todos los métodos abstractos en ella deben implementarse; de ​​lo contrario, la clase aún debe declararse como una clase abstracta. Las interfaces son más abstractas que las clases abstractas, porque las clases abstractas pueden definir constructores, métodos abstractos y métodos concretos, pero las interfaces no pueden definir constructores y todos los métodos en ellos son métodos abstractos. Los miembros de la clase abstracta pueden ser privados, predeterminados, protegidos y públicos, mientras que los miembros de la interfaz son todos públicos. Las variables miembro se pueden definir en clases abstractas y las variables miembro definidas en interfaces son en realidad constantes. Las clases con métodos abstractos deben declararse como clases abstractas y las clases abstractas no necesariamente tienen métodos abstractos.

24. ¿Cuál es la diferencia entre la clase anidada estática y la clase interior?

Respuesta: Static Nested Class es una clase interna que se declara estática y se puede crear una instancia sin depender de instancias de clases externas. Es necesario crear una instancia de la clase interna habitual después de crear una instancia de la clase externa, y la sintaxis parece muy extraña, como se muestra a continuación.


/**
 * 扑克类(一副扑克)
 * @author 骆昊
 *
 */
public class Poker {
    private static String[] suites = {"黑桃", "红桃", "草花", "方块"};
    private static int[] faces = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};

    private Card[] cards;

    /**
     * 构造器
     * 
     */
    public Poker() {
        cards = new Card[52];
        for(int i = 0; i < suites.length; i++) {
            for(int j = 0; j < faces.length; j++) {
                cards[i * 13 + j] = new Card(suites[i], faces[j]);
            }
        }
    }

    /**
     * 洗牌 (随机乱序)
     * 
     */
    public void shuffle() {
        for(int i = 0, len = cards.length; i < len; i++) {
            int index = (int) (Math.random() * len);
            Card temp = cards[index];
            cards[index] = cards[i];
            cards[i] = temp;
        }
    }

    /**
     * 发牌
     * @param index 发牌的位置
     * 
     */
    public Card deal(int index) {
        return cards[index];
    }

    /**
     * 卡片类(一张扑克)
     * [内部类]
     * @author 骆昊
     *
     */
    public class Card {
        private String suite;   // 花色
        private int face;       // 点数

        public Card(String suite, int face) {
            this.suite = suite;
            this.face = face;
        }

        @Override
        public String toString() {
            String faceStr = "";
            switch(face) {
            case 1: faceStr = "A"; break;
            case 11: faceStr = "J"; break;
            case 12: faceStr = "Q"; break;
            case 13: faceStr = "K"; break;
            default: faceStr = String.valueOf(face);
            }
            return suite + faceStr;
        }
    }
}

Código de prueba:


class PokerTest {
    public static void main(String[] args) {
        Poker poker = new Poker();
        poker.shuffle();                // 洗牌
        Poker.Card c1 = poker.deal(0);  // 发第一张牌
        // 对于非静态内部类Card
        // 只有通过其外部类Poker对象才能创建Card对象
        Poker.Card c2 = poker.new Card("红心", 1);    // 自己创建一张牌
        System.out.println(c1);     // 洗牌后的第一张
        System.out.println(c2);     // 打印: 红心A
    }
}

Pregunta de la entrevista: ¿Dónde generará errores de compilación el siguiente código?


class Outer {
    class Inner {}
    public static void foo() { new Inner(); }
    public void bar() { new Inner(); }
    public static void main(String[] args) {
        new Inner();
    }
}

Nota: La creación de objetos de clase internos no estáticos en Java depende de los objetos de clase externos. En las preguntas de la entrevista anteriores, los métodos foo y main son métodos estáticos. No hay esto en el método estático, lo que significa que no existe el llamado objeto de clase externa. Cree un objeto de clase interna. Si desea crear un objeto de clase interna en un método estático, puede hacer esto:


    new Outer().new Inner();

25. ¿Habrá una pérdida de memoria en Java? Descríbala brevemente.

Respuesta: Teóricamente, debido al mecanismo de recolección de basura (GC) en Java, no hay problemas de pérdida de memoria (esta es también una razón importante por la cual Java se usa ampliamente en la programación del lado del servidor); sin embargo, en el desarrollo real, puede haber problemas inútiles pero accesibles Objetos, estos objetos no pueden ser recuperados por GC, por lo que también pueden ocurrir pérdidas de memoria. Por ejemplo, los objetos en la sesión de hibernación (caché de primer nivel) están en un estado persistente y el recolector de basura no recuperará estos objetos. Sin embargo, puede haber objetos basura inútiles en estos objetos. Si no están cerrados o vaciados a tiempo, La caché de primer nivel puede provocar pérdidas de memoria. El código del siguiente ejemplo también provocará pérdidas de memoria.


import java.util.Arrays;
import java.util.EmptyStackException;
public class MyStack<T> {
    private T[] elements;
    private int size = 0;
    private static final int INIT_CAPACITY = 16;
    public MyStack() {
        elements = (T[]) new Object[INIT_CAPACITY];
    }
    public void push(T elem) {
        ensureCapacity();
        elements[size++] = elem;
    }
    public T pop() {
        if(size == 0) 
            throw new EmptyStackException();
        return elements[--size];
    }
    private void ensureCapacity() {
        if(elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

El código anterior implementa una estructura de pila (primero en entrar, último en salir (FILO)). A primera vista, no parece haber ningún problema obvio. Incluso puede pasar las distintas pruebas unitarias que escribe. Sin embargo, el método pop tiene un problema de pérdida de memoria. Cuando colocamos un objeto en la pila con el método pop, el objeto no se tratará como basura, incluso si el programa que usa la pila ya no hace referencia a estos objetos, porque la pila se mantiene internamente. Referencias obsoletas a estos objetos. En los lenguajes que admiten la recolección de basura, las fugas de memoria están muy ocultas y son en realidad retención de objetos inconscientes. Si una referencia de objeto está reservada inconscientemente, entonces el recolector de basura no procesará el objeto, ni procesará otros objetos a los que hace referencia el objeto, incluso si solo hay algunos de esos objetos, puede causar la exclusión de muchos objetos. Además de la recolección de basura, tiene un impacto significativo en el rendimiento. En casos extremos, se activará la paginación de disco (memoria física y memoria virtual de datos de intercambio de disco duro) e incluso se provocará un error de memoria externa.

26. ¿Pueden los métodos abstractos ser estáticos al mismo tiempo, pueden ser métodos nativos al mismo tiempo y pueden ser modificados sincronizados al mismo tiempo?

Respuesta: Ninguno. Los métodos abstractos deben ser reemplazados por subclases, mientras que los métodos estáticos no se pueden reemplazar, por lo que los dos son contradictorios. Los métodos nativos son métodos implementados por códigos nativos (como el código C), mientras que los métodos abstractos no están implementados y son contradictorios. La sincronización está relacionada con los detalles de implementación del método, y los métodos abstractos no involucran detalles de implementación, por lo que son contradictorios.

27. Explique la diferencia entre variables estáticas y variables de instancia.

Respuesta: Una variable estática es una variable modificada por el modificador estático. También se llama variable de clase. Pertenece a una clase y no pertenece a ningún objeto de la clase. No importa cuántos objetos se creen en una clase, solo hay una copia de la variable estática en la memoria. ; Las variables de instancia deben depender de una instancia, debe crear un objeto y luego acceder a él a través del objeto. Se pueden implementar variables estáticas para permitir que varios objetos compartan memoria.

Suplemento: En el desarrollo de Java, suele haber una gran cantidad de miembros estáticos en clases de contexto y clases de herramientas.

28. ¿Es posible llamar a un método no estático desde dentro de un método estático?

Respuesta: No, los métodos estáticos solo pueden acceder a miembros estáticos, porque la llamada de métodos no estáticos primero debe crear el objeto, y es posible que el objeto no se inicialice al llamar al método estático.

29. ¿Cómo implementar la clonación de objetos?

Respuesta: Hay dos formas:
  1) Realice la interfaz Cloneable y vuelva a escribir el método clone () en la clase Object;
  2) Realice la interfaz Serializable, a través de la serialización y deserialización de objetos para lograr la clonación, puede lograr una verdadera clonación profunda , el código se muestra a continuación.


import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class MyUtil {
    private MyUtil() {
        throw new AssertionError();
    }
    @SuppressWarnings("unchecked")
    public static <T extends Serializable> T clone(T obj) throws Exception {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bout);
        oos.writeObject(obj);
        ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bin);
        return (T) ois.readObject();
        // 说明:调用ByteArrayInputStream或ByteArrayOutputStream对象的close方法没有任何意义
        // 这两个基于内存的流只要垃圾回收器清理对象就能够释放资源,这一点不同于对外部资源(如文件流)的释放
    }
}
下面是测试代码:
import java.io.Serializable;
/**
 * 人类
 * @author 骆昊
 *
 */
class Person implements Serializable {
    private static final long serialVersionUID = -9102017020286042305L;
    private String name;    // 姓名
    private int age;        // 年龄
    private Car car;        // 座驾
    public Person(String name, int age, Car car) {
        this.name = name;
        this.age = age;
        this.car = car;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public Car getCar() {
        return car;
    }
    public void setCar(Car car) {
        this.car = car;
    }
    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + ", car=" + car + "]";
    }
}
/**
 * 小汽车类
 * @author 骆昊
 *
 */
class Car implements Serializable {
    private static final long serialVersionUID = -5713945027627603702L;
    private String brand;       // 品牌
    private int maxSpeed;       // 最高时速
    public Car(String brand, int maxSpeed) {
        this.brand = brand;
        this.maxSpeed = maxSpeed;
    }
    public String getBrand() {
        return brand;
    }
    public void setBrand(String brand) {
        this.brand = brand;
    }
    public int getMaxSpeed() {
        return maxSpeed;
    }
    public void setMaxSpeed(int maxSpeed) {
        this.maxSpeed = maxSpeed;
    }
    @Override
    public String toString() {
        return "Car [brand=" + brand + ", maxSpeed=" + maxSpeed + "]";
    }
}

class CloneTest {
    public static void main(String[] args) {
        try {
            Person p1 = new Person("Hao LUO", 33, new Car("Benz", 300));
            Person p2 = MyUtil.clone(p1);   // 深度克隆
            p2.getCar().setBrand("BYD");
            // 修改克隆的Person对象p2关联的汽车对象的品牌属性
            // 原来的Person对象p1关联的汽车不会受到任何影响
            // 因为在克隆Person对象时其关联的汽车对象也被克隆了
            System.out.println(p1);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Nota: La clonación basada en la serialización y deserialización no es solo una clonación profunda, sino que, lo que es más importante, a través de la restricción genérica, puede verificar si el objeto que se va a clonar admite la serialización. Esta verificación la realiza el compilador, no en Se lanza una excepción en tiempo de ejecución. Esta solución es obviamente mejor que usar el método de clonación de la clase Object para clonar un objeto. Siempre es mejor exponer el problema en tiempo de compilación que dejarlo en tiempo de ejecución.

30. ¿Qué es GC? ¿Por qué hay una GC?

Respuesta: GC significa recolección de basura. El procesamiento de memoria es un lugar donde los programadores son propensos a tener problemas. El olvido o la recolección incorrecta de memoria puede causar inestabilidad o incluso fallas en el programa o sistema. La función GC proporcionada por Java puede monitorear automáticamente si el objeto excede el alcance. Para lograr el propósito de recuperar memoria automáticamente, el lenguaje Java no proporciona un método de operación de visualización para liberar la memoria asignada. Los programadores de Java no necesitan preocuparse por la administración de la memoria, porque el recolector de basura la administrará automáticamente. Para solicitar la recolección de basura, puede llamar a uno de los siguientes métodos: System.gc () o Runtime.getRuntime (). Gc (), pero la JVM puede bloquear las llamadas de recolección de basura mostradas.
La recolección de basura puede prevenir eficazmente las pérdidas de memoria y utilizar la memoria disponible de forma eficaz. El recolector de basura generalmente se ejecuta como un subproceso separado de baja prioridad. En circunstancias impredecibles, los objetos que han muerto o no se han utilizado durante mucho tiempo se borran y reciclan. El programador no puede llamar al recolector de basura en tiempo real. Un objeto o todos los objetos se recogen como basura. En los primeros días de Java, la recolección de basura era uno de los aspectos más destacados de Java, porque la programación del lado del servidor necesitaba prevenir eficazmente las pérdidas de memoria. Sin embargo, el tiempo ha pasado y el mecanismo de recolección de basura de Java se ha convertido en algo criticado. Los usuarios de terminales móviles inteligentes generalmente sienten que el sistema iOS tiene una mejor experiencia de usuario que el sistema Android. Una de las razones más profundas radica en la imprevisibilidad de la recolección de basura en el sistema Android.

Suplemento: Hay muchos mecanismos de recolección de basura, que incluyen: recolección de basura de copia generacional, recolección de basura marcada, recolección de basura incremental, etc. Un proceso estándar de Java tiene una pila y un montón. La pila contiene variables locales primitivas y el montón contiene los objetos que se crearán. El algoritmo básico de la plataforma Java para la recuperación y reutilización de la memoria del montón se llama marcado y limpieza, pero Java lo ha mejorado, utilizando "recolección de basura generacional". Este método divide la memoria del montón en diferentes áreas con el ciclo de vida del objeto Java. Durante el proceso de recolección de basura, el objeto puede moverse a diferentes áreas:

  • Edén: esta es el área donde los objetos nacieron por primera vez, y para la mayoría de los objetos, esta es la única área donde han existido.
  • Superviviente: los objetos que sobrevivan del Jardín del Edén se moverán aquí.
  • Titular: Este es el hogar de los sobrevivientes que tienen la edad suficiente. El proceso Minor-GC no tocará este lugar. Cuando la colección de la generación joven no puede colocar el objeto en la residencia de verano de toda la vida, se activará una colección completa (Major-GC), y la compresión también puede estar involucrada aquí para dejar suficiente espacio para objetos grandes.

Parámetros de JVM relacionados con la recolección de basura:

  • -Xms / -Xmx: tamaño inicial del montón / tamaño máximo del montón
  • -Xmn: el tamaño de la generación joven en el montón
  • -XX: -DisableExplicitGC - Deja que System.gc () no tenga ningún efecto
  • -XX: + PrintGCDetails - Imprimir detalles de GC
  • -XX: + PrintGCDateStamps: imprime la marca de tiempo de la operación del GC
  • -XX: NewSize / XX: MaxNewSize - Establece el tamaño de la generación joven / tamaño máximo de la generación joven
  • -XX: NewRatio: puede establecer la proporción de la generación anterior a la nueva generación
  • -XX: PrintTenuringDistribution: establece la distribución de edad de los objetos en el paraíso de los supervivientes que se generarán después de cada GC de nueva generación
  • -XX: InitialTenuringThreshold / -XX: MaxTenuringThreshold: establece el valor inicial y el valor máximo del umbral para la generación anterior
  • -XX: TargetSurvivorRatio: establece la tasa de uso objetivo del área de supervivencia

Supongo que te gusta

Origin blog.51cto.com/15061944/2593360
Recomendado
Clasificación