El ciclo for en Java, Go y Python

Los tres lenguajes tienen for, pero cada uno lo entiende a su manera. Java tiene dos formas, Go usa una sola palabra para todos los bucles, y en Python el for siempre recorre algo. Los comparamos con ejemplos ejecutados en los tres.

Java: el for clásico y el de recorrido

public class CicloFor {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            System.out.println("valor de i: " + i);
        }

        String[] lenguajes = {"Java", "Go", "Python"};
        for (String l : lenguajes) {
            System.out.println(l);
        }

        for (int i = 0, j = 10; i < j; i += 3, j -= 3) {
            System.out.println(i + " " + j);
        }
    }
}
java CicloFor.javavalor de i: 0
valor de i: 1
valor de i: 2
Java
Go
Python
0 10
3 7

El primero es el for heredado de C: una inicialización, una condición que se comprueba antes de cada vuelta y un incremento que se ejecuta al final de cada una. El segundo, el «for mejorado», recorre un arreglo o cualquier colección sin índice. El tercero recuerda que cada una de las tres partes puede tener varias expresiones separadas por comas.

Go: una sola palabra para todos los bucles

package main

import "fmt"

func main() {
	for i := 0; i < 3; i++ {
		fmt.Println("valor de i: ", i)
	}

	for i := range 3 {
		fmt.Println("range:", i)
	}

	lenguajes := []string{"Java", "Go", "Python"}
	for i, l := range lenguajes {
		fmt.Println(i, l)
	}

	n := 1
	for n < 100 {
		n *= 3
	}
	fmt.Println("primera potencia de 3 mayor que 100:", n)

	for {
		fmt.Println("una vez y fuera")
		break
	}
}
go run .valor de i:  0
valor de i:  1
valor de i:  2
range: 0
range: 1
range: 2
0 Java
1 Go
2 Python
primera potencia de 3 mayor que 100: 243
una vez y fuera

Go no tiene while ni do: todo se escribe con for. Con tres partes es el clásico, sin paréntesis. Con solo la condición, hace de while. Sin nada es un bucle infinito, del que se sale con break. Y con range recorre colecciones devolviendo el índice y el valor. Desde Go 1.22, range también acepta un número: for i := range 3 da 0, 1 y 2.

Fíjate en los dos espacios de valor de i: 0. fmt.Println pone un espacio entre cada argumento, y el texto ya terminaba en espacio. En Go se escribe fmt.Println("valor de i:", i), sin el espacio final.

Python: siempre recorre algo

for i in range(3):
    print("valor de i: ", i)

for i in range(3):
    print("valor de i:", i)

lenguajes = ["Java", "Go", "Python"]
for i, l in enumerate(lenguajes):
    print(i, l)

for i in range(10, 0, -3):
    print(i, end=" ")
print()

for l in lenguajes:
    if l == "Rust":
        break
else:
    print("no había Rust")
python ciclo.pyvalor de i:  0
valor de i:  1
valor de i:  2
valor de i: 0
valor de i: 1
valor de i: 2
0 Java
1 Go
2 Python
10 7 4 1
no había Rust

En Python no hay for con contador: se recorre una secuencia, y range genera los números. range(10, 0, -3) cuenta hacia atrás de tres en tres, y enumerate da el índice junto a cada elemento. El primer print tiene el mismo doble espacio que Go, por el mismo motivo: print separa sus argumentos con un espacio.

Lo exclusivo de Python es el else del for: se ejecuta solo si el bucle terminó sin break. Sirve para búsquedas como esta, donde llegar al final significa que no se encontró nada.

Lado a lado

JavaGoPython
Contar de 0 a 9for (int i = 0; i < 10; i++)for i := 0; i < 10; i++ o for i := range 10for i in range(10):
Recorrer una colecciónfor (String l : lista)for _, l := range listafor l in lista:
Con índiceel for clásicofor i, l := range listafor i, l in enumerate(lista):
Mientras se cumpla algowhile (cond)for condwhile cond:
Infinitowhile (true)forwhile True:
Bloquellavesllaves, obligatoriassangría

Lo que cambia por debajo

