Usando var para declarar variables, hay tantos detalles en java, ¿lo sabes?

Introducción

Java SE 10 introdujo la inferencia de tipos para variables locales. Anteriormente, todas las declaraciones de variables locales tenían que declarar tipos claros a la izquierda. Usando la inferencia de tipos, algunos tipos explícitos se pueden reemplazar con el tipo reservado var de variables locales con valores inicializados Este tipo de var como el tipo de variables locales se infiere del tipo de valor inicializado.

Existe cierta controversia con respecto a esta característica. Algunas personas agradecen su concisión, mientras que a otras les preocupa que prive a los lectores del tipo de información que los lectores valoran, comprometiendo así la legibilidad del código. Ambas opiniones son correctas. Puede hacer que el código sea más legible al eliminar la información redundante, y también puede reducir la legibilidad del código al eliminar información útil. Otro punto es que le preocupa que se abuse de él, lo que lleva a escribir peor código Java. Esto también es cierto, pero también puede conducir a escribir mejor código. Como todas las funciones, su uso requiere juicio. No hay un conjunto de reglas sobre cuándo usar y cuándo no usar.

Las declaraciones de variables locales no existen de forma aislada; el código circundante puede afectar o incluso abrumar el impacto del uso de var. El propósito de este documento es examinar el impacto del código circundante en las declaraciones de var, explicar algunas de las compensaciones y proporcionar pautas para el uso efectivo de var.

Principio de uso

P1. Leer código es más importante que escribir código

La frecuencia de lectura de código es mucho más alta que la de escritura de código. Además, cuando escribimos código, generalmente tenemos que tener una idea general, y lleva tiempo; cuando leemos código, a menudo navegamos en contexto y podemos tener más prisa. El uso y la forma de utilizar una función lingüística específica debe depender de su impacto en los lectores futuros, no de su autor. Los programas más cortos pueden ser preferibles a los más largos, pero abreviar demasiado el programa omitirá información útil para comprender el programa. El problema principal aquí es encontrar el tamaño correcto para el programa con el fin de maximizar la legibilidad del programa.

Aquí prestamos especial atención a la cantidad de palabras en clave requeridas al ingresar o escribir programas. Aunque la concisión puede ser un buen estímulo para el autor, centrarse en ella ignorará el objetivo principal, que es mejorar la legibilidad del programa.

P2. Después de la inferencia del tipo de variable local, el código debería quedar claro

Los lectores deberían poder ver las declaraciones de var y comprender inmediatamente lo que está sucediendo en el código cuando se utilizan declaraciones de variables locales. Idealmente, el código se puede entender fácilmente solo a través de fragmentos de código o contexto. Si leer una declaración var requiere que los lectores miren varias ubicaciones alrededor del código, usar var en este momento puede no ser una buena situación. Además, también indica que puede haber un problema con el código en sí.

P3. La legibilidad del código no debería depender del IDE

El código generalmente se escribe y lee en el IDE, por lo que es fácil confiar en la función de análisis de código del IDE. Para declaraciones de tipo, usando var en cualquier lugar, puede usar el IDE para apuntar a una variable para determinar su tipo, pero ¿por qué no hacer esto?

Hay dos razones. El código a menudo se lee fuera del IDE. El código aparece en muchos lugares donde las instalaciones IDE no están disponibles, como fragmentos en documentos, repositorios de exploración o archivos de parche en Internet. Para comprender la función de estos códigos, debe importar el código al IDE. Esto es contraproducente.

La segunda razón es que incluso al leer el código en el IDE, generalmente se requiere una operación clara para consultar el IDE para obtener más información sobre las variables. Por ejemplo, para consultar el tipo de variable declarado con var, es posible que deba pasar el mouse sobre la variable y esperar a que aparezca un mensaje emergente. Esto solo puede tomar un momento, pero interrumpirá el proceso de lectura.

El código debe presentarse automáticamente, su superficie debe ser comprensible y sin la ayuda de herramientas.

P4. La mecanografía explícita es una compensación

Java siempre ha requerido tipos explícitos en las declaraciones de variables locales. Obviamente, los tipos explícitos pueden ser muy útiles, pero a veces no son muy importantes y otras veces se pueden ignorar. Pedir un tipo claro también puede estropear información útil.

