Entrevista 1#conceptos básicos de java

1. Genéricos

1. ¿Qué es un genérico?

Los genéricos son un nuevo azúcar sintáctico agregado en Java5. Esta tecnología puede mencionar errores que ocurren durante la compilación en tiempo de ejecución, lo que mejora en gran medida la eficiencia de la codificación.

De forma predeterminada, los elementos de tipo agregados a la colección se tratan como tipos de objetos. Cuando el programa elimina el objeto de la colección, necesita realizar una conversión de tipo forzada. Esta conversión de tipo forzada no solo hace que el código se hinche, sino que también causa fácilmente ClassCastExeception excepciones. Sin embargo, no hay ningún problema con esta excepción durante la compilación y el compilador pasará. Pero se produce una excepción durante el tiempo de ejecución. Al introducir la sintaxis de los genéricos, el tipo de elementos agregados se restringe durante la compilación y se evita la conversión de tipos.

2. ¿Cuál es la diferencia entre el comodín genérico <?> y la variable de parámetro de tipo Clase <T>{}?
  • El comodín <?> es un argumento de tipo en lugar de un parámetro de tipo.

  • Lista <?> es lógicamente la clase principal de Lista y Lista <parámetro de tipo concreto>, y su uso es más flexible que el parámetro de tipo T.

  • El comodín entrante generalmente realiza muchas operaciones que no tienen nada que ver con el tipo específico. Si se trata de operaciones relacionadas con tipos específicos y valores de retorno, aún necesita usar el método genérico T.

  //虽然Object是所有类的基类,但是List<Object>在逻辑上与List<Integer>等并没有继承关系,
  //这个方法只能传入List<Object>类型的数据 
   public static void showOList(List<Object> list){
    
    
        System.out.println(list.size());
    }
    //同理,这个方法只能传入List<Number>类型的数据,并不能传入List<Integer>
    public static void showList(List<Number> list){
    
    
        System.out.println(list.size());
    }
    //使用通配符,List<?>在逻辑上是所有List<Number>,List<Integer>,List<String>……的父类,
    //可以传递所有List类型的数据,但是不能在List<?>类型的数据进行于具体类型相关的操作,如add
    public static void showList2(List<?> list){
    
    
        System.out.println("<?>");
        System.out.println(list.size());
    }
    //设置通配符上限,可以传入List<Number及Number的子类>
    public static void showNumList(List<? extends Number> list){
    
    
        System.out.println(list.size());
    }
   //设置通配符下限,List<? super Number>只可以传入List<Number及其父类>
    public static boolean Compare(List<? super Number> list1,List<? super Integer> list2){
    
    
        return list1.size()>list2.size();
    }
3. ¿Entiendes el borrado genérico? ¿Por qué escribir borrado? ¿Efectos secundarios del borrado de tipos?

Borrado genérico:

La información de tipo parametrizada de Java solo existe durante la fase de compilación. Durante el proceso de compilación, después de que el compilador javac verifique correctamente el resultado genérico, borrará la información relevante del genérico, y cuando el objeto entre y salgamétodoAgregue métodos de verificación de tipos y conversión de tipos en el límite, es decir, la información genérica no ingresará al tiempo de ejecución.

Por qué necesita un borrado genérico:

Por compatibilidad con versiones anteriores, los genéricos son una sintaxis que apareció en Java 5. Antes de eso, no había genéricos. El objetivo principal de introducir genéricos en ese momento era resolver el problema de tener que forzar siempre manualmente la transformación al atravesar una colección. Eleve las excepciones que se provocan fácilmente durante el tiempo de ejecución al tiempo de compilación. Para mantener la JVM compatible con versiones anteriores, el último recurso es el borrado de tipos.