La versión anterior de esta entrada decía que Java es un lenguaje interpretado, como Python. No es exacto, y la diferencia explica buena parte de la diferencia de velocidad. Java se compila a bytecode, y la máquina virtual compila a código máquina, mientras el programa se ejecuta, las partes que más se usan. Go se compila directamente a un ejecutable nativo. Python, en su implementación habitual, también compila a bytecode, pero lo interpreta instrucción a instrucción, y por eso un bucle con mucho cálculo suele ir bastante más despacio. Para medirlo bien, mira medir el rendimiento en Java.

Entrada publicada en 2020 y revisada en octubre de 2026. Cada ejemplo se ejecutó en un contenedor limpio con Java 25 LTS, Go 1.25 y Python 3.14; las salidas están copiadas de esa ejecución.

Librerías de Java para Reportes

Estas son algunas de las librerías para Java que puedes usar para generar reportes en diversos formatos, agregar gráficas y otros recursos útiles para mostrar datos al usuario final. Algunos proyectos disponen de una versión opensource, edición community y versiones comerciales.

JFreeChart

JFreeChart es una biblioteca de gráficos hecha 100% Java que facilita a los desarrolladores mostrar gráficos de calidad profesional en tus aplicaciones. El amplio conjunto de características de JFreeChart incluye:

  • Una API consistente y bien documentada, que admite una amplia gama de tipos de gráficos
  • Diseño flexible que es fácil de ampliar, dirigida tanto a aplicaciones de servidor como del cliente.
  • Compatibilidad con muchos tipos de salida, incluidos los componentes Swing y JavaFX, archivos de imagen (incluidos PNG y JPEG) y formatos de archivos de gráficos vectoriales (incluidos PDF, EPS y SVG)
  • JFreeChart es de código abierto o más específicamente, software libre. Se distribuye bajo los términos de la GNU Lesser General Public Licence (LGPL), que permite su uso en aplicaciones propietarias.

JasperReport

JasperReport es una de las herramientas para reportes de código abierto más populares y ampliamente utilizadas. Se usa en cientos de miles de entornos de producción y presenta versiones tanto community como comerciales.

  • Formateo y visualización de reportes interactivos
  • Repositorio central seguro
  • Informe de programación y distribución
  • Interfaz de usuario personalizable y personalizable

BIRT

BIRT consta de varios componentes. Los componentes principales incluyen un diseñador de reportes y en tiempo de ejecución de BIRT, pero BIRT también proporciona tres componentes adicionales: un motor de gráficos, un diseñador de gráficos y un visor. Con estos componentes, podrías desarrollar y publicar informes como una solución independiente.

  • Listas
  • Gráficas
  • Tablas cruzadas
  • Cartas y documentos
  • Informas compuestos

Pentaho

Pentaho es una suite empresarial completa, que abarca una amplia gama de casos de uso desde reportes hasta minería de datos. La suite de Pentaho BI abarca varios proyectos de código abierto, de los cuales Pentaho Reporting es uno de ellos. Al igual que las otras herramientas, Pentaho Reporting tiene un amplio conjunto de funciones, listas para su uso en organizaciones empresariales.

  • Permite crear reportes en PDF, Excel, HTML, texto, archivo de texto enriquecido, XML y CSV.

SpagoBI

SpagoBI es otra suite de inteligencia empresarial completa. Algunas herramientas que incluye son, por ejemplo: reportes, gráficos, cockpits, minería de datos, ETL y mucho más. La empresa que respalda este desarrollo ofrece servicios profesionales como asistencia al usuario, mantenimiento, consultoría y capacitación.

  • Reportes en formatos HTML, PDF, XLS, XML, TXT, CSV, RTF
  • Análisis multidimensional (OLAP)
  • Gráficas
  • KPIs
  • Coopits Interactivos
  • Reportes ad-hoc
  • Muchas otras características.

KNIME

KNIME es una plataforma de análisis de código abierto, con orígenes en la investigación farmacéutica. Su uso se ha expandido a industrias como bancos, editores, fabricantes de automóviles, compañías de telecomunicaciones y otros.

KNIME proporciona una gama de características, que incluyen extensiones de colaboración y un portal web. El aprendizaje automático también está integrado, incluido el soporte de Weka.

