Serie de preguntas de la entrevista Parte 3: La historia interna del juicio de signo igual de números enteros, ¿es posible que no lo sepas?

"Java Interview Questions Series": exploración en profundidad del contenido clásico de las preguntas de la entrevista, análisis del código fuente, resumen de principios, formación de una serie de artículos sobre la cuenta oficial, entrevista o no puede mejorar las habilidades. Bienvenidos a todos para que sigan prestando atención a [Program New Vision]. Este es el tercero de la serie.

En el proceso de la entrevista, hay un sinfín de preguntas sobre la comparación de enteros "==", pero no importa cómo cambie, siempre que comprenda los principios subyacentes, puede obtener la respuesta de inmediato y no tiene que memorizar las preguntas del examen de memoria.

Existe un requisito obligatorio en el "Manual de desarrollo de Java de Alibaba":

"Todas las comparaciones de valores entre los objetos de embalaje de plástico se comparan utilizando el método equals. Explicación: Para la asignación de Integer var =? En el rango de -128 a 127, el objeto Integer se genera en IntegerCache.cache y reutilizará el objeto existente. El valor entero de este intervalo se puede juzgar directamente con ==, pero todos los datos fuera de este intervalo se generarán en el montón y los objetos existentes no se reutilizarán. Este es un gran pozo, y se recomienda utilizar el método de igualdad para juzgar ".

De hecho, si memoriza el párrafo anterior, básicamente puede responder correctamente el 50% de las preguntas de la entrevista (sobre la probabilidad de adivinar). Pero si desea comprender los principios profundos y el 50% restante de las preguntas, continuemos mirando hacia abajo.

Preguntas de entrevista

Primero mire una pregunta común de la entrevista, compare las conclusiones anteriores y vea cuántos elementos se pueden responder correctamente. ¿Cuántos elementos se imprimen como verdaderos en el siguiente código?

@Test
public void test2() {
	Integer i1 = 64;
	int i2 = 64;

	Integer i3 = Integer.valueOf(64);
	Integer i4 = new Integer(64);

	Integer i5 = 256;
	Integer i6 = Integer.valueOf(256);

	System.out.println("A:" + (i1 == i2));
	System.out.println("B:" + (i1 == i3));
	System.out.println("C:" + (i3 == i4));
	System.out.println("D:" + (i2 == i4));
	System.out.println("E:" + (i3.equals(i4)));
	System.out.println("F:" + (i5 == i6));
}

Ejecute el programa anterior, el resultado de la impresión es:

A:true
B:true
C:false
D:true
E:true
F:false

Solo los elementos C y F se imprimen como falsos. ¿Se está preguntando por qué i1 es igual a i2, i1 es igual a i3, i2 es igual a i4, todos son verdaderos, entonces según la transitividad del signo igual, i3 debería ser igual a i4?

¿Por qué i1 e i3 son iguales, pero i5 e i6 no son iguales?

Mantén la duda primero. A continuación, analizamos la estructura de almacenamiento de int y Integer en JVM. Domine la estructura de almacenamiento subyacente y descubrirá que no importa cómo cambie el tema, nunca cambiará.

Almacenamiento variable en JVM

Antes de aclarar completamente el problema anterior, primero comprendamos el almacenamiento de variables de tipo básico y variables de tipo de referencia en la JVM.

Por lo general, las variables se dividen en variables locales y variables globales (miembros). Las variables locales son variables declaradas en métodos; las variables globales son variables miembro declaradas en clases.

Las variables y los valores de los tipos básicos están juntos cuando se asignan, y todos están en el área de método o en la memoria de pila o en la memoria de pila. La variable y el valor de un tipo de referencia no están necesariamente juntos.

Las variables locales se almacenan en la pila de métodos

Cuando se llama al método, la máquina virtual Java crea un marco de pila de forma sincrónica y las variables locales se almacenan en él. Cuando el método termina, la máquina virtual libera la pila de métodos y las variables declaradas terminan con la destrucción del marco de la pila. Por tanto, las variables locales solo pueden ser válidas en métodos.

