Skip to main content

Guía rápida de inferencia de tipos locales para Java 10 y versiones posteriores

Escrito por

Stuart Marks

26 de abril de 2018

0 minutos de lectura
Guía rápida de Snyk que explica la inferencia de tipos locales en Java, con tres principios de codificación y siete pautas ilustradas con ejemplos de código.

Te damos la bienvenida a la primera entrega de una nueva serie de guías rápidas que publicaremos en el blog de Snyk. Compartiremos contenido que podrás imprimir y colocar en un lugar visible para ayudarte a ser mejor desarrollador. En esta primera entrega, justo después del lanzamiento de Java 10, nos centraremos en la tan comentada inferencia de tipos para variables locales. Puedes descargar la versión en PDF haciendo clic en la imagen de arriba o en este enlace. La premisa principal de la función de inferencia de tipos locales es bastante sencilla: reemplazar el tipo explícito de la declaración por el nuevo nombre de tipo reservado «var» para que el tipo se infiera. Así, podríamos reemplazar:

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

por:

var outputStream = new ByteArrayOutputStream();

Y el tipo de outputStream se inferirá como ByteArrayOutputStream. Un momento, ¿estamos diciendo que Java ahora permite el tipado dinámico? ¡En absoluto! TODA inferencia de tipos ocurre en tiempo de compilación, y el compilador incorpora los tipos explícitos en el bytecode. En tiempo de ejecución, Java sigue siendo tan estático como siempre. Como su uso es muy sencillo, esta guía rápida se enfocará en el aspecto más importante de la inferencia de tipos locales: su uso práctico. Te orientará sobre cuándo usar tipado explícito y cuándo considerar la inferencia de tipos.

Desde que quise escribir esta guía rápida, Stuart Marks, ingeniero de JDK en Oracle, escribió el artículo perfecto, con principios de programación y recomendaciones sobre el uso de la inferencia de tipos. Así que, cuando quise crear una guía rápida, acudí directamente a Stuart para ver si podíamos incluir sus ideas y resumirlas en una guía para que los desarrolladores la tuvieran a mano y la usaran a diario. Te recomiendo encarecidamente leer el artículo completo de Stuart. ¡De verdad vale la pena!

Principios

1. Leer código > Escribir código.

Ya sea que te tome 10 minutos o 10 días escribir una línea de código, casi con toda seguridad la leerás durante muchos años. El código solo será fácil de mantener y comprender en el futuro si es claro, conciso y, sobre todo, contiene toda la información necesaria para entender su propósito. El objetivo es maximizar la facilidad de comprensión.

2. El código debe ser claro a partir del análisis local.

Incluye en el código toda la información posible para evitar que quien lo lea tenga que revisar distintas partes de la base de código para entender qué está pasando. Puedes lograrlo, por ejemplo, con los nombres de métodos o variables.

3. La legibilidad del código no debería depender de los IDE.

Los IDE pueden ser geniales. ¡Me refiero a realmente geniales! Pueden hacer que un desarrollador sea más productivo o más preciso al programar. El código debe poder leerse y entenderse sin depender de un IDE. A menudo se lee código fuera de un IDE. Además, los IDE pueden diferir en la cantidad de información que muestran. El código debe explicarse por sí mismo. Debe entenderse a simple vista, sin ayuda de herramientas.

La decisión es tuya.

Elegir entre asignar un tipo explícito a una variable o dejar que el compilador de Java lo deduzca es un equilibrio. Por un lado, quieres reducir el desorden, el código repetitivo y la formalidad. Por el otro, no quieres perjudicar la comprensión del código. La declaración del tipo no es la única manera de transmitir información a quien lee el código. También cuentan el nombre de la variable y la expresión de inicialización. Al decidir si está bien omitir el tipado explícito, debemos tener en cuenta todos los recursos disponibles para cada variable.

Recomendaciones

1. Elige nombres de variables que aporten información útil.