ReportServer

ReportServer es otra plataforma de inteligencia empresarial de código abierto, con una edición comercial disponible también. ReportServer permite crear reportes, análisis ad-hoc, informes de Excel y Word y análisis OLAP multidimensionales.

Curiosidades del punto flotante: por qué 9999999999999999.0 − 9999999999999998.0 da 2

¿Cuánto es 9999999999999999.0 - 9999999999999998.0? Debería ser 1, pero en la mayoría de los lenguajes el resultado es 2. Geo Carncross hizo la prueba en una veintena de ellos, y la tabla de arriba resume lo que obtuvo. Aquí lo comprobamos en Java y vemos por qué pasa.

En Java

double a = 9999999999999999.0;
double b = 9999999999999998.0;
IO.println("a: " + a);
IO.println("b: " + b);
IO.println("a - b: " + (a - b));
IO.println("a exacto: " + new BigDecimal(a));
IO.println("b exacto: " + new BigDecimal(b));
IO.println("separación entre doubles cerca de a: " + Math.ulp(a));
IO.println("2^53 = " + (long) Math.pow(2, 53));
salidaa: 1.0E16
b: 9.999999999999998E15
a - b: 2.0
a exacto: 10000000000000000
b exacto: 9999999999999998
separación entre doubles cerca de a: 2.0
2^53 = 9007199254740992

La segunda línea de la salida ya da la pista: a se imprime como 1.0E16, es decir, 10000000000000000. El número que escribimos no es el que se guardó. new BigDecimal(a) muestra el valor exacto que hay en memoria, y la resta, con esos valores, sí está bien hecha: 10000000000000000 − 9999999999999998 = 2.

Por qué no cabe

Un double ocupa 64 bits: 1 para el signo, 11 para el exponente y 52 para la mantisa, que con un bit implícito dan 53 bits de precisión. Con 53 bits se pueden representar exactamente todos los enteros hasta 253 = 9,007,199,254,740,992. Nuestros números son mayores.

Por encima de ese límite, los double ya no están en todos los enteros: entre 253 y 254 van de dos en dos, que es lo que dice Math.ulp(a). 9999999999999998 es par y cae justo en uno. 9999999999999999 es impar, queda exactamente a medio camino entre 9999999999999998 y 10000000000000000, y la regla de redondeo de IEEE 754 (al vecino «par») lo lleva a 10000000000000000.

No es un fallo de Java: es el formato que usan casi todos los lenguajes y los procesadores. Por eso la mayoría de la tabla dice 2.

El caso de todos los días: 0.1 + 0.2

No hace falta irse a números tan grandes. Con decimales pequeños pasa algo parecido, porque la mayoría no tienen representación exacta en binario, igual que 1/3 no la tiene en decimal:

IO.println("0.1 + 0.2 = " + (0.1 + 0.2));
IO.println("0.1 + 0.2 == 0.3 -> " + (0.1 + 0.2 == 0.3));
IO.println("0.1 exacto: " + new BigDecimal(0.1));
IO.println("Math.abs((0.1 + 0.2) - 0.3) < 1e-9 -> " + (Math.abs((0.1 + 0.2) - 0.3) < 1e-9));
salida0.1 + 0.2 = 0.30000000000000004
0.1 + 0.2 == 0.3 -> false
0.1 exacto: 0.1000000000000000055511151231257827021181583404541015625
Math.abs((0.1 + 0.2) - 0.3) < 1e-9 -> true

El 0.1 que se guarda es un poquito mayor que 0.1, y al sumar se acumulan esos errores. De ahí la regla: nunca compares double con ==. Compara la diferencia con un margen de tolerancia adecuado a tu problema.

Cuando hace falta exactitud

IO.println("new BigDecimal(\"0.1\").add(new BigDecimal(\"0.2\")) = " + new BigDecimal("0.1").add(new BigDecimal("0.2")));
IO.println("new BigDecimal(0.1).add(new BigDecimal(0.2)) = " + new BigDecimal(0.1).add(new BigDecimal(0.2)));
IO.println("9999999999999999L - 9999999999999998L = " + (9999999999999999L - 9999999999999998L));
double suma = 0;
for (int i = 0; i < 10; i++) suma += 0.1;
IO.println("0.1 sumado diez veces = " + suma);
salidanew BigDecimal("0.1").add(new BigDecimal("0.2")) = 0.3
new BigDecimal(0.1).add(new BigDecimal(0.2)) = 0.3000000000000000166533453693773481063544750213623046875
9999999999999999L - 9999999999999998L = 1
0.1 sumado diez veces = 0.9999999999999999