En este proceso, el almacenamiento de tipos básicos y tipos de referencia son diferentes:

(1) Tipos básicos: las variables y los valores correspondientes se almacenan en la pila de la máquina virtual JAVA;

(2) Tipo de referencia: la variable se almacena en la pila y es una dirección de memoria, y el valor de la dirección apunta a un objeto en el montón.

imagen

La pila pertenece al espacio privado del subproceso, y el ciclo de vida y el alcance de las variables locales son generalmente muy cortos. Para mejorar la eficiencia de gc, no es necesario ponerlo en el montón.

Las variables globales se almacenan en el montón

Las variables globales se almacenan en el montón y no se destruirán cuando finalice el método. Las variables declaradas en la clase también se dividen en tipos básicos y tipos de referencia.

(1) Tipos básicos: los nombres y valores de las variables se almacenan en la memoria del montón.

(2) Tipo de referencia: la variable es una dirección de referencia, que apunta al objeto referenciado. En este punto, las variables y los objetos están en el montón.

Para un ejemplo simple, el siguiente código:

public class Person {
	int age = 10;
	String name = "Tom";
}

La estructura de almacenamiento correspondiente de edad y nombre es la siguiente:
imagen

Combinando la teoría anterior, usamos un fragmento de código para analizar la ubicación de almacenamiento de varios tipos.

public class DemoTest {

 int y; // 变量和值均在堆上
 
 public static void main(String[] args) {

     int x = 1; // 变量和值分配在栈上
     
     String name = new String("cat"); // 数据在堆上,name变量的指针在栈上
     
     String address = "北京"; // 数据在常量池,属于堆空间,指针在栈上
     
     Integer price = 4; // 包装类型为引用类型,编译时会自动装拆箱,数据在堆上,指针在栈
 }
}

Tipos básicos de almacenamiento en pila

A través de los ejemplos anteriores, básicamente hemos entendido la asignación de memoria de diferentes tipos de valores. A continuación, nos centramos en las variables locales.

Primero veamos el modo de procesamiento para el tipo int en el mismo marco de pila.

int a = 3;
int b = 3;

Tanto a como b en el código anterior son variables locales. Suponiendo que el compilador procesa int a = 3 primero, creará una variable de referencia de a en la pila, y luego averiguará si el valor de 3 existe en la pila, si no, almacena 3 en y luego apuntará a 3.

Luego trate con int b = 3, después de crear la variable de referencia de b, también se realiza la búsqueda. Como ya hay un valor de 3 en la pila, b apunta directamente a 3.

En este momento, ayb apuntan al valor 3 al mismo tiempo, que es naturalmente igual.

En cuanto a la comparación de bajo nivel entre tipos básicos y tipos de referencia, se puede ampliar un poco: Para el símbolo de operación "==", la JVM generará diferentes instrucciones en tiempo de compilación según los tipos de operandos que se comparen en ambos lados:

(1) Para operandos enteros booleanos, byte, cortos, int y largos, se genera una instrucción if_icmpne. Esta instrucción se usa para comparar si los valores enteros son iguales.

(2) Si el operando es un objeto, el compilador generará una instrucción if_acmpne, que cambia i (int) a una (referencia de objeto) en comparación con if_icmpne.

Volver al tema

Después de estudiar el conocimiento teórico subyacente anterior, básicamente podemos sacar las siguientes conclusiones: (1) Para comparar dos tipos int, use el signo doble igual directamente; (2) Al comparar el objeto Integer de la clase de paquete int, use equals para comparar OKAY.

Pero los resultados anteriores solo pueden decir que el proyecto E es correcto. El elemento de comparación también incluye las operaciones de empaquetado y desempaquetado de modelado y la caché de Integer. Analicemos uno por uno a continuación.

Comparación de diferentes formas de creación