Esta es una buena práctica en general, pero es mucho más importante al usar var. En una declaración con var, el nombre de la variable puede transmitir información sobre su significado y uso. Reemplazar un tipo explícito por var suele ser una buena oportunidad para mejorar el nombre de la variable. A veces puede ser útil incluir el tipo de la variable en su nombre. Por ejemplo:

List<Customer> x = dbconn.executeQuery(query);

✅ var custList = dbconn.executeQuery(query);

2. Reduce al mínimo el alcance de las variables locales.

La recomendación de limitar el alcance de las variables locales se describe en Effective Java (3.ª edición), punto 57. Es aún más importante si se usa var. El problema surge cuando la variable tiene un alcance amplio, es decir, cuando hay muchas líneas de código entre su declaración y su uso. A medida que se mantiene el código, los cambios de tipos y otros ajustes pueden generar comportamientos distintos. Por ejemplo, pasar de una List a un Set podría parecer correcto, pero ¿tu código depende del orden más adelante, dentro del mismo alcance? Aunque los tipos siempre se establecen de forma estática, las diferencias sutiles entre implementaciones que usan la misma interfaz pueden causar problemas. En lugar de simplemente evitar var en estos casos, conviene modificar el código para reducir el alcance de las variables locales y solo entonces declararlas con var. Considera el siguiente código:

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

Ahora este código tiene un error, porque los conjuntos no tienen un orden de iteración definido. Sin embargo, es probable que el programador corrija el error enseguida, ya que los usos de la variable items están junto a su declaración. Ahora supongamos que este código forma parte de un método extenso y que, por consiguiente, la variable items tiene un alcance amplio:

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

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

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

Ahora es mucho más difícil detectar este error, porque la línea que intenta agregar un elemento al final del conjunto no está lo suficientemente cerca de la declaración del tipo como para que el problema resulte evidente.

3. Considera usar var cuando la inicialización proporcione suficiente información.

Las variables locales suelen inicializarse con constructores. A menudo, el nombre de la clase que se instancia se repite como tipo explícito a la izquierda. Si el nombre del tipo es largo, usar var hace el código más conciso sin perder información:

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

✅ var outputStream = new ByteArrayOutputStream();

También es razonable usar var cuando la inicialización es una llamada a un método, como Files.newBufferedReader(…) o List stringList = List.of("a", "b", "c").

4. Usa var para dividir expresiones encadenadas o anidadas en variables locales.

Considera el código que recibe una colección de cadenas y busca la que aparece con mayor frecuencia. Podría verse así:

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

Este código es correcto, pero resulta más legible si se distribuye en varias instrucciones. El problema de dividirlo en instrucciones, como se muestra aquí, es el siguiente.

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 probablemente el autor evitó hacerlo porque el tipado explícito se ve extremadamente desordenado y distrae de lo importante del código. Con var podemos expresar el código de forma más natural sin pagar el alto costo de declarar explícitamente los tipos de las 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);

Es válido preferir el primer fragmento, con su larga cadena de llamadas a métodos. Sin embargo, en algunos casos conviene dividir las cadenas largas de métodos.

5. No te preocupes demasiado por «programar para la interfaz» con variables locales.

Un modismo común en la programación con Java es crear una instancia de un tipo concreto y asignarla a una variable de tipo interfaz. Por ejemplo:

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

Sin embargo, si se usa var, se infiere el tipo concreto en lugar de la interfaz:

// Inferred type of list is ArrayList<String>.
✅ var list = new ArrayList<String>();

El código que usa la variable list ahora puede depender de la implementación concreta. Si en el futuro cambia la inicialización de la variable, también podría cambiar el tipo inferido y provocar errores o fallos en el código posterior que la utiliza.

Esto representa un problema menor si se sigue la recomendación 2: cuando el alcance de la variable local es pequeño, se limita el riesgo de que la implementación concreta se «filtre» y afecte al código posterior.

6. Ten cuidado al usar var con el diamante o con métodos genéricos.

Tanto var como la función «diamante» permiten omitir información de tipo explícita cuando esta puede deducirse de la información existente. Sin embargo, si se usan juntas, podrían omitir toda la información útil que el compilador necesita para inferir correctamente el tipo deseado.