Dos detalles importantes. BigDecimal se crea a partir de una cadena: new BigDecimal(0.1) recibe el double ya redondeado y conserva el error con todos sus decimales. Y la resta original, hecha con long, da 1, porque un long guarda enteros exactos hasta unos 9.2 trillones.

Para…UsaPor qué
Medidas, física, gráficos, estadísticadoubleRápido, y un error en el decimal 16 no importa
DineroBigDecimal creado desde texto, o long en céntimosLos céntimos tienen que cuadrar siempre
Contadores e identificadores grandeslongExacto hasta 9,223,372,036,854,775,807
Comparar dos doubleMath.abs(x - y) < tolerancia== falla con resultados de operaciones

La tabla de la imagen está tomada del experimento de Geo Carncross, que sigue en línea con algunos lenguajes más.

Entrada publicada en 2019 y revisada en octubre de 2026. Todo el código se compiló y se ejecutó con Java 25 LTS en un contenedor limpio; las salidas están copiadas de esa ejecución.

Ese detalle…

¿Que otra cosa puede ser más perfecta para regalar?

La parodia de una pedida de mano, con un manual de Java en lugar de anillo. Para un programador, quizá el regalo más romántico posible.

Pregunta Java: campos transient al serializar

Piensa la respuesta antes de abrirla. Todas las opciones parecen razonables, y casi siempre hay un detalle que lo cambia todo.

Pregunta Java · Serialización

¿Cuál es el resultado de compilar y ejecutar la clase Persona?

import java.io.*;
public class Persona implements Serializable {
    transient String nombre;
    transient int edad;
    String ocupacion;

    public Persona(String nombre, int edad, String ocupacion) {
        this.edad = edad;
        this.nombre = nombre;
        this.ocupacion = ocupacion;
    }
    public String toString() {
        return "Soy " + nombre + ", tengo " + edad + " años y soy " + ocupacion;
    }
    public static void main(String[] args) throws Exception {
        Persona antes = new Persona("Yo", 30, "programador");
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("/dir/ejemplo.txt"));
        out.writeObject(antes);

        ObjectInputStream input = new ObjectInputStream(new FileInputStream("/dir/ejemplo.txt"));
        Persona despues = (Persona) input.readObject();
        System.out.println(despues);
    }
}
  1. Soy Yo, tengo 30 años y soy programador
  2. Soy Yo, tengo 30 años y soy null
  3. Soy null, tengo 0 años y soy programador
  4. Error de ejecución
Ver la respuesta

Respuesta: c.

java PersonaSoy null, tengo 0 años y soy programador

transient marca un campo para que no se guarde al serializar. Al leer el objeto, esos campos reciben su valor por omisión: null para nombre y 0 para edad. Solo ocupacion viaja en el archivo. Tampoco se ejecuta el constructor de Persona, que podría haberles dado otro valor.

Se usa para lo que no tiene sentido o no debe guardarse: contraseñas, cachés, conexiones abiertas. El enunciado original pedía «la clase Test», pero el código solo tiene la clase Persona. Para ejecutarlo creamos antes la carpeta /dir.

Pregunta publicada en 2014 y revisada en octubre de 2026. La respuesta se comprobó compilando y ejecutando el código con Java 25 LTS en un contenedor limpio; las salidas están copiadas de esa ejecución.

Pregunta Java: HashSet sin equals ni hashCode

Piensa la respuesta antes de abrirla. Todas las opciones parecen razonables, y casi siempre hay un detalle que lo cambia todo.

Pregunta Java · Collections

¿Cuál es el resultado de compilar y ejecutar la clase Test?