Consecuencias del borrado de tipos:

  • Todavía existe el riesgo de que se produzca ClassCastException. Debido a que las variables de tipos genéricos y las variables de tipos primitivos se pueden asignar entre sí y no se informará ningún error de compilación, es fácil causar ClassCastException.
    public static void main(String[] args) {
    
    

        List<Integer> integerList = new ArrayList<>(); //带泛型类型的变量
        integerList.add(1);
        List rawList = integerList; //rawList  原始类型的变量
        List<String> stringList = new ArrayList<>(); //带泛型类型的变量
        stringList = rawList; //编译时只会警告,但通过
        System.out.println(stringList.get(0)); //运行时java.lang.ClassCastException

    }
  • Las firmas de los métodos pueden entrar en conflicto
   // 编译后后T被还原为原始的Object类型。编译后的方法可以看做和下面的方法一样。
   // 注意泛型擦除的疑惑点,这里编译后方法签名信息还是会保留到class文件中的,方法体中的语句与泛型相关的都被擦除。在
   // 编译期间自动添加强制类型转化。
    public boolean test(T t) {
    
    
        return true;
    }

    public boolean test(Object object) {
    
    
        return true;
    }
  • Los tipos de argumentos genéricos no admiten tipos de datos básicos: T en el tipo original será reemplazado por Objeto después del borrado genérico, y Objeto no puede almacenar tipos de datos básicos. Entonces, para resolver este problema, aparecieron clases contenedoras.
  • No puede utilizar instancia de argumentos genéricos (excepto con comodines)

        // instanceof 存在继承实现关系即true。

        List<String> list = new ArrayList<>();

        System.out.println(list instanceof List<?>); //true

        System.out.println(list instanceof List<String>); // 编译报错,擦除后String丢失
  • Métodos estáticos | Los parámetros genéricos no se pueden usar en clases estáticas: los parámetros genéricos en una clase genérica se especifican cuando se crea el objeto, pero los estáticos no necesitan crear objetos, por lo que no se pueden usar genéricos.
  • No se puede crear una instancia genérica: existe borrado, tipo indefinido.
    /**
     * 无法为泛型创建实例(反射可以)
     * */
    public static <E> void test(List<E> eList, Class<E> eClass) {
    
    
        // E e = new E(); // 编译报错,信息擦除。
        try {
    
    
            // 编译通过
            E e = eClass.newInstance(); // Class<E> eClass 泛型作为了方法的参数,作为了元数据存在。可以使用这个信息。
            eList.add(e);
        } catch (InstantiationException | IllegalAccessException e1) {
    
    
            e1.printStackTrace();
        }
    }
  • Sin matriz genérica

referencia

4. Dado que existe el borrado, ¿por qué todavía podemos usar la reflexión para obtener tipos genéricos específicos en tiempo de ejecución?

De hecho, se borra todo excepto la información estructurada; aquí la información estructurada se refiere a la información relacionada con la estructura de la clase, es decir, se conservan los metadatos relacionados con la clase y sus campos y parámetros de tipo de método, que se pueden obtener mediante reflexión.


    public static <E> void test(List<E> eList, Class<E> eClass) {
    
     // 如方法签名相关信息都是元数据

        // 如下:涉及到泛型的代码会被擦除为原始类型
        List<String> list = new ArrayList<>();
        Iterator<String> it = list.iterator();
        while (it.hasNext()) {
    
    
            String s = it.next();
        }
        //如上

        // 上面代码擦除后和如下代码字节码一致

        List list = new ArrayList();
        Iterator it = list.iterator();
        while (it.hasNext()) {
    
    
            String s = (String) it.next();
        }
    }

De hecho, el llamado borrado de tipo de genéricos se refiere a restaurar una referencia genérica específica al tipo de Objeto después de completar la verificación de tipo en el momento de la compilación, agregar conversión de tipo forzada en los lugares necesarios del código y perderla. en tiempo de ejecución.

En tiempo de ejecución, solo puede obtener el tipo genérico que contiene información genérica en el archivo de clase actual, pero no puede obtener dinámicamente el tipo de referencia genérica en tiempo de ejecución.

Después del borrado del tipo, no hay forma de obtener la información del tipo de E dentro del método de prueba en el siguiente código. Este es el efecto real después del borrado. La información genérica que usted dijo que se puede obtener mediante la reflexión debe ser el tipo genérico específico de una clase como variable miembro, valor de retorno del método, etc., por ejemplo:


    public static <E> void test(List<E> eList, Class<E> eClass) {
    
    
        // E e = new E(); // 编译报错,信息擦除。无法确定E的实际类型。
        try {
    
    
            // 反射代码,编译通过。
            E e = eClass.newInstance(); // Class<E> eClass 泛型作为了方法的参数,作为了元数据存在。可以使用这个信息。
            eList.add(e);
        } catch (InstantiationException | IllegalAccessException e1) {
    
    
            e1.printStackTrace();
        }
    }

referencia

5. Escenarios genéricos utilizados en tu trabajo.

(1) Extracción de la clase base mvp

(2) Interfaz de solicitud de red Encapsulación de objetos de bean base de estilo tranquilo

(3) Encapsulación de clases de herramientas generales: cualquier matriz inversa.

6. ¿Entiendes los comodines genéricos y los límites superior e inferior? ¿Cuáles son los principios PECS?

Su propósito es hacer que la interfaz del método sea más flexible y aceptar una gama más amplia de tipos.

<? super E> se utiliza para escritura o comparación flexible, de modo que los objetos se puedan escribir en el contenedor del tipo principal, de modo que el método de comparación del tipo principal se pueda aplicar a los objetos de la subclase.

Los genéricos pueden aceptar la clase E especificada y su tipo de clase principal (solo recuérdelo de acuerdo con la función de super)

<? extiende E> se utiliza para lectura flexible, lo que permite que el método lea objetos contenedores de E o cualquier subtipo de E.

Los genéricos pueden aceptar la clase E y sus tipos de subclase (solo recuérdelo de acuerdo con la función de extensión)