Primero mire la inicialización de Integer. De acuerdo con la implementación interna de Integer, hay tres tipos de creación de Integer, a saber:

Integer a = new Integer(1); //创建新的类

Integer b = Integer.valueOf(2);  

Integer c = 3; //自动包装,会调用valueOf方法

La capa inferior de asignación directa llamará al método valueOf para operar, por lo que el efecto de estas dos operaciones es el mismo.

Debido a que los dos objetos creados por new y valueOf son completamente dos objetos, para el elemento C en la pregunta, comparar directamente las referencias de los dos objetos definitivamente no es igual, por lo que el resultado es falso. Pero, ¿por qué es cierto el punto B? Hablaremos de ello más tarde.

Unboxing en comparación

En la pregunta, encontramos que tanto A como D son verdaderos, y su formato de comparación es la comparación entre el tipo básico y el tipo de empaque.

Para este tipo de comparación, el tipo de empaque se desempacará automáticamente y se convertirá en el tipo básico (int). Evidentemente, los resultados son iguales.

Caché de enteros

¿Por qué i1 e i3 son iguales, pero i5 e i6 no son iguales? Corresponden a los ítems B y G de la pregunta. Esto implica el mecanismo de almacenamiento en caché de Integer.

Como ya sabemos anteriormente, la asignación directa de Integer y valueOf son equivalentes, primero echemos un vistazo a valueOf y los métodos relacionados.

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

El método valueOf determina si el número es mayor que bajo (-128) y menor que alto (127). Si se cumple la condición, el número correspondiente se devuelve directamente desde IntegerCache.

IntegerCache se utiliza para almacenar algunos números de uso común para evitar la creación repetida. Se inicializa mediante código estático cuando la clase Integer se carga en la memoria.

Por tanto, siempre que el objeto se cree mediante la asignación directa de valueOf o Integer, y su valor sea menor que 127 y mayor que -128, es cierto si se compara con == o igual.

El código fuente y los principios anteriores también explican las razones explicadas en el Manual de desarrollo de Java de Ali.

¿Por qué los iguales pueden evitar el problema?

Para los números que no cumplen con el rango de -128 a 127, sin importar cómo se creen, se creará un nuevo objeto y solo se puede comparar por iguales. A continuación, veremos el método de iguales.

public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}

La implementación de equals es relativamente simple, primero compare si los tipos son consistentes, si son inconsistentes, devuelva falso directamente; de ​​lo contrario, compare los dos valores y devuelva verdadero si son iguales.

resumen

Los puntos centrales de la comparación de Integer incluyen los siguientes tres puntos: la estructura de almacenamiento del objeto referenciado, el mecanismo de almacenamiento en caché de Integer y el boxeo y unboxing automático.

Entero en la operación ==, resumir:

(1) Si uno de los dos extremos de == es un tipo básico (int), se producirá una operación de desempaquetado automático y se comparará el valor.

(2) Si ambos extremos de == son del tipo de empaquetado (Integer), no se desempacará automáticamente. Primero, enfrentará el problema de la caché. Incluso los datos en la caché enfrentarán el problema del método de creación nuevamente, por lo que se recomienda encarecidamente usar equals Métodos para comparar.

Si cree que el artículo está bien escrito, simplemente preste atención. En el próximo artículo, hablaremos sobre la reescritura de la lógica subyacente de los métodos equals y hashcode.

Enlace original: " Serie de preguntas de la entrevista Parte 3: La historia interna del juicio de signo igual de números enteros, ¿tal vez no lo sepa?


Nueva visión del procedimiento

La cuenta pública " Nueva Visión del Programa ", una plataforma que le permite mejorar simultáneamente su poder blando y su tecnología dura, proporcionando cantidades masivas de datos.

Cuenta oficial de WeChat: nueva visión del programa

Supongo que te gusta

Origin blog.csdn.net/wo541075754/article/details/108240367
Recomendado
Clasificación