import java.util.HashSet;
public class Test {
    public static void main(String args[]) {
        HashSet hashset = new HashSet();
        String s1 = new String("uno");
        String s2 = new String("uno");
        CadenaBean s3 = new CadenaBean("dos");
        CadenaBean s4 = new CadenaBean("dos");
        CadenaBean s5 = new CadenaBean("tres");
        hashset.add(s1);
        hashset.add(s2);
        hashset.add(s3);
        hashset.add(s4);
        System.out.println(hashset);
    }
}
class CadenaBean {
    private String str;
    CadenaBean(String str) {
        this.str = str;
    }
    public String toString() {
        return str;
    }
}
  1. [uno,dos,tres]
  2. [uno,uno,dos,dos]
  3. [dos,dos,uno]
  4. No hay garantía del orden.
  5. Error de ejecución
Ver la respuesta

Respuesta: d.

java Test[dos, uno, dos]

Hay tres cosas que mirar:

  • s1 y s2 son objetos distintos, pero String define equals y hashCode por contenido, así que el conjunto los ve iguales y solo guarda uno.
  • CadenaBean no redefine ninguno de los dos y usa los de Object, que comparan identidad. Los dos "dos" cuentan como distintos y entran ambos.
  • s5 se crea pero nunca se añade.

Quedan tres elementos, uno, dos y dos, en un orden que HashSet no garantiza. En nuestra ejecución salió [dos, uno, dos], que se parece a la opción c pero no es lo que el código asegura. La respuesta correcta es la d. (La quinta opción también estaba marcada como «d» en la imagen original.)

Al compilar sale además un aviso, Test.java uses unchecked or unsafe operations, por usar HashSet sin tipo. Hoy se escribiría Set<Object> hashset = new HashSet<>();.

Pregunta publicada en 2014 y revisada en octubre de 2026. La respuesta se comprobó compilando y ejecutando el código con Java 25 LTS en un contenedor limpio; las salidas están copiadas de esa ejecución.

Pregunta Java: llamar a run() en vez de start()

Piensa la respuesta antes de abrirla. Todas las opciones parecen razonables, y casi siempre hay un detalle que lo cambia todo.

Pregunta Java · Threads

¿Cuál es el resultado de compilar y ejecutar la clase Test02?

public class Test02 {
    public static void main(String argv[]) {
        Test1 pm1 = new Test1("Uno");
        pm1.run();
        Test1 pm2 = new Test1("Dos");
        pm2.run();
    }
}

class Test1 extends Thread {
    private String nombre = "";
    Test1(String s) {
        nombre = s;
    }
    public void run() {
        for (int i = 0; i < 2; i++) {
            try {
                sleep(1000);
            } catch (InterruptedException e) {
            }
            yield();
            System.out.println(nombre);
        }
    }
}
  1. Uno Dos
  2. Uno Dos Uno Dos
  3. Uno Uno Dos Dos
  4. Error de ejecución
Ver la respuesta

Respuesta: c.

java Test02  —  con Thread.yield()Uno
Uno
Dos
Dos

El código llama a run() directamente, no a start(). Así no se crea ningún hilo: run() es un método normal que se ejecuta en el hilo principal, de principio a fin, antes de pasar a la línea siguiente. Primero Uno dos veces, luego Dos dos veces, y tarda unos cuatro segundos por los cuatro sleep(1000). Con start(), los dos hilos irían a la vez y lo normal sería ver los nombres intercalados.

En Java actual este código ya no compila tal cual. Desde Java 14, yield es una palabra con significado propio en las expresiones switch, y una llamada sin calificar se rechaza:
javac Test02.java  —  Java 25Test02.java:21: error: invalid use of a restricted identifier 'yield'
            yield();
            ^
  (to invoke a method called yield, qualify the yield with a receiver or type name)
1 error

Basta con escribir Thread.yield();. Con ese cambio se obtuvo la salida de arriba. En 2014, con Java 7 u 8, el código original compilaba.

Pregunta publicada en 2014 y revisada en octubre de 2026. La respuesta se comprobó compilando y ejecutando el código con Java 25 LTS en un contenedor limpio; las salidas están copiadas de esa ejecución.

Pregunta Java: serializar una clase cuyo padre no es Serializable