Utilice una frase de "Java efectivo" para profundizar su comprensión:
para obtener la máxima flexibilidad, utilice comodines en los parámetros de entrada que representan a los productores o consumidores. La regla utilizada es: los productores tienen límites superiores y los consumidores tienen límites inferiores.

PECS: productor-extiende, cliente-super

Si el tipo parametrizado representa un productor de T, use <? extends T>;
si representa un consumidor de T, use <? super T>;
si es a la vez un productor y un consumidor, entonces no tiene sentido usar comodines Porque lo que necesitas es el tipo de parámetro exacto.

2. Reflexión

1. ¿Qué es la reflexión?

Una tecnología proporcionada por jdk que utiliza la API de reflexión para analizar dinámicamente los componentes de una clase durante la ejecución del programa.

2. ¿Cuáles son los escenarios de aplicación de la reflexión?

(1) Clases en el archivo de configuración en ejecución: comunes en los marcos, generalmente expresadas como cadenas de nombres completos de clases de configuración en xml o archivos de configuración. En este momento, los objetos de la clase se pueden generar dinámicamente mediante la reflexión.
(2) Procesamiento de anotaciones en tiempo de ejecución
(3) Indicaciones dinámicas del compilador: las herramientas de desarrollo utilizan la reflexión para analizar dinámicamente el tipo y la estructura de los objetos y solicitar dinámicamente las propiedades y métodos de los objetos.
(4) Implementación del proxy dinámico: el proxy dinámico jdk se implementa mediante reflexión. De hecho, los servidores proxy dinámicos como cglib también se pueden implementar utilizando el marco de operación de código de bytes de alto rendimiento ASM.
(5) En JDBC, la reflexión se usa para cargar dinámicamente el controlador de la base de datos
(6) En el servidor web, la reflexión se usa para llamar al método de servicio Sevlet

3. Ventajas y desventajas de la reflexión

Ventajas: la ejecución dinámica y la adquisición dinámica de componentes de clase durante el tiempo de ejecución maximizan la flexibilidad de Java.

Desventajas: Principalmente gastos generales de rendimiento. Al tener un impacto en el rendimiento, las operaciones de reflexión son siempre más lentas que la ejecución directa del código Java.

  • Gastos generales de rendimiento: la reflexión implica la resolución dinámica de tipos, por lo que la JVM no puede optimizar estos códigos. Por lo tanto, la operación de reflexión es ineficiente y se intenta evitar el uso de la reflexión bajo requisitos de alto rendimiento.
  • Restricciones de seguridad: el uso de la reflexión requiere que el programa se ejecute en un entorno sin restricciones de seguridad. De lo contrario, no obtendrá los resultados esperados.
  • Exposición interna: debido a que la reflexión permite que el código realice operaciones que normalmente no están permitidas, el uso de la reflexión puede provocar efectos secundarios no deseados.
4. Principio de reflexión, ¿cómo implementa JVM la reflexión?

(1) Primero, dé una introducción general.

Hay dos tipos principales de clases relacionadas con la reflexión en el paquete reflect de Java: clase y miembro. La clase es fácil de entender como una clase que encapsula toda la información de una determinada clase.
La clase tiene miembros y los miembros de la clase incluyen tres tipos: constructores, métodos y campos. Los miembros se pueden obtener a través de la API de Class.

Member es una interfaz que define principalmente las interfaces getName y getModifiers. Hay tres clases de implementación más comunes: campo, método y constructor.

Estas tres clases de implementación tienen la misma clase principal, AccessibleObject. Al acceder a propiedades privadas, debe establecer setAccessible en verdadero. La desactivación de la verificación de permisos de acceso del sistema proviene del método de esta clase.

Insertar descripción de la imagen aquí

Insertar descripción de la imagen aquí

Insertar descripción de la imagen aquí

(2) Implementación específica

Aquí tomamos el código fuente de Method como ejemplo para explicar:

    public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
    {
    
    
     ...
     ...
     // 检查访问权限
        MethodAccessor ma = methodAccessor;             // read volatile
        if (ma == null) {
    
    
            ma = acquireMethodAccessor();
        }
        return ma.invoke(obj, args);
    }

El método de ejecución de reflexión llamará al método Method#invoke, y el método se delega internamente a la clase MethodAccessor para su procesamiento. MethodAccessor es una interfaz que tiene dos implementaciones específicas existentes: una es implementar la reflexión a través de métodos locales (NativeMethodAccessorImpl), denominada implementación local, y la otra utiliza el modo de delegación (DelegatingMethodAccessorImpl), denominada implementación delegada.
Insertar descripción de la imagen aquí
Entonces, ¿cuándo utilizar la implementación nativa? ¿Cuándo utilizar la implementación delegada? Primero, veamos la creación de instancias de MethodAccessor. Las instancias de MethodAccessor se crean en ReflectionFactory. ReflectionFactory es una clase de fábrica de reflexión que es responsable de crear FieldAccessor correspondiente a Field, MethodAccessor correspondiente a Method y ConstructorAccessor correspondiente a Constructor.

