
Tips para escribir mejor código en Java
JavaFundamentos
Cada uno de los siguientes tips/trucos por sí solos pueden parecer insignificantes, pero en conjunto permiten tener un código más fácil de leer, más mantenible y menos propenso a errores, a la vez que hacen uso de características modernas de Java. En cada uno de los ejemplos se muestra una versión “mala” y una versión “buena” del código, donde la segunda es la recomendada.
1. try-with-resources
En lugar de tener que preocuparse por cerrar recursos manualmente, ya sea comprobando si han sido abiertos o utilizando un bloque finally
, podemos utilizar un bloque try-with-resources
que se encarga de cerrar automáticamente los recursos una vez que ya no se están utilizando. Esto es especialmente útil para manejar archivos, conexiones de red, bases de datos, etc.
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("data.txt"));
String line;
while ((line = br.readLine()) != null) System.out.println(line);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
String line;
while ((line = br.readLine()) != null) System.out.println(line);
} catch (IOException e) {
e.printStackTrace();
}
2. StringBuilder
Dado que un String
es inmutable, cuando se concatena utilizando el operador +
se crean múltiples objetos String
en memoria, lo que puede ser ineficiente. En su lugar, podemos utilizar un StringBuilder
, que es mutable y permite concatenar cadenas de manera más eficiente. Incluso el propio IDE puede llegar a realizar esta recomendación de manera automática.
String res = "";
for (int i = 0; i < 10000; i++) {
res += i;
}
System.out.println(res);
StringBuilder res = new StringBuilder();
for (int i = 0; i < 10000; i++) {
res.append(i);
}
System.out.println(res);
3. Early Return
En lugar de anidar múltiples bloques if
y else
, podemos utilizar un retorno anticipado para simplificar el flujo del código. Esto mejora la legibilidad y mantenibilidad del código.
private void processOrder(Order o) {
if (o != null) {
if (o.isValid()) {
if (o.hasValidItems()) {
// process order
} else {
// handle invalid items
}
} else {
// handle invalid order
}
} else {
// handle null order
}
}
private void processOrder(Order o) {
if (o == null) {
// handle null order
return;
}
if (!o.isValid()) {
// handle invalid order
return;
}
if (!o.hasValidItems()) {
// handle order with invalid items
return;
}
// Process order
}
4. Enums
Los enums
proveen seguridad de tipos, permiten crear un código más autodocumentado y evitan el uso de constantes mágicas, incluso permiten agregar métodos y atributos a los mismos. En lugar de utilizar constantes enteras o cadenas de texto, podemos utilizar enums
para representar un conjunto fijo de valores.
class Status {
public static final int ACTIVE = 1;
public static final int INACTIVE = 0;
}
public void process(int status) {
if (status == Status.ACTIVE) {
// do something
}
}
public enum Status {
ACTIVE,
INACTIVE
}
public void process(Status status) {
if (status == Status.ACTIVE) {
// do something
}
}
5. Optional
El uso de Optional
permite evitar las posibles comprobaciones respecto de null
, las excepciones NullPointerException
y el uso de bloques if-else
, lo que mejora la legibilidad y mantenibilidad del código. En lugar de devolver null
para indicar la ausencia de un valor, podemos utilizar Optional
para representar un valor que puede o no estar presente.
public static String getNormalizedName(Person p) {
if (p != null && p.getName() != null) {
return p.getName().toUpperCase();
}
return null;
}
public static String getNormalizedName(Person p) {
return Optional.ofNullable(p)
.map(Person::getName)
.map(String::toUpperCase)
.orElse("UNKNOWN");
}
6. Lambdas y Referencias de Métodos
Utilizar lambdas y referencias de métodos permite escribir código más conciso y legible, especialmente al trabajar con colecciones y la API de Streams. De esta manera se pueden eliminar clases anónimas, bucles innecesarios y código verboso.
List<String> names = Arrays.asList("John", "Mary", "Peter", "Paul", "Anna");
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
List<String> names = Arrays.asList("John", "Mary", "Peter", "Paul", "Anna");
names.sort(String::compareTo);
7. Static Factory Methods
En ocasiones puede resultar factible usar métodos de fábrica estáticos en lugar de constructores, ya que permiten crear instancias de una clase de manera más legible y flexible. Esto es especialmente útil cuando se trabaja con clases inmutables o cuando se desea proporcionar diferentes formas de crear instancias.
public class Car {
public Car (String model, int year, String color) {
// logic
}
}
public static void main(String[] args) {
var car = new Car("Fiat", 2020, "red");
}
public class Car {
private String model;
private int year;
private String color;
private Car (String model, int year, String color) {
this.model = model;
this.year = year;
this.color = color;
}
public static Car of(String model, int year, String color) {
// Extra validation or logic
return new Car(model, year, color);
}
}
public static void main(String[] args) {
var car = Car.of("Toyota", 2020, "Red");
}
8. Logging
En lugar de utilizar System.out.println
para imprimir mensajes de depuración o información, es recomendable utilizar un framework de logging como Log4j
, SLF4J
o java.util.logging
. Esto permite un mejor control sobre los niveles de logging, la configuración y la salida de los mensajes. Incluso algunos frameworks, como SLF4J, permiten un logging parametrizable, o el uso de Supplier
para evitar la creación de cadenas innecesarias.
System.out.println("Attempting to read last fetch date");
LOGGER.log(Level.INFO, () -> "Attempting to read last fetch date");
9. Objects.requireNonNull
Objects.requireNonNull
es un método estático que permite verificar si un objeto es null
y lanzar una NullPointerException
con un mensaje personalizado. Esto es útil para validar argumentos en métodos y constructores, evitando la necesidad de escribir bloques if
adicionales.
public class Person {
private String name;
public void setName(String name) {
if (name == null) {
throw new IllegalArgumentException("Name cannot be null");
}
this.name = name;
}
}
import java.util.*;
public class Person {
private String name;
public void setName(String name) {
this.name = Objects.requireNonNull(name, "Name cannot be null");
}
}
10. computeIfAbsent en Map
El método computeIfAbsent
de la interfaz Map
permite calcular un valor y agregarlo al mapa solo si la clave no está presente. Esto es útil para evitar comprobaciones adicionales e inicializar valores de manera más concisa.
String str = "hello";
Map<String, List<String>> map = new HashMap<>();
if (!map.containsKey("key")) {
map.put("key", new ArrayList<>());
}
map.get("key").add(str);
String str = "hello";
Map<String, List<String>> map = new HashMap<>();
map.computeIfAbsent("key", k -> new ArrayList<>()).add(str);
11. merge en Map
El método merge
de la interfaz Map
permite combinar valores de manera más concisa y eficiente. Esto es útil para evitar comprobaciones adicionales y simplificar la lógica de combinación.
String str = "hello";
Map<Character, Integer> map = new HashMap<>();
for (char c : str.toCharArray()) {
if (!map.containsKey(c)) {
map.put(c, 1);
} else {
map.put(c, map.get(c) + 1);
}
}
String str = "hello";
Map<Character, Integer> map = new HashMap<>();
for (char c : str.toCharArray()) {
map.merge(c, 1, Integer::sum);
}
12. Records
Los records
son una característica relativamente nueva que permite crear clases inmutables de manera más concisa. Esto es útil para representar datos sin necesidad de escribir código adicional para constructores, métodos equals
, hashCode
y toString
, es decir, evitar el típico boilerplate de Java. Pueden resultar muy útiles para representar objetos de valor o DTOs (Data Transfer Objects), y también pueden ser utilizados con Pattern Matching.
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
public record Person(String name, int age) {}
Conclusión
Estos son solo algunos de los muchos tips y trucos que pueden ayudarte a escribir código más limpio y eficiente en Java. A continuación puedes encontrar enlaces a diferentes post donde se ven a profundidad algunos de estos temas.