Piensa la respuesta antes de abrirla. Todas las opciones parecen razonables, y casi siempre hay un detalle que lo cambia todo.

Pregunta Java · Serialización

¿Cuál es el resultado de ejecutar la clase Prueba?

public class Padre {
    public Padre() {
        System.out.println("Padre");
    }
}
import java.io.Serializable;
public class Hijo extends Padre implements Serializable {
    public Hijo() {
        System.out.println("Hijo");
    }
}
import java.io.*;
public class Prueba {

    public static void main(String... args) throws Exception {
        Hijo a = new Hijo();
        ObjectOutputStream save = new ObjectOutputStream(new FileOutputStream("archivo.txt"));
        save.writeObject(a);
        save.flush();
        System.out.println("-");
        ObjectInputStream restore = new ObjectInputStream(new FileInputStream("archivo.txt"));
        Hijo b = (Hijo) restore.readObject();
    }
}
  1. Padre Hijo – Padre Hijo
  2. Padre Hijo – Padre
  3. Padre – Hijo
  4. Error en tiempo de ejecución
Ver la respuesta

Respuesta: b.

java PruebaPadre
Hijo
-
Padre

Al crear el objeto se ejecutan los dos constructores: Padre y Hijo. Al deserializarlo no se llama al constructor de la clase serializable, porque su estado se recupera del archivo. Pero Padre no es Serializable, así que su parte no se guardó, y Java la reconstruye llamando a su constructor sin parámetros. Por eso sale Padre una vez más, y Hijo no.

De ahí una regla poco conocida: si una clase serializable hereda de una que no lo es, esa clase padre tiene que tener un constructor sin parámetros accesible. Si no lo tiene, la deserialización falla con InvalidClassException.

Pregunta publicada en 2014 y revisada en octubre de 2026. La respuesta se comprobó compilando y ejecutando el código con Java 25 LTS en un contenedor limpio; las salidas están copiadas de esa ejecución.

Lo que no es Java

Texto sobre fondo claro: «Java != javascript».
«Java != javascript»

A pesar del nombre, Java y JavaScript son lenguajes distintos, que solo comparten parte de la sintaxis. JavaScript se llamó así en 1995 por motivos de marketing, aprovechando la fama que Java tenía entonces.

Comparar cadenas en Java: == frente a equals

¿Qué devuelven las seis expresiones de la imagen? Todo se resume en una idea: == compara si dos referencias apuntan al mismo objeto, y equals compara el contenido. Lo que lía es que, con las cadenas, a veces son el mismo objeto sin que lo parezca.

Las respuestas, ejecutadas

Este programa evalúa las seis expresiones de la imagen y unas cuantas más que ayudan a entenderlas:

import java.util.Objects;

void main() {
    String variable = null;

    IO.println("\"cadena\" == \"cadena\"                    -> " + ("cadena" == "cadena"));
    IO.println("\"cadena\" == \"cad\" + \"ena\"               -> " + ("cadena" == "cad" + "ena"));

    String cad = "cad";
    final String cadFinal = "cad";
    IO.println("\"cadena\" == cad + \"ena\"                 -> " + ("cadena" == cad + "ena"));
    IO.println("\"cadena\" == cadFinal + \"ena\"            -> " + ("cadena" == cadFinal + "ena"));

    IO.println("\"cadena\" == \"-cadena\".substring(1)      -> " + ("cadena" == "-cadena".substring(1)));
    IO.println("new String(\"cadena\") == \"cadena\"        -> " + (new String("cadena") == "cadena"));
    IO.println("new String(\"cadena\").equals(\"cadena\")   -> " + new String("cadena").equals("cadena"));
    IO.println("\"-cadena\".substring(1).intern() == \"cadena\" -> " + ("-cadena".substring(1).intern() == "cadena"));

    IO.println("\"cadena\".equals(variable)               -> " + "cadena".equals(variable));
    IO.println("Objects.equals(variable, \"cadena\")      -> " + Objects.equals(variable, "cadena"));
    IO.println("\"Cadena\".equalsIgnoreCase(\"CADENA\")     -> " + "Cadena".equalsIgnoreCase("CADENA"));
    IO.println("\"abeja\".compareTo(\"abeto\")              -> " + "abeja".compareTo("abeto"));

    try {
        IO.println(variable.equals("cadena"));
    } catch (NullPointerException e) {
        IO.println("variable.equals(\"cadena\") -> " + e);
    }
}
javac -g Cadenas.java
java Cadenas
salida"cadena" == "cadena"                    -> true
"cadena" == "cad" + "ena"               -> true
"cadena" == cad + "ena"                 -> false
"cadena" == cadFinal + "ena"            -> true
"cadena" == "-cadena".substring(1)      -> false
new String("cadena") == "cadena"        -> false
new String("cadena").equals("cadena")   -> true
"-cadena".substring(1).intern() == "cadena" -> true
"cadena".equals(variable)               -> false
Objects.equals(variable, "cadena")      -> false
"Cadena".equalsIgnoreCase("CADENA")     -> true
"abeja".compareTo("abeto")              -> -10
variable.equals("cadena") -> java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "variable" is null
La opción -g guarda los nombres de las variables locales en el .class, y por eso el último mensaje dice "variable". Si lo ejecutas directamente con java Cadenas.java sale "<local1>" en su lugar: el lanzador compila sin esa información.