public class ReflectionFactory {
    
    

    private static boolean initted = false;
    // 反射工厂类,负责创建FieldAccessor 、MethodAccessor 、ConstructorAccessor 
    private static final ReflectionFactory soleInstance = new ReflectionFactory();
    private static volatile LangReflectAccess langReflectAccess;
    private static volatile Method hasStaticInitializerMethod;

    // "Inflation" mechanism. Loading bytecodes to implement
    // Method.invoke() and Constructor.newInstance() currently costs
    // 3-4x more than an invocation via native code for the first
    // invocation (though subsequent invocations have been benchmarked
    // to be over 20x faster). Unfortunately this cost increases
    // startup time for certain applications that use reflection
    // intensively (but only once per class) to bootstrap themselves.
    // To avoid this penalty we reuse the existing JVM entry points
    // for the first few invocations of Methods and Constructors and
    // then switch to the bytecode-based implementations.
    //
    // Package-private to be accessible to NativeMethodAccessorImpl
    // and NativeConstructorAccessorImpl
    private static boolean noInflation        = false; // noInflation 默认关闭。关闭时会采用本地实现。
    //[ˈθreʃhəʊld]  阈;
    private static int     inflationThreshold = 15; // 阈值,发射次数大于inflationThreshold 时则noInflation为true
    
    //MethodAccessor 对象创建
    public MethodAccessor newMethodAccessor(Method method) {
    
    
        checkInitted();

        if (Reflection.isCallerSensitive(method)) {
    
    
            Method altMethod = findMethodForReflection(method);
            if (altMethod != null) {
    
    
                method = altMethod;
            }
        }

        // use the root Method that will not cache caller class
        Method root = langReflectAccess.getRoot(method);
        if (root != null) {
    
    
            method = root;
        }

        // 这里需要注意一点,VMAnonymousClass 并不是指匿名内部类
        // 它可以看做是 JVM 里面的一个模板机制
        if (noInflation && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
    
    
            //动态生成字节码技术
            return new MethodAccessorGenerator().
                generateMethod(method.getDeclaringClass(),
                               method.getName(),
                               method.getParameterTypes(),
                               method.getReturnType(),
                               method.getExceptionTypes(),
                               method.getModifiers());
        } else {
    
    
            //本地实现
            NativeMethodAccessorImpl acc = new NativeMethodAccessorImpl(method);
            //委派实现,代理本地实现。
            DelegatingMethodAccessorImpl res = new DelegatingMethodAccessorImpl(acc);
            acc.setParent(res);
            return res;
        }
    }
} 

Se puede concluir que cuando se llama a la reflexión por primera vez, el valor del campo noInflation es obviamente falso por defecto. En este momento se generará una implementación de delegación, y la implementación específica de la implementación de delegación es una implementación local.

De hecho, también podemos usar una castaña para verificar la conclusión anterior:

/**
 * Create by SunnyDay on 2022/04/19 17:14
 */
public class TestReflectTrack {
    
    

    /**
     * 执行方法时直接搞个异常
     * */
    public static void showTrack(int i) {
    
    
        new Exception("#" + i).printStackTrace();
    }

    public static void main(String[] args) throws Exception {
    
    
        Class<?> clazz = Class.forName("TestReflectTrack");
        Method method = clazz.getMethod("showTrack", int.class);
        method.invoke(null, 0);
    }
}

Insertar descripción de la imagen aquí

La implementación local se entiende bien como la implementación de llamar al método nativo. Después de ingresar a la máquina virtual Java, tenemos la dirección específica del método al que apunta la instancia del Método. En este momento, la llamada de reflexión no es más que preparar los parámetros entrantes y luego llamar al método de destino.

¿Por qué necesitamos utilizar la implementación de la delegación como capa intermedia para las llamadas de reflexión? ¿No es posible dejarlo directamente en manos de la implementación local?

De hecho, el mecanismo de llamada reflexiva de Java también configura otra implementación de código de bytes generado dinámicamente (denominado implementación dinámica), que utiliza directamente instrucciones de invocación para llamar al método de destino. La razón para utilizar la implementación de delegación es permitir la implementación local y la implementación dinámica. Cambiar durante la implementación.

Como se menciona en el comentario de ReflectionFactory, la implementación dinámica es 20 veces más rápida que la implementación local. Esto se debe a que la implementación dinámica no necesita cambiar de Java a C++ y luego a Java. Sin embargo, debido a que generar código de bytes requiere mucho tiempo, si se llama solo una vez, por el contrario, la implementación local es de 3 a 4 veces más rápida.