La omisión de tipos explícitos puede reducir esta confusión, pero solo si esta confusión no daña su inteligibilidad. Este tipo no es la única forma de transmitir información a los lectores. Otros métodos, incluidos nombres de variables y expresiones de inicialización, también pueden transmitir información. Al determinar si uno de los canales se puede silenciar, debemos considerar todos los canales disponibles.

Pauta G1: Suele ser una buena práctica elegir un nombre de variable que proporcione información útil, pero es más importante en el contexto de la var. En una declaración var, el nombre de la variable se puede usar para transmitir información sobre el significado y uso de la variable. El uso de var para reemplazar tipos explícitos también mejora los nombres de las variables. P.ej:

//原始写法
List<Customer> x = dbconn.executeQuery(query);

//改进写法
var custList = dbconn.executeQuery(query);

En este caso, el nombre de variable inútil x ha sido reemplazado por un nombre custList que evoca el tipo de variable, que ahora está implícito en la declaración var. Según el resultado lógico del método, se codifica el tipo de variable y se obtiene el nombre de variable custList en forma de "notación húngara". Al igual que la escritura explícita, esto a veces es útil, a veces simplemente complicado. En este ejemplo, el nombre custList significa que se devuelve List. Esto también puede no ser importante. A diferencia de los tipos explícitos, los nombres de variables a veces pueden expresar mejor el rol o la naturaleza de la variable, como "clientes":

//原始写法    
try (Stream<Customer> result = dbconn.executeQuery(query)) {
     return result.map(...)
                  .filter(...)
                  .findAny();
 }
//改进写法
try (var customers = dbconn.executeQuery(query)) {
    return customers.map(...)
                    .filter(...)
                    .findAny();
}

G2 Minimizar el alcance de uso de variables locales Minimizar el alcance de variables locales suele ser un buen hábito. Este enfoque se describe en Effective Java (Third Edition), ítem 57. Si desea utilizar var, será un impulso adicional.

En el siguiente ejemplo, el método add agrega correctamente el elemento especial al último elemento de la colección de listas, por lo que se procesa al final como se esperaba.

var items = new ArrayList<Item>(...);
items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) ...

Ahora suponga que para eliminar elementos duplicados, el programador quiere modificar este código para usar HashSet en lugar de ArrayList:

var items = new HashSet<Item>(...);
items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) ...

Este código ahora tiene un error porque Set no define el orden de iteración. Sin embargo, el programador puede corregir este error inmediatamente porque el uso de la variable items es adyacente a su declaración.

Ahora suponga que este código es parte de un método grande y, en consecuencia, la variable items tiene un amplio rango de uso:

var items = new HashSet<Item>(...);

// ... 100 lines of code ...

items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) ...

El impacto de cambiar ArrayList a HashSet ya no es obvio, porque el código que usa elementos está muy lejos del código que declara elementos. Esto significa que el error mencionado anteriormente parece sobrevivir más tiempo.

Si los elementos se han declarado claramente como Lista, también debe cambiar el inicializador para cambiar el tipo a Establecer. Esto puede hacer que el programador compruebe el resto del método en busca de código afectado por dichos cambios. (Si surge el problema, es posible que no lo compruebe). Si usa var eliminará tales efectos, pero también puede aumentar el riesgo de introducir errores en dicho código.

Este parece ser un argumento en contra del uso de var, pero no lo es. El procedimiento de inicialización con var es muy sencillo. Este problema solo ocurre cuando el uso de la variable es grande. Debería cambiar el código para reducir el alcance de las variables locales y declararlas con var en lugar de simplemente evitar var en estas situaciones.

G3 Cuando el programa de inicialización proporciona suficiente información para el lector, considere usar var variables locales que normalmente se inicializan en el constructor. El nombre del constructor que se crea normalmente duplica el tipo declarado explícitamente a la izquierda. Si el nombre del tipo es muy largo, puede usar var para mejorar la concisión sin perder información:

// 原始写法:
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// 改进写法
var outputStream = new ByteArrayOutputStream();

En el caso de que el inicializador sea una llamada a un método, es razonable utilizar var, como un método de fábrica estático, y su nombre contiene suficiente información de tipo:

//原始写法
BufferedReader reader = Files.newBufferedReader(...);
List<String> stringList = List.of("a", "b", "c");
// 改进写法
var reader = Files.newBufferedReader(...);
var stringList = List.of("a", "b", "c"); 

En estos casos, el nombre del método implica fuertemente su tipo de retorno específico, que luego se usa para inferir el tipo de variable.

G4. Use var variables locales para descomponer expresiones encadenadas o anidadas. Considere usar una colección de cadenas y busque el código de la cadena que aparece con más frecuencia, que puede tener este aspecto:

return strings.stream()
               .collect(groupingBy(s -> s, counting()))
               .entrySet()
               .stream()
               .max(Map.Entry.comparingByValue())
               .map(Map.Entry::getKey);

Este código es correcto, pero puede resultar confuso porque parece una única canalización de flujo. De hecho, es un flujo corto primero, luego el segundo flujo generado por el resultado del primer flujo, y luego el flujo después de que se mapea el resultado opcional del segundo flujo. La forma más legible de expresar este código son dos o tres oraciones; el primer grupo de entidades se coloca en un mapa, luego el segundo grupo se filtra fuera de este mapa y el tercer grupo se extrae del resultado del mapa por clave, como se muestra a continuación:

Map<String, Long> freqMap = strings.stream()
                                   .collect(groupingBy(s -> s, counting()));
Optional<Map.Entry<String, Long>> maxEntryOpt = freqMap.entrySet()
                                                       .stream()
                                                       .max(Map.Entry.comparingByValue());
return maxEntryOpt.map(Map.Entry::getKey);

Pero el escritor puede negarse a hacer esto porque parece demasiado engorroso escribir los tipos de variables intermedias, por el contrario, alteraron el flujo de control. El uso de var nos permite expresar código de manera más natural sin el alto costo de declarar explícitamente los tipos de variables intermedias:

var freqMap = strings.stream()
                     .collect(groupingBy(s -> s, counting()));
var maxEntryOpt = freqMap.entrySet()
                         .stream()
                         .max(Map.Entry.comparingByValue());
return maxEntryOpt.map(Map.Entry::getKey);

Algunas personas pueden preferir una única cadena larga de llamadas en la primera parte del código. Sin embargo, bajo ciertas condiciones, es mejor romper las cadenas de métodos largas. Usar var para estas situaciones es un método viable, y usar la declaración completa de la variable intermedia en el segundo párrafo sería una mala elección. Como en muchas otras situaciones, el uso correcto de var puede implicar tirar cosas (tipos de visualización) y agregar cosas (mejores nombres de variables, mejor estructura de código).

G5. No se preocupe demasiado por el uso de variables locales en "Programación con interfaces"

Un lenguaje común en la programación Java es construir instancias de tipos específicos, pero asignarlas a variables de tipos de interfaz. Esto vincula el código a lo abstracto en lugar de a la implementación concreta, y conserva la flexibilidad para el mantenimiento futuro del código.

//原始写法, list类型为接口List类型
List<String> list = new ArrayList<>()

Si usa var, puede inferir que el tipo de implementación específico de lista es ArrayList en lugar del tipo de interfaz List.

// 推断出list的类型是 ArrayList<String>.
var list = new ArrayList<String>(); 

Para repetir aquí, var solo se puede usar para variables locales. No se puede utilizar para inferir tipos de atributos, tipos de parámetros de métodos y tipos de retorno de métodos. El principio de "programación con interfaces" sigue siendo tan importante en estas situaciones como siempre.

El principal problema es que el código que usa esta variable puede formar una dependencia de la implementación específica. Si el procedimiento de inicialización de la variable se va a cambiar en el futuro, esto puede hacer que cambie su tipo inferido, dando como resultado una excepción o error en el código subsiguiente que usa la variable.

Si, como se sugiere en la Guía G2, el alcance de las variables locales es pequeño, las "vulnerabilidades" que pueden afectar la implementación específica de códigos posteriores son limitadas. Si la variable solo se usa para unas pocas líneas de código, debería ser fácil evitar estos problemas o aliviarlos.