Por qué cada una da lo que da

ExpresiónResultadoPor qué
"cadena" == "cadena"trueLos literales iguales se guardan una sola vez, en el pool de cadenas. Los dos son el mismo objeto.
"cadena" == "cad" + "ena"trueEs una expresión constante: el compilador une las dos partes y en el .class ya solo está "cadena".
"cadena" == "-cadena".substring(1)falsesubstring crea un objeto nuevo mientras el programa se ejecuta. Mismo texto, otro objeto.
new String("cadena").equals("cadena")trueequals compara los caracteres, que son los mismos.
new String("cadena") == "cadena"falsenew siempre crea un objeto nuevo, distinto del literal.
new String("cadena") == new String("cadena")falseDos new, dos objetos.

Lo interesante está en las dos líneas que añadimos: si una de las partes es una variable normal, la concatenación ocurre al ejecutar y da un objeto nuevo (false). Si la variable es final y se inicializa con un literal, cuenta como constante y el compilador vuelve a unirlas antes de tiempo (true). Es decir, el resultado de == depende de detalles que no se ven al leer el código. Basta un cambio inocente, como quitar un final, para que una comparación que funcionaba deje de hacerlo.

Pasa lo mismo con cualquier texto que llegue de fuera: lo que se lee de un archivo, de la consola o de una petición web son objetos nuevos, aunque contengan exactamente lo mismo que un literal del programa.

La regla: equals, siempre

Para saber si dos cadenas tienen el mismo texto, equals. == solo sirve para preguntar si son el mismo objeto, y eso casi nunca es lo que quieres saber.

intern() devuelve la copia del pool, y por eso "-cadena".substring(1).intern() == "cadena" da true. Es una curiosidad que conviene conocer, pero no una forma de comparar: obliga a acordarse de llamarlo en todas partes.

Cuando la variable puede ser null

Si la variable es null, variable.equals("cadena") lanza un NullPointerException, como se ve en la última línea de la salida. Hay dos formas de evitarlo:

"cadena".equals(variable)

Poner el literal delante: un literal nunca es null, y equals(null) simplemente devuelve false.

Objects.equals(variable, "cadena")

Objects.equals sirve cuando las dos pueden ser null: devuelve true si ambas lo son y nunca lanza la excepción.

Otras comparaciones

Para…Se usaEjemplo
Mismo textoa.equals(b)true / false
Mismo texto, con posibles nullObjects.equals(a, b)true / false
Sin importar mayúsculasa.equalsIgnoreCase(b)"Cadena" y "CADENA" → true
Ordenara.compareTo(b)negativo, cero o positivo: "abeja" va antes que "abeto" → -10

El -10 de compareTo no es una distancia con significado: es la diferencia entre la primera pareja de caracteres que no coinciden (j y t). Lo único que importa es el signo.

Entrada publicada en 2013 y revisada en octubre de 2026. Todo el código se compiló y se ejecutó con Java 25 LTS en un contenedor limpio; las salidas están copiadas de esa ejecución.