Teniendo en cuenta que muchas llamadas de reflexión solo se ejecutarán una vez, la máquina virtual Java establece un umbral de 15. Cuando el número de llamadas para una determinada llamada de reflexión es inferior a 15, se utiliza la implementación local; cuando llega a 15, el código de bytes comienza a ser dinámico generado. Y cambiar el objeto de delegación de la implementación de la delegación a implementación dinámica. Este proceso se llama inflación.

Ahora que sabemos el Umbral de inflación = 15, veamos cómo se maneja esta lógica de control.

/** Used only for the first few invocations of a Method; afterward,
    switches to bytecode-based implementation */

class NativeMethodAccessorImpl extends MethodAccessorImpl {
    
    
    private final Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;

    NativeMethodAccessorImpl(Method method) {
    
    
        this.method = method;
    }

    public Object invoke(Object obj, Object[] args)
        throws IllegalArgumentException, InvocationTargetException
    {
    
    
        // We can't inflate methods belonging to vm-anonymous classes because
        // that kind of class can't be referred to by name, hence can't be
        // found from the generated bytecode.
        
        //invoke方法没次被调用时计数器+1,当计数器值大于15时执行如下逻辑
        if (++numInvocations > ReflectionFactory.inflationThreshold()
                && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
    
    
            // 生成java版的MethodAccessor 实现类
            MethodAccessorImpl acc = (MethodAccessorImpl)
                new MethodAccessorGenerator().
                    generateMethod(method.getDeclaringClass(),
                                   method.getName(),
                                   method.getParameterTypes(),
                                   method.getReturnType(),
                                   method.getExceptionTypes(),
                                   method.getModifiers());
             //  改变委托实现DelegatingMethodAccessorImpl 的引用为java版。之后就是java动态实现了。                  
            parent.setDelegate(acc);
        }
        return invoke0(method, obj, args);
    }

    void setParent(DelegatingMethodAccessorImpl parent) {
    
    
        this.parent = parent;
    }

    private static native Object invoke0(Method m, Object obj, Object[] args);
}

InflationThreshold = 15 se determina en la clase NativeMethodAccessorImpl. Cada vez que se llama al método NativeMethodAccessorImpl.invoke, se incrementará un contador para ver si se excede el umbral; una vez superado, se llama a MethodAccessorGenerator.generateMethod para generar la versión Java de la clase de implementación MethodAccessor y cambiar el MethodAccessor al que hace referencia DelegatingMethodAccessorImpl a la versión Java. Las llamadas posteriores a través de DelegatingMethodAccessorImpl.invoke son las implementaciones de la versión Java.

Insertar descripción de la imagen aquí
Resumen de factores que afectan el rendimiento:

Factor externo:

Class.forName llamará al método local, lo que requiere muchos pasos en comparación con llamar al método directamente usando el objeto Java. Class.getMethod atravesará los métodos públicos de la clase. Si no hay coincidencia, también atravesará los métodos públicos de la clase principal. Como puede imaginar, ambas operaciones requieren mucho tiempo.

Vale la pena señalar que la operación del método de búsqueda representada por getMethod devolverá una copia del resultado de la búsqueda. Por lo tanto, debemos evitar el uso de los métodos getMethods o getDeclaredMethods que devuelven matrices de métodos en el código del punto de acceso para reducir el consumo innecesario de espacio en el montón.

Factores propios:

  • Method.invoke es un método de parámetro de longitud variable y su último parámetro en el nivel de código de bytes será una matriz de objetos. El compilador de Java generará una matriz de objetos con una longitud igual al número de parámetros entrantes en el punto de llamada del método y almacenará los parámetros entrantes uno por uno en la matriz.
  • Las matrices de objetos no pueden almacenar tipos básicos y el compilador de Java encuadrará automáticamente los tipos de datos básicos pasados.

Por lo tanto, los factores que requieren mucho tiempo de reflexión son los siguientes:

  • Búsqueda de tabla de métodos: recorra todos los métodos de la clase y posiblemente la clase principal.
  • Construcción de matrices de objetos y posibles operaciones automáticas de empaquetado y desempaquetado.
  • Verificación de permisos en tiempo de ejecución: cada llamada de reflexión verifica los permisos del método de destino, y esta verificación también se puede desactivar en el código Java.
  • método en línea

¿Cómo evitar la sobrecarga del rendimiento de la reflexión?

  • Trate de evitar llamadas de reflexión a métodos virtuales: para invokevirtual o invokeinterface, la máquina virtual Java registra el tipo específico de la persona que llama, al que llamamos perfil de tipo. Cuando hay muchos métodos virtuales, la máquina virtual no puede registrar tantas clases al mismo tiempo. al mismo tiempo, por lo que puede causar el reflejo que se está probando. La llamada no está en línea.
  • Desactive la comprobación de permisos en tiempo de ejecución.
  • Puede ser necesario aumentar el caché de la clase contenedora correspondiente al tipo de datos básico.
  • Apague el mecanismo de inflación (se puede configurar a través de parámetros)
  • Mejorar el perfil de JVM en la cantidad de tipos que se pueden registrar para cada llamada (el valor predeterminado 2 se puede ajustar a través de los parámetros de la máquina virtual)