En este caso especial, ArrayList solo contiene algunos métodos que no están en List, como asegurarCapacidad () y trimToSize (). Estos métodos no afectarán a la Lista, por lo que llamarlos no afectará la corrección del programa. Esto reduce aún más el impacto de los tipos inferidos como tipos de implementación concretos en lugar de tipos de interfaz.

G6. Tenga cuidado al usar var con <> y métodos genéricos

Las funciones var y <> le permiten omitir información de tipo específico cuando puede derivar de información existente. ¿Puedes usarlos en la misma declaración de variable?

Considere el siguiente código:

PriorityQueue<Item> itemQueue = new PriorityQueue<Item>();

Este código se puede reescribir usando var o <> sin perder la información de tipo:

// 正确:两个变量都可以声明为PriorityQueue<Item>类型
PriorityQueue<Item> itemQueue = new PriorityQueue<>();
var itemQueue = new PriorityQueue<Item>();   

Es legal usar var y <> al mismo tiempo, pero el tipo inferido cambiará:

// 危险: 推断类型变成了 PriorityQueue<Object>
var itemQueue = new PriorityQueue<>();

A partir de los resultados de la inferencia anteriores, <> puede usar el tipo de destino (generalmente en el lado izquierdo de la declaración) o el constructor como tipo de parámetro en <>. Si no existe ninguno, volverá al tipo aplicable más amplio, generalmente Object. Por lo general, esto no es lo que esperábamos.

Los métodos genéricos ya han proporcionado inferencia de tipo. El uso de métodos genéricos rara vez requiere parámetros de tipo explícitos. Si los parámetros del método real no proporcionan suficiente información de tipo, la inferencia de métodos genéricos dependerá del tipo de destino. No hay un tipo de destino en la declaración var, por lo que pueden ocurrir problemas similares. P.ej:

// 危险: list推断为 List<Object>
var list = List.of();

Usando <> y métodos genéricos, se puede proporcionar otra información de tipo a través de los parámetros reales del constructor o método, lo que permite inferir el tipo esperado, así:

// 正确: itemQueue 推断为 PriorityQueue<String>
Comparator<String> comp = ... ;
var itemQueue = new PriorityQueue<>(comp);

// 正确: infers 推断为 List<BigInteger>
var list = List.of(BigInteger.ZERO);

Si desea usar var con <> o métodos genéricos, debe asegurarse de que el método o los parámetros del constructor proporcionen suficiente información de tipo para que el tipo inferido coincida con el tipo que desea. De lo contrario, evite usar var y <> o métodos genéricos en la misma declaración.

G7. Tenga cuidado al utilizar tipos básicos de var

Los tipos básicos se pueden inicializar mediante declaraciones var. Es poco probable que el uso de var en estas situaciones proporcione muchas ventajas, porque el nombre del tipo suele ser corto. Sin embargo, var a veces es útil, por ejemplo, para alinear los nombres de variables.

Los tipos básicos de booleano, carácter, largo y cadena usan var. La inferencia de estos tipos es precisa porque el significado de var es claro.

// 原始做法
boolean ready = true;
char ch = '\ufffd';
long sum = 0L;
String label = "wombat";

// 改进做法
var ready = true;
var ch    = '\ufffd';
var sum   = 0L;
var label = "wombat";

Cuando el valor inicial es un número, debe tener especial cuidado, especialmente el tipo int. Si hay un tipo de visualización a la izquierda, la derecha convertirá silenciosamente el valor int al tipo correspondiente a la izquierda mediante conversión ascendente o descendente. Si se utiliza var a la izquierda, se deducirá que el valor de la derecha es de tipo int. Esto puede no ser intencional.

// 原始做法
byte flags = 0;
short mask = 0x7fff;
long base = 17;

// 危险: 所有的变量类型都是int
var flags = 0;
var mask = 0x7fff;
var base = 17;  

Si el valor inicial es un flotante, el tipo inferido es en su mayoría inequívoco:

// 原始做法
float f = 1.0f;
double d = 2.0;

// 改进做法
var f = 1.0f;
var d = 2.0;

Tenga en cuenta que el tipo flotante se puede convertir silenciosamente al tipo doble. Usar una variable flotante explícita (como 3.0f) para inicializar una variable doble puede ser un poco lento. Sin embargo, si usa var para inicializar variables dobles con variables flotantes, debe prestar atención a:

// 原始做法
static final float INITIAL = 3.0f;
...
double temp = INITIAL;

// 危险: temp被推断为float类型了
var temp = INITIAL;  

(De hecho, este ejemplo viola las pautas de G3, porque el programa de inicialización no proporciona suficiente información de tipo para que el lector comprenda su tipo inferido).

Ejemplo

Esta sección contiene algunos ejemplos en los que se puede utilizar var para lograr buenos resultados.

El siguiente código representa la eliminación de entidades coincidentes de un mapa en función del número máximo de coincidencias max. El límite de tipo comodín (?) Puede mejorar la flexibilidad del método, pero la longitud será muy larga. Desafortunadamente, aquí también se requiere que el tipo Iterator sea un tipo comodín anidado, lo que hace que su declaración sea más detallada, por lo que la longitud del título del bucle for no puede caber en una línea. También hace que el código sea más difícil de entender.

// 原始做法
void removeMatches(Map<? extends String, ? extends Number> map, int max) {
    for (Iterator<? extends Map.Entry<? extends String, ? extends Number>> iterator =
             map.entrySet().iterator(); iterator.hasNext();) {
        Map.Entry<? extends String, ? extends Number> entry = iterator.next();
        if (max > 0 &amp;&amp; matches(entry)) {
            iterator.remove();
            max--;
        }
    }
}  

El uso de var aquí puede eliminar algunas declaraciones de tipo interferentes para variables locales. En este tipo de bucle, el iterador de tipo explícito, Map.Entry es en gran medida innecesario. El uso de la declaración var puede colocar el título del bucle for en la misma línea. El código también es más fácil de entender.

// 改进做法
void removeMatches(Map<? extends String, ? extends Number> map, int max) {
    for (var iterator = map.entrySet().iterator(); iterator.hasNext();) {
        var entry = iterator.next();
        if (max > 0 &amp;&amp; matches(entry)) {
            iterator.remove();
            max--;
        }
    }
}

Considere usar la instrucción try-with-resources para leer una sola línea de texto del socket. Las clases de red e IO son generalmente clases decorativas. Cuando esté en uso, cada objeto intermedio debe declararse como una variable de recurso para que el recurso se pueda cerrar correctamente al abrir la clase de decoración posterior. El código de escritura convencional requiere que se declaren nombres de clases repetidos alrededor de variables, lo que puede causar mucha confusión:

// 原始做法
try (InputStream is = socket.getInputStream();
     InputStreamReader isr = new InputStreamReader(is, charsetName);
     BufferedReader buf = new BufferedReader(isr)) {
    return buf.readLine();
}  

El uso de declaraciones var reducirá mucha de esta confusión:

// 改进做法
try (var inputStream = socket.getInputStream();
     var reader = new InputStreamReader(inputStream, charsetName);
     var bufReader = new BufferedReader(reader)) {
    return bufReader.readLine();
}

Conclusión

El uso de declaraciones var puede mejorar el código al reducir el desorden, permitiendo que se destaque información más importante. Por otro lado, usar var indiscriminadamente también puede empeorar las cosas. Si se usa correctamente, var puede ayudar a mejorar un buen código, haciéndolo más corto y claro, sin comprometer la comprensión.

Responda a la "información" mediante un mensaje privado para recibir un resumen de las preguntas de la entrevista Java de un fabricante importante + manual de Alibaba Taishan + una guía para aprender y pensar en cada punto de conocimiento + un resumen de los puntos de conocimiento básicos de Java en un documento pdf de 300 páginas.

El contenido de estos materiales son todos los puntos de conocimiento que el entrevistador debe preguntar durante la entrevista. El capítulo incluye muchos puntos de conocimiento, incluidos conocimientos básicos, colecciones de Java, JVM, concurrencia multiproceso, principios de primavera, microservicios, Netty y RPC, Kafka , Diario, patrón de diseño, algoritmo Java, base de datos, Zookeeper, caché distribuida, estructura de datos, etc.

expediente

Supongo que te gusta

Origin blog.csdn.net/weixin_46577306/article/details/107800101
Recomendado
Clasificación