Considera lo siguiente:

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

// DANGEROUS: infers as PriorityQueue<Object>
❌ var itemQueue = new PriorityQueue<>();

Los métodos genéricos también han aprovechado la inferencia de tipos con tanto éxito que es muy poco común que los programadores proporcionen argumentos de tipo explícitos. Si no hay argumentos reales para el método que aporten suficiente información de tipo, la inferencia de métodos genéricos depende del tipo de destino. En una declaración con var no hay tipo de destino, así que puede surgir un problema similar al del diamante. Por ejemplo:

// DANGEROUS: infers as List<Object>
❌ var list = List.of();

Tanto con el diamante como con los métodos genéricos, se puede proporcionar información de tipo adicional mediante argumentos reales del constructor o del método, lo que permite inferir el tipo deseado. Esto agrega un nivel adicional de indirección, pero sigue siendo predecible. Por lo tanto:

// OK: itemQueue infers as PriorityQueue<String>
Comparator<String> comp = ... ;
✅ var itemQueue = new PriorityQueue<>(comp);

7. Ten cuidado al usar var con literales.

Es poco probable que usar var con literales aporte una gran ventaja, ya que los nombres de los tipos suelen ser cortos. Sin embargo, var puede ser útil en algunos casos, por ejemplo, para alinear los nombres de las variables.

No hay ningún problema con los literales booleanos, de caracteres, long y de cadenas. El tipo que se infiere a partir de estos literales es preciso, por lo que el significado de var es inequívoco. Hay que tener especial cuidado cuando la inicialización es un valor numérico, en particular un literal entero. Si se especifica explícitamente el tipo a la izquierda, el valor numérico puede convertirse silenciosamente a tipos distintos de int, ya sea ampliándose o reduciéndose. Con var, el valor se inferirá como int, lo que quizá no sea lo deseado.

// ORIGINAL
boolean ready = true;
char ch = '\ufffd';
long sum = 0L;
String label = "wombat";
byte flags = 0;
short mask = 0x7fff;
long base = 17;

✅ var ready = true;
✅ var ch    = '\ufffd';
✅ var sum   = 0L;
✅ var label = "wombat";

// DANGEROUS: all infer as int
❌ var flags = 0;
❌ var mask = 0x7fff;
❌ var base = 17;

Conclusión

Usar var en las declaraciones puede mejorar el código al reducir el desorden y hacer que destaque la información más importante. Por otro lado, usar var indiscriminadamente puede empeorar las cosas. Si se usa correctamente, var puede mejorar un buen código: hacerlo más breve y claro sin perjudicar su comprensión. Al usar var, pregúntate si el código se volvió más ambiguo o si se entiende con claridad sin tener que investigarlo demasiado.

¡Descarga la guía rápida ahora!

Comienza con Capture the Flag

Aprende a resolver desafíos de Capture the Flag con nuestro taller virtual 101 a pedido.

Leer más

blog feature snyk container custom base recommendations
Blog

Presentamos Evo Agentic AppSec: seguridad al ritmo del desarrollo con IA

El desarrollo impulsado por IA supera el ritmo de los flujos de trabajo de seguridad tradicionales. Agentic AppSec de Snyk usa agentes de seguridad para encontrar, corregir y verificar vulnerabilidades de forma continua, mientras las personas definen el estándar.

feature insights context
Blog

Los ataques autónomos ya están aquí. La defensa debe estar a su altura.

Los atacantes autónomos están reduciendo el tiempo disponible para defenderse. Descubre cómo el descubrimiento, la corrección, la validación y la prevención continuos pueden ayudar a los equipos de seguridad a seguirles el ritmo.

illustration hero ai
Blog

El huracán de la IA ha llegado

La IA está acelerando por igual la creación de software y los ciberataques. Los líderes deben proteger los agentes y el código desde el inicio, aplicar controles en tiempo de ejecución y validar las defensas de forma independiente.