3. Notas

1. ¿Qué es la anotación?

La anotación puede entenderse como una etiqueta de código Java, que proporciona algunos datos para el código marcado.

2. El período de existencia de la anotación

Esto corresponde al valor del parámetro de la metaanotación Retención. Los valores de los parámetros son los tres valores de enumeración en RetentionPolicy, SOURCE, CLASS y RUNTIME. Representa la existencia en la etapa del código fuente, la existencia en el archivo de clase y la existencia en el archivo de clase, respectivamente. Existe una diferencia entre CLASS y RUNTIME: el valor de CLASS significa que jvm descartará la información de anotación al leer el archivo de clase, mientras que el valor de RUNTIME significa que jvm leerá el valor de anotación en el archivo de clase.

3. ¿Qué es la metaanotación?

Las metanotaciones son anotaciones que actúan sobre las anotaciones. Java proporciona cuatro metanotaciones.

  • @Retención: la duración de la anotación. El valor predeterminado es CLASE
  • @Target: el destino de la anotación. El valor predeterminado es trabajar con cualquier código.
  • @Inherited: [ɪnˈherɪtɪd], si se permite que las subclases hereden las anotaciones de la clase principal, el valor predeterminado es falso.
  • @Documented: si la información de la anotación se puede guardar en javadoc
4. Escenarios de procesamiento de anotaciones
  • Procesamiento en tiempo de compilación (principio de implementación del marco ButterKnife, implementación de LayerHelper en proyectos de trabajo)
  • Procesamiento en tiempo de ejecución. (Principio de implementación de ViewUtils en el marco de Uutils)
5. Proceso general de uso del procesador de anotaciones.

(1) ¿Qué es APT?

APT: La abreviatura de AnnotationProcessorTool.El compilador utiliza el procesador de anotaciones para permitirle procesar anotaciones de acuerdo con nuestros deseos.

(2) Principio de funcionamiento de APT

El proceso de compilación del código fuente de Java se divide en tres pasos:

Analizar el archivo fuente en un árbol de sintaxis abstracta -> llamar al procesador de anotaciones registrado -> generar código de bytes

Si se genera un nuevo archivo fuente durante el segundo paso de llamar al procesador de anotaciones, el compilador repetirá el primer y segundo paso para analizar y procesar el archivo fuente recién generado. Por lo tanto, se puede entender que el compilador utiliza el procesador de anotaciones, lo que le permite procesar las anotaciones según nuestros deseos.

(3) Usos comunes de los procesadores de anotaciones

  • La primera es definir reglas de compilación y verificar los archivos fuente compilados (como @Override que viene con Java)

  • El segundo es modificar el código fuente existente (este método rara vez se usa e involucra la API interna del compilador de Java, lo que puede causar problemas de compatibilidad)

  • El tercero es generar nuevo código fuente (común, actualmente el método más utilizado, como Butterknife, EventBus y otros marcos).

(4) Proceso de uso general del procesador de anotaciones

Puede personalizar la lógica de procesamiento de anotaciones heredando AbstractProcress, pero aún necesita registrar el procesador de anotaciones con el compilador. Esto es algo muy problemático. Debe registrarlo manualmente en el directorio META-INFO, generalmente confiando en el AutoService de Google. biblioteca resolver.

public interface Processor {
    
    

  //一般做一些初始化工作可通过ProcessingEnvironment#getXXX来获取相应的对象如:
  //filer:用于给注解处理器创建文件,可把文件保存到本地。生成文件时经常会使用。
  //Messager:打印编译处理期间的信息。可在build->outPut 窗口查看。
  void init(ProcessingEnvironment processingEnv);
  
  // 获取注解处理器所支持的注解类型,一般固定写法,生成、返回一个set即可。
  Set<String> getSupportedAnnotationTypes();
  // 注解处理器支持的java版本,一般与编译器保持一致即可,固定写法。
  SourceVersion getSupportedSourceVersion();
  /**
    注解处理器的核心处理方法。
    annotations:注解处理器能够处理的注解类型。同上getSupportedAnnotationTypes。
    roundEnv:封装了当前轮抽象语法树的注解信息。一般通过如下方式处理:
    
 Set<? extends Element> elementSet = roundEnvironment.getElementsAnnotatedWith(BindView.class);//BindView 为自定义的注解
  Element是一个接口,代表元素,他的实现类有很多如
  TypeElement:表示一个类或接口程序元素
  VariableElement:表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数
  这样通过相应的api就可以获取注解信息进行处理了。

  */
  boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv);
  
  ...
}

4. Anormalidad

1. Habla sobre excepciones en Java.

Throwable es la clase raíz de todas las excepciones en el lenguaje Java. Throwable deriva dos subclases directas, Error y Excepción.

El error representa un problema grave que la aplicación por sí misma no puede superar ni recuperarse. Cuando se activa un error, el hilo o incluso la máquina virtual finalizarán. Generalmente, errores relacionados con máquinas virtuales como fallas del sistema, memoria insuficiente, desbordamiento de pila, etc.

Las excepciones representan problemas que el programa puede superar y recuperar. Las excepciones se pueden dividir en excepciones en tiempo de compilación y excepciones en tiempo de ejecución según el tiempo de procesamiento.

Las excepciones en tiempo de compilación son excepciones que se pueden reparar. Durante la compilación del código, el programa Java debe manejar explícitamente las excepciones en tiempo de compilación; de lo contrario, no se puede compilar. Las excepciones de tiempo de ejecución suelen ser problemas causados ​​por una mala consideración por parte de los desarrolladores de software. Los usuarios de software no pueden superar y recuperarse de este problema. Sin embargo, el sistema de software puede continuar ejecutándose bajo tales problemas. En casos graves, el sistema de software morirá.

2. ¿Cómo maneja jvm las excepciones?

Cuando el archivo de clase se compila en código de bytes, cada método va acompañado de una tabla de excepciones. Cada entrada en la tabla de excepciones representa un controlador de excepciones.

El procesador consta de un puntero de origen, un puntero de destino, un puntero de destino y el tipo de excepción capturada. El valor de estos punteros es el índice de código de bytes utilizado para localizar el código de bytes.

  • desde y para representar el rango de monitoreo del controlador de excepciones, es decir, el rango monitoreado por el bloque de código de prueba.
  • El objetivo representa la posición inicial del controlador de excepciones, es decir, la posición inicial de la captura.
  • El tipo de excepción es xxxException.

Cuando un programa desencadena una excepción, la máquina virtual Java genera una instancia de excepción para ser lanzada y luego recorre todas las entradas en la tabla de excepciones de arriba a abajo. Cuando el valor del índice del código de bytes que desencadena la excepción está dentro del rango de monitoreo de una entrada de la tabla de excepciones, la máquina virtual Java determina si la excepción que se lanzará coincide con la excepción que la entrada desea detectar. Si hay una coincidencia, la máquina virtual Java transfiere el flujo de control al código de bytes al que apunta el puntero de destino de la entrada.

Si se recorren todas las entradas de la tabla de excepciones y la máquina virtual Java aún no coincide con un controlador de excepciones, aparecerá el marco de pila de Java correspondiente al método actual y repetirá las operaciones anteriores en la persona que llama. En el peor de los casos, la máquina virtual Java necesita atravesar la tabla de excepciones de todos los métodos en la pila Java del subproceso actual. Finalmente lanza la excepción.

La compilación del bloque de código finalmente es más complicada: la versión actual del compilador de Java copia el contenido del bloque de código finalmente y lo coloca en las salidas de todas las rutas de ejecución normales y rutas de ejecución anormales del bloque de código try-catch. Para garantizar que finalmente se debe ejecutar.

3. ¿Qué parte de try-catch-finalmente se puede omitir?

De hecho, try solo es adecuado para manejar excepciones en tiempo de ejecución, y try+catch es adecuado para manejar excepciones en tiempo de ejecución + excepciones en tiempo de compilación.

En otras palabras, si solo usa try para manejar excepciones en tiempo de compilación sin usar catch, la compilación no pasará, porque el compilador estipula rígidamente que si elige capturar excepciones en tiempo de compilación, debe usar catch para declararlas explícitamente para más procesamiento. No existe tal disposición para excepciones de tiempo de ejecución en tiempo de compilación, por lo que se puede omitir catch y el compilador catch lo encuentra comprensible.

En teoría, el compilador mirará cualquier código con disgusto y pensará que puede tener problemas potenciales, por lo que incluso si agrega try a todo el código, el código simplemente agregará una capa adicional sobre la operación normal durante el tiempo de ejecución. Pero una vez que agrega try a un fragmento de código, le promete explícitamente al compilador que detectará las excepciones que pueda generar este fragmento de código en lugar de lanzarlas hacia arriba. Si se trata de una excepción en tiempo de compilación, el compilador requiere que se capture con catch para su posterior procesamiento; si es una excepción en tiempo de ejecución,Capture, luego descarte y finalmente limpieo agregue captura para su posterior procesamiento.

     try{
    
    
       // 运行时异常|编译时异常
     }catch(XXXException e){
    
    
      //1、try中为运行时异常时,这里catch 块可无。但是finally必须有。
      //2、try中为编译时异常时,cacth必须有,finally可有可无。
     }finally{
    
    
     // finally 块可有可无,做资源处理。
     }

       //栗子
        try{
    
    
           // 运行时异常
           int a = 1/0;
        }finally {
    
    
         // 必须有
        }
4. En try-catch-finally, si se produce un retorno en catch, ¿se seguirá ejecutando finalmente?

será ejecutado.

El mecanismo de excepción tiene tal principio: si se encuentra un retorno o una excepción en la captura, lo que puede hacer que la función finalice, finalmente debe ejecutar primero el código en el bloque de código finalmente y luego regresar al punto de lanzamiento o retorno en el atrapar. (Excepto por salir activamente de la máquina virtual, como System.exit() en catch, finalmente no se ejecutará)

Sin embargo, existen excepciones al mecanismo de ejecución anterior. Generalmente, puede utilizar el procesamiento de azúcar de sintaxis try-catch-resource de Java7. Sin embargo, la intención original del diseño try-catch-resource es optimizar el código inflado del cierre del recurso try-catch-finally. Al mismo tiempo, se introducen excepciones suprimidas para usar automáticamente excepciones suprimidas a nivel de código de bytes. Esta nueva característica permite a los desarrolladores adjuntar una excepción a otra. Por lo tanto, la excepción lanzada puede ir acompañada de información de excepción múltiple.

5. ¿Cuál es la diferencia entre NoClassDefFoundError y ClassNotFoundException?

NoClassDefFoundError es una excepción de tipo Error, causada por la JVM. No debe intentar detectar esta excepción. El motivo de esta excepción es que la JVM o ClassLoader no puede encontrar la definición de una determinada clase en la memoria al intentar cargarla. Esta acción ocurre durante el tiempo de ejecución, es decir, la clase existe en el momento de la compilación, pero no se puede encontrar en el tiempo de ejecución. Puede ser que haya sido eliminado después de una mutación y otras razones.

ClassNotFoundException es una excepción marcada y debe detectarse y manejarse explícitamente mediante try-catch o declararse mediante la palabra clave throws en la firma del método. Cuando se utiliza Class.forName, ClassLoader.loadClass o ClassLoader.findSystemClass para cargar dinámicamente una clase en la memoria y la clase no se encuentra a través del parámetro de ruta de clases pasado, se generará esta excepción; otra posible razón para generar esta excepción. cargado en la memoria por un cargador de clases y otro cargador intenta cargarlo.

5. Diferencias entre clases abstractas de interfaz

1. La diferencia entre miembros

Clase abstracta
1. Método de construcción: existe un método de construcción para la creación de instancias de subclases.
2. Variables miembro: Pueden ser variables o constantes.
3. Métodos integrantes: Pueden ser abstractos o no abstractos.

Interfaz
1. Constructor: sin constructor
2. Variables miembro: solo pueden ser constantes. El modificador predeterminado es public static final
3. Métodos miembros: jdk1.7 solo puede ser abstracto. El modificador predeterminado es resumen público. jdk1.8 puede escribir métodos específicos comenzando con predeterminado y estático.

2. Diferencias en las relaciones

Clases y clases:
1. Relación de herencia, sólo herencia única. Son posibles múltiples niveles de herencia.

Clases e interfaces:
1. Relación de implementación: Se puede implementar de forma única o múltiple.
2. Una clase también puede implementar múltiples interfaces mientras hereda una clase.

Interfaces e interfaces:
1. Relación de herencia: es posible herencia única o herencia múltiple.

3. Diferentes conceptos

1. Lo que se define en clases abstractas son contenidos comunes en un sistema de herencia.
2. Una interfaz es una colección de funciones, una función adicional de un sistema y una regla expuesta.

4. Elección de interfaces y clases abstractas.

Los conceptos de interfaces y clases abstractas son diferentes. La interfaz es una abstracción de acciones, que indica lo que el objeto puede hacer, y la clase abstracta es una abstracción de la fuente, que indica qué es el objeto. Cuando te concentras en la esencia de una cosa, usa clases abstractas; cuando te concentras en una operación, usa interfaces.

castaña:

Hombres, mujeres, estas dos clases, su clase abstracta son las personas. Explique que todos son seres humanos. Las personas pueden comer y los perros también pueden comer. Puede definir "comer" como una interfaz. Por lo tanto, en los lenguajes de alto nivel, una clase solo puede heredar una clase (al igual que una persona no puede ser humana y no humana al mismo tiempo), pero puede implementar múltiples interfaces (interfaz para comer, interfaz para caminar).

La funcionalidad de las clases abstractas supera con creces la de las interfaces, pero el costo de definir clases abstractas es alto. Porque en lenguajes de alto nivel (y en términos de diseño real), cada clase solo puede heredar una clase. En esta clase, debes heredar o escribir todas las características comunes de todas sus subclases. Aunque la interfaz tendrá una función mucho más débil, es solo una descripción de una acción. Y puedes implementar múltiples interfaces en una clase al mismo tiempo. La dificultad se reducirá durante la fase de diseño.

Supongo que te gusta

Origin blog.csdn.net/qq_38350635/article/details/124252614
Recomendado
Clasificación