Java

Tutorial Java: Operadores

Java operadores: tipos y ejemplos detallados. Domina los diferentes tipos de operadores en Java con ejemplos detallados.

Aprende Java y certifícate

Operadores aritméticos

Los operadores aritméticos en Java permiten realizar operaciones matemáticas básicas entre variables y valores. Estos operadores son fundamentales para cualquier tipo de cálculo en programación y se utilizan de manera similar a como se emplean en matemáticas convencionales.

Java proporciona un conjunto completo de operadores aritméticos que se pueden aplicar a tipos numéricos como int, long, float, double, byte y short.

Operador de suma (+)

El operador de suma se utiliza para añadir valores numéricos o concatenar cadenas de texto.

int a = 10;
int b = 20;
int suma = a + b;  // suma contiene 30

// También funciona con otros tipos numéricos
double x = 15.5;
double y = 4.5;
double resultadoDouble = x + y;  // resultadoDouble contiene 20.0

// Con String se usa para concatenación
String nombre = "Java";
String version = "23";
String completo = nombre + " " + version;  // completo contiene "Java 23"

Es importante tener en cuenta que cuando se utiliza el operador + con un String y otro tipo de dato, Java convierte automáticamente el otro tipo a String y realiza una concatenación.

Operador de resta (-)

El operador de resta se utiliza para sustraer un valor de otro.

int a = 30;
int b = 12;
int resta = a - b;  // resta contiene 18

double precio = 99.99;
double descuento = 20.00;
double precioFinal = precio - descuento;  // precioFinal contiene 79.99

Operador de multiplicación (*)

El operador de multiplicación se utiliza para multiplicar dos valores.

int a = 5;
int b = 6;
int multiplicacion = a * b;  // multiplicacion contiene 30

// Cálculo del área de un círculo
double radio = 5.0;
double area = Math.PI * radio * radio;  // área del círculo con radio 5

Operador de división (/)

El operador de división divide el operando izquierdo por el derecho. El comportamiento varía dependiendo de si los operandos son enteros o de punto flotante:

// División con enteros (resultado truncado)
int a = 20;
int b = 3;
int divisionEntera = a / b;  // divisionEntera contiene 6 (se trunca el decimal)

// División con punto flotante
double x = 20.0;
double y = 3.0;
double divisionReal = x / y;  // divisionReal contiene 6.666666666666667

// División mixta (el resultado será de punto flotante)
double resultado = 20 / 3.0;  // resultado contiene 6.666666666666667

Cuando se dividen dos números enteros, el resultado se trunca (no se redondea). Si se necesita precisión decimal, al menos uno de los operandos debe ser de tipo flotante.

Operador de módulo (%)

El operador de módulo devuelve el resto de la división entre dos números.

int a = 17;
int b = 5;
int modulo = a % b;  // modulo contiene 2 (17 dividido por 5 es 3 con resto 2)

// Comprobar si un número es par
int numero = 24;
boolean esPar = (numero % 2 == 0);  // esPar contiene true

El operador módulo es útil para:

  • Determinar si un número es divisible por otro
  • Operaciones cíclicas (como recorrer un array circularmente)
  • Limitar un valor dentro de un rango específico

Operadores de incremento (++) y decremento (--)

Java proporciona operadores especiales para incrementar o decrementar un valor en una unidad:

int contador = 10;

// Incremento
contador++;  // Equivalente a: contador = contador + 1
System.out.println(contador);  // Muestra 11

// Decremento
contador--;  // Equivalente a: contador = contador - 1
System.out.println(contador);  // Muestra 10

Estos operadores tienen dos formas: prefijo y sufijo, que difieren en cuándo se aplica el incremento o decremento:

int a = 5;
int b = 5;

// Forma prefijo: incrementa y luego devuelve el valor
int resultadoPrefijo = ++a;  // a se incrementa a 6, resultadoPrefijo es 6

// Forma sufijo: devuelve el valor y luego incrementa
int resultadoSufijo = b++;  // resultadoSufijo es 5, luego b se incrementa a 6

Esta diferencia es crucial cuando se utilizan estos operadores dentro de expresiones más complejas:

int x = 10;
int y = 5;
int resultado = x + (++y);  // y se incrementa a 6, resultado es 16

x = 10;
y = 5;
resultado = x + (y++);  // resultado es 15, luego y se incrementa a 6

Operadores aritméticos compuestos

Java ofrece operadores compuestos que combinan una operación aritmética con una asignación:

int numero = 10;

// Equivalentes a las operaciones expandidas
numero += 5;  // Equivalente a: numero = numero + 5
numero -= 3;  // Equivalente a: numero = numero - 3
numero *= 2;  // Equivalente a: numero = numero * 2
numero /= 4;  // Equivalente a: numero = numero / 4
numero %= 3;  // Equivalente a: numero = numero % 3

Estos operadores no solo hacen que el código sea más conciso, sino que también pueden ser más eficientes en la ejecución, ya que la variable se evalúa solo una vez.

Precedencia de operadores aritméticos

Al igual que en matemáticas, los operadores aritméticos en Java siguen un orden de precedencia:

1. Operadores de incremento/decremento (++, --)

2. Operadores unarios (+, -)

3. Multiplicación, división y módulo (*, /, %)

4. Suma y resta (+, -)

int resultado = 10 + 20 * 3;  // resultado es 70, no 90
int conParentesis = (10 + 20) * 3;  // conParentesis es 90

Se recomienda usar paréntesis para hacer el código más legible y evitar errores, incluso cuando no sean estrictamente necesarios:

// Más claro con paréntesis explícitos
int a = 5;
int b = 3;
int c = 2;
int resultado = a + (b * c);  // Más legible que: a + b * c

Desbordamiento aritmético

Un aspecto importante a considerar es el desbordamiento aritmético. Los tipos primitivos en Java tienen rangos limitados, y las operaciones que exceden estos límites provocan un desbordamiento:

int numeroMaximo = Integer.MAX_VALUE;  // 2,147,483,647
int resultado = numeroMaximo + 1;  // Desbordamiento: resultado es -2,147,483,648

Para operaciones que podrían exceder los límites de int, se recomienda usar tipos de mayor capacidad como long:

long resultadoLargo = (long)Integer.MAX_VALUE + 1;  // 2,147,483,648

Precisión en operaciones de punto flotante

Las operaciones con números de punto flotante (float y double) pueden presentar problemas de precisión debido a la representación binaria de estos números:

double a = 0.1;
double b = 0.2;
double suma = a + b;  // suma es 0.30000000000000004, no exactamente 0.3

Para cálculos financieros o donde se requiera precisión exacta, se recomienda usar la clase BigDecimal:

import java.math.BigDecimal;

BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
BigDecimal suma = a.add(b);  // suma es exactamente 0.3

Operaciones con tipos mixtos

Cuando se realizan operaciones entre diferentes tipos numéricos, Java aplica promoción automática de tipos siguiendo estas reglas:

1. Si alguno de los operandos es double, el resultado es double

2. Si no, si alguno de los operandos es float, el resultado es float

3. Si no, si alguno de los operandos es long, el resultado es long

4. En otro caso, el resultado es int

int entero = 5;
double decimal = 2.5;
double resultado = entero * decimal;  // resultado es 12.5 (double)

byte pequeño = 10;
short mediano = 20;
int suma = pequeño + mediano;  // suma es de tipo int, no byte ni short

Ejemplos prácticos

Veamos algunos ejemplos prácticos de uso de operadores aritméticos:

// Cálculo del promedio de tres notas
int nota1 = 85;
int nota2 = 90;
int nota3 = 78;
double promedio = (nota1 + nota2 + nota3) / 3.0;  // Usar 3.0 para obtener resultado decimal

// Conversión de temperatura de Celsius a Fahrenheit
double celsius = 25.0;
double fahrenheit = (celsius * 9/5) + 32;  // fahrenheit contiene 77.0

// Cálculo del IVA (21%)
double precioSinIVA = 100.0;
double iva = precioSinIVA * 0.21;
double precioConIVA = precioSinIVA + iva;

// Cálculo del interés compuesto
double principal = 1000.0;  // Capital inicial
double tasaInteres = 0.05;  // 5% anual
int años = 3;
double montoFinal = principal * Math.pow(1 + tasaInteres, años);

Operadores de asignación

Los operadores de asignación en Java se utilizan para asignar valores a variables. Estos operadores permiten almacenar y modificar datos durante la ejecución. El operador de asignación básico es el signo igual (=), pero Java ofrece también operadores de asignación compuestos que combinan una operación aritmética con la asignación.

Operador de asignación simple (=)

El operador de asignación básico (=) asigna el valor de la expresión de la derecha a la variable de la izquierda:

int edad = 25;                // Asigna 25 a la variable edad
String nombre = "Ana";        // Asigna "Ana" a la variable nombre
double salario = 2500.50;     // Asigna 2500.50 a la variable salario
boolean activo = true;        // Asigna true a la variable activo

Es importante entender que el operador = no compara valores (para eso se usa ==), sino que realiza una asignación. La expresión de la derecha se evalúa primero y luego su resultado se asigna a la variable de la izquierda.

Se pueden realizar asignaciones en cadena, donde el valor se propaga de derecha a izquierda:

int a, b, c;
a = b = c = 10;  // Todas las variables reciben el valor 10

En este caso, primero se asigna 10 a c, luego el valor de c (que ahora es 10) se asigna a b, y finalmente el valor de b (también 10) se asigna a a.

Operadores de asignación compuestos

Los operadores de asignación compuestos combinan una operación aritmética, de bits o lógica con una asignación. Estos operadores hacen que el código sea más conciso y a menudo más eficiente, ya que la variable se evalúa solo una vez.

Operador de asignación con suma (+=)

int contador = 5;
contador += 3;  // Equivalente a: contador = contador + 3
System.out.println(contador);  // Muestra 8

Este operador también funciona con cadenas para concatenación:

String mensaje = "Hola";
mensaje += " mundo";  // Equivalente a: mensaje = mensaje + " mundo"
System.out.println(mensaje);  // Muestra "Hola mundo"

Operador de asignación con resta (-=)

int puntos = 100;
puntos -= 25;  // Equivalente a: puntos = puntos - 25
System.out.println(puntos);  // Muestra 75

Operador de asignación con multiplicación (*=)

int factor = 4;
factor *= 2;  // Equivalente a: factor = factor * 2
System.out.println(factor);  // Muestra 8

double precio = 50.0;
precio *= 1.21;  // Añadir 21% de IVA
System.out.println(precio);  // Muestra 60.5

Operador de asignación con división (/=)

int dividendo = 20;
dividendo /= 4;  // Equivalente a: dividendo = dividendo / 4
System.out.println(dividendo);  // Muestra 5

double valor = 100.0;
valor /= 3;  // Equivalente a: valor = valor / 3
System.out.println(valor);  // Muestra aproximadamente 33.33333

Operador de asignación con módulo (%=)

int numero = 17;
numero %= 5;  // Equivalente a: numero = numero % 5
System.out.println(numero);  // Muestra 2 (el resto de dividir 17 entre 5)

Este operador es útil para mantener un valor dentro de un rango específico, como en algoritmos cíclicos:

int posicion = 7;
int tamanioArray = 5;
posicion %= tamanioArray;  // Mantiene posicion en el rango 0-4
System.out.println(posicion);  // Muestra 2

Operadores de asignación con operadores de bits

Java también proporciona operadores de asignación compuestos para operaciones a nivel de bits:

Operador de asignación con AND a nivel de bits (&=)

int flags = 0b1010;  // Valor binario 1010 (decimal 10)
flags &= 0b1100;     // AND a nivel de bits con 1100
System.out.println(Integer.toBinaryString(flags));  // Muestra "1000" (decimal 8)

Operador de asignación con OR a nivel de bits (|=)

int permisos = 0b101;  // Valor binario 101 (decimal 5)
permisos |= 0b010;     // OR a nivel de bits con 010
System.out.println(Integer.toBinaryString(permisos));  // Muestra "111" (decimal 7)

Operador de asignación con XOR a nivel de bits (^=)

int bits = 0b1010;  // Valor binario 1010 (decimal 10)
bits ^= 0b1100;     // XOR a nivel de bits con 1100
System.out.println(Integer.toBinaryString(bits));  // Muestra "110" (decimal 6)

Operadores de asignación con desplazamiento de bits (<<=, >>=, >>>=)

int valor = 8;       // Valor binario 1000
valor <<= 2;         // Desplazamiento a la izquierda 2 posiciones
System.out.println(valor);  // Muestra 32 (binario 100000)

int numero = 16;     // Valor binario 10000
numero >>= 2;        // Desplazamiento a la derecha 2 posiciones
System.out.println(numero);  // Muestra 4 (binario 100)

int valorNegativo = -8;
valorNegativo >>>= 1;  // Desplazamiento a la derecha sin signo
System.out.println(valorNegativo);  // Muestra un número positivo grande

Comportamiento con tipos diferentes

Cuando se asigna un valor a una variable de un tipo diferente, se aplican las reglas de conversión de tipos de Java:

// Asignación con conversión implícita (widening)
int entero = 100;
long numeroLargo = entero;  // Conversión implícita de int a long

// Asignación que requiere conversión explícita (narrowing)
double decimal = 123.45;
int enteroDesdeDecimal = (int) decimal;  // Se requiere casting explícito
System.out.println(enteroDesdeDecimal);  // Muestra 123 (se trunca la parte decimal)

En el caso de los operadores de asignación compuestos, Java realiza automáticamente el casting necesario:

byte pequeño = 10;
pequeño += 5;  // Funciona bien, Java hace el casting implícitamente

// Lo anterior es equivalente a:
// pequeño = (byte)(pequeño + 5);

// Esto daría error de compilación:
// pequeño = pequeño + 5;  // Error: posible pérdida de precisión

Asignación en expresiones

En Java, las asignaciones son expresiones que devuelven un valor, no solo sentencias. Esto permite encadenar asignaciones o usarlas dentro de otras expresiones:

int a, b;
System.out.println(a = b = 5);  // Asigna 5 a b, luego a a, y muestra 5

int c = 0;
if ((c = obtenerValor()) > 0) {  // Asigna el resultado de obtenerValor() a c y luego compara
    System.out.println("Valor positivo: " + c);
}

Sin embargo, este estilo de programación puede hacer que el código sea más difícil de leer y mantener, por lo que se recomienda usarlo con moderación.

Patrones comunes y ejemplos prácticos

Los operadores de asignación se utilizan en varios patrones de programación:

Acumuladores y contadores

// Sumar elementos de un array
int[] numeros = {5, 10, 15, 20, 25};
int suma = 0;
for (int numero : numeros) {
    suma += numero;
}
System.out.println("La suma es: " + suma);  // Muestra 75

// Contador de caracteres específicos en un String
String texto = "programación en Java";
int contadorA = 0;
for (char c : texto.toCharArray()) {
    if (c == 'a') {
        contadorA++;  // Equivalente a contadorA += 1
    }
}
System.out.println("Número de 'a': " + contadorA);  // Muestra 3

Construcción de cadenas

// Construcción de una cadena con StringBuilder
StringBuilder builder = new StringBuilder();
for (int i = 1; i <= 5; i++) {
    builder.append("Elemento ").append(i).append("\n");
}
String resultado = builder.toString();

Manipulación de flags de bits

// Configuración de flags de permisos
final int LEER = 0b001;    // 1 en decimal
final int ESCRIBIR = 0b010; // 2 en decimal
final int EJECUTAR = 0b100; // 4 en decimal

int permisos = 0;
permisos |= LEER;     // Añadir permiso de lectura
permisos |= EJECUTAR; // Añadir permiso de ejecución

// Verificar permisos
boolean puedeEscribir = (permisos & ESCRIBIR) != 0;
System.out.println("¿Puede escribir? " + puedeEscribir);  // Muestra false

// Eliminar un permiso
permisos &= ~EJECUTAR;  // Quitar permiso de ejecución

Intercambio de valores sin variable temporal

// Intercambio de valores usando operadores de asignación compuestos
int x = 5;
int y = 10;

x ^= y;
y ^= x;
x ^= y;

System.out.println("x = " + x + ", y = " + y);  // Muestra "x = 10, y = 5"

Consideraciones de rendimiento

Los operadores de asignación compuestos pueden ofrecer mejoras de rendimiento en ciertos escenarios:

// Menos eficiente
String mensaje = "";
for (int i = 0; i < 1000; i++) {
    mensaje = mensaje + i + " ";  // Crea un nuevo objeto String en cada iteración
}

// Más eficiente
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append(i).append(" ");  // Modifica el mismo objeto StringBuilder
}
mensaje = sb.toString();

Operadores de comparación

Los operadores de comparación en Java permiten evaluar la relación entre dos valores o expresiones, devolviendo siempre un resultado booleano (true o false). Estos operadores permiten crear condiciones que determinan el flujo de ejecución de un programa.

Operador de igualdad (==)

El operador de igualdad compara si dos valores son idénticos. Es importante distinguirlo del operador de asignación (=), que asigna un valor a una variable.

int a = 5;
int b = 5;
boolean sonIguales = (a == b);  // sonIguales contiene true

int c = 10;
boolean otraComparacion = (a == c);  // otraComparacion contiene false

Para tipos primitivos, el operador == compara directamente los valores. Sin embargo, para objetos, compara las referencias (direcciones de memoria), no el contenido:

// Comparación de objetos String
String str1 = "hola";
String str2 = "hola";
boolean mismaReferencia = (str1 == str2);  // Puede ser true debido al pool de strings

// Creación explícita de nuevos objetos String
String str3 = new String("hola");
String str4 = new String("hola");
boolean mismaReferencia2 = (str3 == str4);  // Siempre será false

Para comparar el contenido de objetos, se debe utilizar el método equals():

String texto1 = new String("Java");
String texto2 = new String("Java");

// Comparación incorrecta (compara referencias)
boolean comparacionIncorrecta = (texto1 == texto2);  // false

// Comparación correcta (compara contenido)
boolean comparacionCorrecta = texto1.equals(texto2);  // true

Operador de desigualdad (!=)

El operador de desigualdad evalúa si dos valores son diferentes:

int x = 8;
int y = 12;
boolean sonDiferentes = (x != y);  // sonDiferentes contiene true

char letra1 = 'A';
char letra2 = 'A';
boolean caracteresDistintos = (letra1 != letra2);  // caracteresDistintos contiene false

Al igual que con el operador de igualdad, para objetos compara referencias, no contenido:

StringBuilder sb1 = new StringBuilder("texto");
StringBuilder sb2 = new StringBuilder("texto");
boolean distintos = (sb1 != sb2);  // distintos contiene true (son objetos diferentes)

Operador mayor que (>)

El operador mayor que evalúa si el valor de la izquierda es mayor que el de la derecha:

int edad = 25;
boolean esMayorDeEdad = (edad > 18);  // esMayorDeEdad contiene true

double temperatura = 36.5;
boolean tieneFiebre = (temperatura > 37.5);  // tieneFiebre contiene false

Operador menor que (<)

El operador menor que evalúa si el valor de la izquierda es menor que el de la derecha:

int puntuacion = 65;
boolean suspenso = (puntuacion < 70);  // suspenso contiene true

char letra = 'B';
boolean anteriorAC = (letra < 'C');  // anteriorAC contiene true (comparación de códigos ASCII)

Operador mayor o igual que (>=)

El operador mayor o igual que evalúa si el valor de la izquierda es mayor o igual que el de la derecha:

int edad = 18;
boolean puedeVotar = (edad >= 18);  // puedeVotar contiene true

double descuento = 15.0;
boolean descuentoSignificativo = (descuento >= 10.0);  // descuentoSignificativo contiene true

Operador menor o igual que (<=)

El operador menor o igual que evalúa si el valor de la izquierda es menor o igual que el de la derecha:

int velocidad = 120;
boolean dentroDelLimite = (velocidad <= 120);  // dentroDelLimite contiene true

byte nivel = 5;
boolean nivelBajo = (nivel <= 3);  // nivelBajo contiene false

Comparación de tipos diferentes

Java permite comparar valores de diferentes tipos numéricos, realizando promociones automáticas de tipos:

int entero = 100;
long numeroLargo = 100L;
boolean comparacion = (entero == numeroLargo);  // true, el int se promueve a long

double decimal = 100.0;
boolean otraComparacion = (entero == decimal);  // true, el int se promueve a double

Sin embargo, hay que tener cuidado con las comparaciones entre tipos de punto flotante debido a problemas de precisión:

double a = 0.1 + 0.2;
double b = 0.3;
boolean sonIguales = (a == b);  // false debido a imprecisiones de punto flotante

// Forma correcta de comparar números de punto flotante
final double EPSILON = 0.0001;
boolean sonAproximadamenteIguales = Math.abs(a - b) < EPSILON;  // true

Comparación de caracteres

Los caracteres (char) se comparan según su valor numérico en la tabla Unicode:

char c1 = 'A';  // Valor Unicode: 65
char c2 = 'B';  // Valor Unicode: 66

boolean menorQue = (c1 < c2);  // true, porque 65 < 66
boolean mayorQue = ('z' > 'a');  // true, porque 'z' tiene un valor Unicode mayor que 'a'

Esto permite realizar comparaciones alfabéticas:

char letra = 'M';
boolean estaEnPrimerasMitad = (letra <= 'M');  // true, está en la primera mitad del alfabeto

Comparación de objetos comparables

Muchas clases en Java implementan la interfaz Comparable, lo que permite utilizar métodos como compareTo() para establecer un orden natural:

String cadena1 = "abc";
String cadena2 = "def";

// compareTo() devuelve un valor negativo si cadena1 es "menor" que cadena2
boolean esMenor = cadena1.compareTo(cadena2) < 0;  // true

// Para comparar ignorando mayúsculas/minúsculas
String texto1 = "Java";
String texto2 = "java";
boolean igualesIgnorandoMayusculas = texto1.compareToIgnoreCase(texto2) == 0;  // true

Operador instanceof

Aunque no es estrictamente un operador de comparación numérica, instanceof es un operador de comparación que verifica si un objeto es una instancia de un tipo específico:

String texto = "Hola mundo";
boolean esString = texto instanceof String;  // esString contiene true
boolean esObject = texto instanceof Object;  // esObject contiene true (herencia)

Integer numero = 42;
boolean esInteger = numero instanceof Integer;  // esInteger contiene true
boolean esNumber = numero instanceof Number;    // esNumber contiene true (Integer extiende Number)

Con Java moderno, se puede combinar instanceof con una asignación de variable (pattern matching):

Object obj = "Texto de ejemplo";

// Pattern matching con instanceof (disponible desde Java 16)
if (obj instanceof String texto) {
    // La variable 'texto' ya está disponible y tipada como String
    System.out.println("La longitud es: " + texto.length());
}

Comparaciones encadenadas

En Java, no se pueden encadenar operadores de comparación como en matemáticas (por ejemplo, a < b < c). En su lugar, se deben usar operadores lógicos:

// Incorrecto (no funciona como se espera)
int valor = 5;
boolean enRango = (0 < valor < 10);  // Error de compilación

// Correcto
boolean enRango = (0 < valor && valor < 10);  // true

Comparaciones en estructuras de control

Los operadores de comparación se utilizan frecuentemente en estructuras de control como if, while y for:

// En una sentencia if
int edad = 20;
if (edad >= 18) {
    System.out.println("Es mayor de edad");
} else {
    System.out.println("Es menor de edad");
}

// En un bucle while
int contador = 0;
while (contador < 5) {
    System.out.println("Iteración: " + contador);
    contador++;
}

// En un bucle for
for (int i = 0; i < 10; i++) {
    if (i % 2 == 0) {
        System.out.println(i + " es par");
    }
}

Comparaciones compuestas con operadores lógicos

Los operadores de comparación suelen combinarse con operadores lógicos para formar condiciones más complejas:

int edad = 25;
double salario = 30000.0;

// Usando AND lógico (&&)
boolean elegibleParaPrestamo = (edad >= 18 && salario > 25000.0);  // true

// Usando OR lógico (||)
int puntuacion = 65;
boolean aprobado = (puntuacion >= 70 || puntuacion >= 60 && asistenciaCompleta);

// Usando NOT lógico (!)
boolean fueraDeRango = !(valor >= 1 && valor <= 100);  // true si valor < 1 o valor > 100

Ejemplos prácticos

Veamos algunos ejemplos prácticos del uso de operadores de comparación:

// Validación de entrada de usuario
int edad = obtenerEdadDelUsuario();
if (edad < 0 || edad > 120) {
    System.out.println("Edad no válida");
} else if (edad < 18) {
    System.out.println("Menor de edad");
} else if (edad >= 65) {
    System.out.println("Edad de jubilación");
} else {
    System.out.println("Adulto en edad laboral");
}

// Búsqueda binaria en un array ordenado
int[] numeros = {2, 5, 8, 12, 16, 23, 38, 56, 72, 91};
int objetivo = 23;
int inicio = 0;
int fin = numeros.length - 1;
boolean encontrado = false;

while (inicio <= fin) {
    int medio = (inicio + fin) / 2;
    
    if (numeros[medio] == objetivo) {
        encontrado = true;
        break;
    } else if (numeros[medio] < objetivo) {
        inicio = medio + 1;
    } else {
        fin = medio - 1;
    }
}

// Validación de rango de fechas
LocalDate fechaInicio = LocalDate.of(2023, 1, 1);
LocalDate fechaFin = LocalDate.of(2023, 12, 31);
LocalDate fechaActual = LocalDate.now();

boolean dentroDelPeriodo = !fechaActual.isBefore(fechaInicio) && !fechaActual.isAfter(fechaFin);

Buenas prácticas

Al trabajar con operadores de comparación, se recomienda seguir estas buenas prácticas:

  • Evitar comparaciones de igualdad con punto flotante: Debido a imprecisiones, usar un epsilon o BigDecimal para comparaciones exactas.
  • Usar equals() para objetos: No usar == para comparar el contenido de objetos.
  • Mantener la legibilidad: Usar paréntesis para clarificar el orden de evaluación en expresiones complejas.
  • Evitar comparaciones redundantes: Por ejemplo, if (esMayorDeEdad == true) se puede simplificar a if (esMayorDeEdad).
  • Cuidado con los valores nulos: Verificar que los objetos no sean null antes de invocar métodos en ellos.
// Comparación segura con posibles nulos
String texto = obtenerTextoQuePuedeSerNulo();
boolean esIgual = "valor".equals(texto);  // No lanza NullPointerException si texto es null

// Alternativa con Objects.equals() (desde Java 7)
boolean sonIguales = Objects.equals(texto1, texto2);  // Maneja correctamente si alguno es null

Operadores lógicos

Los operadores lógicos en Java permiten combinar expresiones booleanas y evaluar condiciones complejas. Estos operadores trabajan con valores boolean (verdadero o falso) y son fundamentales para implementar la lógica de decisión en los programas.

Java proporciona varios tipos de operadores lógicos, cada uno con características específicas en cuanto a precedencia y comportamiento de evaluación.

Operador AND lógico (&&)

El operador && (AND lógico) devuelve true únicamente cuando ambas expresiones que conecta son verdaderas:

boolean condicion1 = true;
boolean condicion2 = false;

boolean resultado = condicion1 && condicion2;  // resultado contiene false

Una característica importante del operador && es su evaluación de cortocircuito. Si la primera expresión es false, la segunda expresión no se evalúa, ya que el resultado final será false independientemente:

int x = 5;
// La segunda parte no se evalúa porque la primera es false
if (x > 10 && metodoQueDevuelveBoolean()) {
    System.out.println("Nunca se ejecuta");
}

Esta característica es útil para evitar errores como divisiones por cero o referencias nulas:

// Evita NullPointerException verificando primero que objeto no sea null
if (objeto != null && objeto.algunMetodo()) {
    // Código seguro
}

// Evita división por cero
if (divisor != 0 && (numero / divisor > 10)) {
    // Operación segura
}

Operador OR lógico (||)

El operador || (OR lógico) devuelve true si al menos una de las expresiones que conecta es verdadera:

boolean condicion1 = true;
boolean condicion2 = false;

boolean resultado = condicion1 || condicion2;  // resultado contiene true

Al igual que &&, el operador || utiliza evaluación de cortocircuito. Si la primera expresión es true, la segunda no se evalúa, ya que el resultado final será true independientemente:

int edad = 25;
// La segunda parte no se evalúa porque la primera es true
if (edad >= 18 || tienePermisoDePadres()) {
    System.out.println("Puede acceder");
}

Este comportamiento se puede aprovechar para implementar valores predeterminados o alternativas:

String nombre = obtenerNombre() || "Invitado";  // Si obtenerNombre() devuelve null o vacío, usa "Invitado"

Operador NOT lógico (!)

El operador ! (NOT lógico) invierte el valor de una expresión booleana:

boolean activo = true;
boolean inactivo = !activo;  // inactivo contiene false

boolean resultado = !(5 > 3);  // resultado contiene false

Este operador es útil para negar condiciones o invertir resultados:

// Verificar si un número es impar
boolean esImpar = !(numero % 2 == 0);  // Alternativa a: numero % 2 != 0

// Verificar si una lista no está vacía
boolean tieneElementos = !lista.isEmpty();

Operador AND a nivel de bits (&)

Además del AND lógico (&&), Java proporciona un operador AND a nivel de bits (&) que también puede usarse con valores booleanos:

boolean a = true;
boolean b = false;
boolean resultado = a & b;  // resultado contiene false

La diferencia clave es que el operador & siempre evalúa ambas expresiones, incluso si la primera es false:

if (x > 10 & metodoConEfectosSecundarios()) {
    // metodoConEfectosSecundarios() siempre se ejecuta, incluso si x <= 10
}

Esto puede ser útil cuando se necesita que ambas expresiones se evalúen, por ejemplo, si la segunda expresión tiene efectos secundarios importantes.

Operador OR a nivel de bits (|)

De manera similar, el operador OR a nivel de bits (|) puede usarse con booleanos:

boolean a = true;
boolean b = false;
boolean resultado = a | b;  // resultado contiene true

Al igual que con &, el operador | siempre evalúa ambas expresiones, incluso si la primera es true:

if (edad >= 18 | registrarIntento()) {
    // registrarIntento() siempre se ejecuta, incluso si edad >= 18
}

Operador XOR lógico (^)

El operador ^ (XOR lógico o OR exclusivo) devuelve true si exactamente una de las expresiones es verdadera, pero no ambas:

boolean a = true;
boolean b = false;
boolean resultado1 = a ^ b;  // resultado1 contiene true

boolean c = true;
boolean d = true;
boolean resultado2 = c ^ d;  // resultado2 contiene false

Este operador es útil para situaciones donde se necesita verificar que solo una de dos condiciones sea verdadera:

// Verificar que exactamente una de las opciones esté seleccionada
boolean opcionValida = opcion1Seleccionada ^ opcion2Seleccionada;

// Alternar un valor booleano
bandera = bandera ^ true;  // Equivalente a: bandera = !bandera

Tabla de verdad de operadores lógicos

Para resumir el comportamiento de los operadores lógicos, aquí está la tabla de verdad:

A B A && B A || B !A A & B A | B A ^ B
true true true true false true true false
true false false true false false true true
false true false true true false true true
false false false false true false false false

Precedencia de operadores lógicos

Los operadores lógicos tienen diferentes niveles de precedencia:

1. ! (NOT lógico) - Mayor precedencia

2. & (AND a nivel de bits)

3. ^ (XOR lógico)

4. | (OR a nivel de bits)

5. && (AND lógico)

6. || (OR lógico) - Menor precedencia

Para evitar confusiones, se recomienda usar paréntesis para hacer explícito el orden de evaluación:

// Sin paréntesis (difícil de leer)
boolean resultado = a && b || c && d;

// Con paréntesis (más claro)
boolean resultadoClaro = (a && b) || (c && d);

Operadores lógicos en expresiones condicionales

Los operadores lógicos se utilizan mucho en estructuras de control condicionales:

// Verificar rango
int edad = 25;
if (edad >= 18 && edad <= 65) {
    System.out.println("Edad laboral");
}

// Condiciones alternativas
String rol = "editor";
if (rol.equals("admin") || rol.equals("editor")) {
    System.out.println("Puede modificar contenido");
}

// Negación de condición
boolean estaCompleto = true;
if (!estaCompleto) {
    System.out.println("Falta completar datos");
}

Operadores lógicos con expresiones complejas

Los operadores lógicos permiten construir condiciones extensas combinando múltiples expresiones:

// Validación de formulario
boolean formularioValido = !nombre.isEmpty() && 
                           email.contains("@") && 
                           (edad >= 18 || tienConsentimientoPaterno);

// Filtrado de datos
boolean cumpleCriterios = (precio >= precioMinimo && precio <= precioMaximo) &&
                          (categoria.equals("electrónica") || categoria.equals("informática")) &&
                          !agotado;

Operador ternario con operadores lógicos

El operador ternario (? :) se puede combinar con operadores lógicos para crear expresiones condicionales compactas:

// Determinar categoría de edad
String categoria = (edad < 18) ? "Menor" : 
                   (edad >= 65) ? "Jubilado" : "Adulto";

// Mensaje basado en múltiples condiciones
String mensaje = (puntuacion >= 90) ? "Excelente" :
                 (puntuacion >= 70) ? "Bueno" :
                 (puntuacion >= 50) ? "Aprobado" : "Suspenso";

Ejemplos prácticos

Veamos algunos ejemplos prácticos del uso de operadores lógicos:

// Validación de contraseña
boolean esContraseñaValida = contraseña.length() >= 8 &&  // Longitud mínima
                            contraseña.matches(".*[A-Z].*") &&  // Al menos una mayúscula
                            contraseña.matches(".*[0-9].*") &&  // Al menos un número
                            !contraseñasComunes.contains(contraseña);  // No es común

// Filtro de búsqueda
List<Producto> resultados = productos.stream()
    .filter(p -> (p.getPrecio() >= precioMin && p.getPrecio() <= precioMax) &&
                (categoriaSeleccionada.equals("todas") || p.getCategoria().equals(categoriaSeleccionada)) &&
                (!soloDisponibles || p.getStock() > 0))
    .collect(Collectors.toList());

// Lógica de juego
boolean puedeAvanzar = (jugador.tieneItem("llave") || jugador.tieneHabilidad("ganzúa")) &&
                       jugador.getSalud() > 0 &&
                       !zonaActual.equals("bloqueada");

Optimización de expresiones lógicas

El orden de las expresiones en una condición lógica puede afectar al rendimiento debido a la evaluación de cortocircuito:

// Menos eficiente: operación costosa siempre se evalúa
if (operacionCostosa() && condicionSimple) {
    // Código
}

// Más eficiente: operación costosa solo se evalúa si es necesario
if (condicionSimple && operacionCostosa()) {
    // Código
}

Algunas recomendaciones para optimizar expresiones lógicas:

  • Colocar las condiciones más propensas a fallar (o más rápidas de evaluar) primero en expresiones &&
  • Colocar las condiciones más propensas a ser verdaderas (o más rápidas de evaluar) primero en expresiones ||
  • Simplificar expresiones complejas aplicando leyes de la lógica booleana (como las leyes de De Morgan)
// Aplicando ley de De Morgan
// En lugar de: !(a && b)
boolean resultado = !a || !b;

// En lugar de: !(a || b)
boolean otroResultado = !a && !b;

Operadores lógicos con tipos no booleanos

En Java, a diferencia de algunos otros lenguajes como JavaScript o C, los operadores lógicos && y || solo funcionan con valores booleanos. No se puede usar expresiones como:

// Esto NO funciona en Java
int resultado = valor && 10;  // Error de compilación

Para lograr efectos similares, se deben usar expresiones condicionales explícitas:

// Equivalente al operador OR de "valor predeterminado" en otros lenguajes
String nombre = (nombreUsuario != null && !nombreUsuario.isEmpty()) ? nombreUsuario : "Invitado";

Operadores lógicos en programación funcional

En la programación funcional con Java, los operadores lógicos se pueden combinar con interfaces funcionales como Predicate:

// Combinación de predicados
Predicate<Producto> precioAlto = p -> p.getPrecio() > 100;
Predicate<Producto> enOferta = p -> p.getDescuento() > 0;

// AND lógico
Predicate<Producto> precioAltoYEnOferta = precioAlto.and(enOferta);

// OR lógico
Predicate<Producto> precioAltoOEnOferta = precioAlto.or(enOferta);

// NOT lógico
Predicate<Producto> noPrecioAlto = precioAlto.negate();

// Uso con streams
List<Producto> resultados = productos.stream()
    .filter(precioAltoOEnOferta)
    .collect(Collectors.toList());

Operadores de bits

Los operadores de bits en Java permiten manipular los bits individuales que componen un valor numérico. Estos operadores trabajan a nivel binario, tratando cada valor como una secuencia de bits (unos y ceros). Son particularmente útiles en programación de bajo nivel, optimización, criptografía, gráficos y cuando se necesita un control preciso sobre la representación binaria de los datos.

Representación binaria en Java

Antes de profundizar en los operadores, es importante entender cómo se representan los números en formato binario en Java:

  • Los tipos enteros (byte, short, int, long) se almacenan en complemento a dos
  • Un int ocupa 32 bits, un long ocupa 64 bits
  • El bit más significativo (el de la izquierda) representa el signo (0 para positivo, 1 para negativo)
// Representación de enteros en binario
int numero = 42;  // En binario: 00000000 00000000 00000000 00101010
System.out.println(Integer.toBinaryString(numero));  // Muestra "101010"

// Representación de números negativos (complemento a dos)
int negativo = -42;  // En binario: 11111111 11111111 11111111 11010110
System.out.println(Integer.toBinaryString(negativo));  // Muestra "11111111111111111111111111010110"

Operador AND a nivel de bits (&)

El operador & (AND a nivel de bits) compara cada par de bits en la misma posición y produce un 1 en el resultado solo si ambos bits son 1:

int a = 12;  // En binario: 00000000 00000000 00000000 00001100
int b = 25;  // En binario: 00000000 00000000 00000000 00011001
int resultado = a & b;  // resultado = 8 (binario: 00000000 00000000 00000000 00001000)

System.out.println("a & b = " + resultado);  // Muestra "a & b = 8"
System.out.println("Binario: " + Integer.toBinaryString(resultado));  // Muestra "1000"

El operador AND es útil para:

  • Enmascaramiento de bits: Aislar bits específicos de un valor
  • Verificar si un bit está activado: Comprobar si un bit específico es 1
  • Borrar bits específicos: Establecer bits específicos a 0
// Verificar si un número es par (el último bit es 0)
boolean esPar = (numero & 1) == 0;

// Extraer el byte menos significativo
int ultimoByte = numero & 0xFF;  // 0xFF = 255 = 11111111 en binario

// Verificar si un bit específico está activado (por ejemplo, el bit 3)
boolean bitActivado = (valor & (1 << 3)) != 0;

Operador OR a nivel de bits (|)

El operador | (OR a nivel de bits) compara cada par de bits y produce un 1 en el resultado si al menos uno de los bits es 1:

int a = 12;  // En binario: 00000000 00000000 00000000 00001100
int b = 25;  // En binario: 00000000 00000000 00000000 00011001
int resultado = a | b;  // resultado = 29 (binario: 00000000 00000000 00000000 00011101)

System.out.println("a | b = " + resultado);  // Muestra "a | b = 29"
System.out.println("Binario: " + Integer.toBinaryString(resultado));  // Muestra "11101"

El operador OR es útil para:

  • Combinar flags: Activar bits específicos
  • Unir conjuntos de bits: Combinar múltiples configuraciones
  • Establecer bits a 1: Activar bits específicos sin afectar a otros
// Definición de flags usando constantes
final int PERMISO_LECTURA = 0b100;  // 4 en decimal
final int PERMISO_ESCRITURA = 0b010;  // 2 en decimal
final int PERMISO_EJECUCION = 0b001;  // 1 en decimal

// Combinar permisos
int permisos = PERMISO_LECTURA | PERMISO_ESCRITURA;  // 6 en decimal (binario: 110)

// Añadir un permiso
permisos = permisos | PERMISO_EJECUCION;  // Ahora permisos = 7 (binario: 111)

Operador XOR a nivel de bits (^)

El operador ^ (XOR a nivel de bits o OR exclusivo) compara cada par de bits y produce un 1 en el resultado si los bits son diferentes (uno es 0 y el otro es 1):

int a = 12;  // En binario: 00000000 00000000 00000000 00001100
int b = 25;  // En binario: 00000000 00000000 00000000 00011001
int resultado = a ^ b;  // resultado = 21 (binario: 00000000 00000000 00000000 00010101)

System.out.println("a ^ b = " + resultado);  // Muestra "a ^ b = 21"
System.out.println("Binario: " + Integer.toBinaryString(resultado));  // Muestra "10101"

El operador XOR es útil para:

  • Alternar bits: Cambiar el estado de bits específicos
  • Cifrado simple: Operaciones de cifrado básicas
  • Intercambio de valores sin usar una variable temporal
  • Detección de bits cambiados entre dos valores
// Intercambio de valores sin variable temporal
a = a ^ b;
b = a ^ b;  // b ahora tiene el valor original de a
a = a ^ b;  // a ahora tiene el valor original de b

// Alternar un bit específico (por ejemplo, el bit 2)
valor = valor ^ (1 << 2);

// Cifrado XOR simple
byte[] datos = obtenerDatos();
byte clave = 0x3F;  // Clave de cifrado
for (int i = 0; i < datos.length; i++) {
    datos[i] = (byte)(datos[i] ^ clave);  // Cifrar/descifrar
}

Operador NOT a nivel de bits (~)

El operador ~ (NOT a nivel de bits o complemento) invierte todos los bits de un valor, convirtiendo los 0 en 1 y los 1 en 0:

int a = 12;  // En binario: 00000000 00000000 00000000 00001100
int resultado = ~a;  // resultado = -13 (binario: 11111111 11111111 11111111 11110011)

System.out.println("~a = " + resultado);  // Muestra "~a = -13"
System.out.println("Binario: " + Integer.toBinaryString(resultado));

El operador NOT es útil para:

  • Invertir bits: Cambiar todos los bits de un valor
  • Crear máscaras complementarias: Generar el complemento de una máscara
  • Calcular el complemento a uno de un número
// Eliminar un permiso específico
permisos = permisos & ~PERMISO_ESCRITURA;  // Desactiva el bit de escritura

// Crear una máscara para todos los bits excepto algunos específicos
int mascara = ~0b00001111;  // Máscara con 1s en todas las posiciones excepto los 4 bits menos significativos

Operadores de desplazamiento de bits

Java proporciona tres operadores de desplazamiento que mueven los bits a la izquierda o derecha:

Desplazamiento a la izquierda (<<)

El operador << desplaza los bits hacia la izquierda, rellenando con ceros por la derecha:

int a = 5;  // En binario: 00000000 00000000 00000000 00000101
int resultado = a << 2;  // resultado = 20 (binario: 00000000 00000000 00000000 00010100)

System.out.println("a << 2 = " + resultado);  // Muestra "a << 2 = 20"
System.out.println("Binario: " + Integer.toBinaryString(resultado));  // Muestra "10100"

Desplazar a la izquierda n posiciones es equivalente a multiplicar por 2^n:

int valor = 3;
int desplazado1 = valor << 1;  // 6 (equivalente a valor * 2)
int desplazado2 = valor << 3;  // 24 (equivalente a valor * 8)

Este operador es útil para:

  • Multiplicación rápida por potencias de 2
  • Crear valores con bits en posiciones específicas
  • Implementar algoritmos de hash
// Crear un valor con el bit n activado
int mascara = 1 << n;  // Activa solo el bit en la posición n

// Multiplicación eficiente
int resultado = valor * 8;  // Equivalente a: valor << 3

Desplazamiento a la derecha con signo (>>)

El operador >> desplaza los bits hacia la derecha, preservando el bit de signo (el bit más significativo):

int a = 20;  // En binario: 00000000 00000000 00000000 00010100
int resultado = a >> 2;  // resultado = 5 (binario: 00000000 00000000 00000000 00000101)

System.out.println("a >> 2 = " + resultado);  // Muestra "a >> 2 = 5"

// Con números negativos
int negativo = -20;  // En binario (complemento a dos): 11111111 11111111 11111111 11101100
int resultadoNeg = negativo >> 2;  // resultado = -5 (binario: 11111111 11111111 11111111 11111011)
System.out.println("negativo >> 2 = " + resultadoNeg);  // Muestra "negativo >> 2 = -5"

Desplazar a la derecha n posiciones es equivalente a dividir por 2^n (con truncamiento):

int valor = 20;
int desplazado1 = valor >> 1;  // 10 (equivalente a valor / 2)
int desplazado2 = valor >> 2;  // 5 (equivalente a valor / 4)

Este operador es útil para:

  • División rápida por potencias de 2
  • Extraer bits de posiciones específicas
  • Preservar el signo en operaciones de desplazamiento
// División eficiente
int resultado = valor / 4;  // Equivalente a: valor >> 2

// Extraer un bit específico
int bit3 = (valor >> 3) & 1;  // Extrae el bit en la posición 3

Desplazamiento a la derecha sin signo (>>>)

El operador >>> desplaza los bits hacia la derecha, rellenando siempre con ceros por la izquierda, independientemente del signo:

int a = 20;  // En binario: 00000000 00000000 00000000 00010100
int resultado = a >>> 2;  // resultado = 5 (binario: 00000000 00000000 00000000 00000101)

// Con números negativos (aquí está la diferencia con >>)
int negativo = -20;  // En binario: 11111111 11111111 11111111 11101100
int resultadoNeg = negativo >>> 2;  // resultado = 1073741819 (binario: 00111111 11111111 11111111 11111011)
System.out.println("negativo >>> 2 = " + resultadoNeg);

Este operador es útil para:

  • Tratar valores como sin signo
  • Implementar algoritmos criptográficos
  • Manipular bits sin preocuparse por el signo
// Convertir un int a su representación sin signo
long valorSinSigno = valor >>> 0;

// Implementar rotación de bits a la derecha
int rotarDerecha(int valor, int posiciones) {
    return (valor >>> posiciones) | (valor << (32 - posiciones));
}

Aplicaciones prácticas de los operadores de bits

Manipulación de flags y permisos

Los operadores de bits son ideales para trabajar con conjuntos de flags o permisos:

// Definición de constantes para permisos
public static final int NINGUNO = 0;          // 000000
public static final int LEER = 1;             // 000001
public static final int ESCRIBIR = 1 << 1;    // 000010
public static final int EJECUTAR = 1 << 2;    // 000100
public static final int ADMINISTRAR = 1 << 3; // 001000

// Asignar permisos
int permisos = LEER | ESCRIBIR;  // 000011

// Verificar si tiene un permiso específico
boolean puedeEjecutar = (permisos & EJECUTAR) != 0;  // false

// Añadir un permiso
permisos |= EJECUTAR;  // Ahora permisos = 000111

// Quitar un permiso
permisos &= ~ESCRIBIR;  // Ahora permisos = 000101

// Alternar un permiso
permisos ^= ADMINISTRAR;  // Activa ADMINISTRAR, ahora permisos = 001101
permisos ^= ADMINISTRAR;  // Desactiva ADMINISTRAR, ahora permisos = 000101

Manipulación de colores RGB

Los operadores de bits son útiles para trabajar con colores en formato RGB:

// Crear un color RGB (cada componente usa 8 bits)
int rojo = 255;    // 11111111
int verde = 128;   // 10000000
int azul = 64;     // 01000000

int colorRGB = (rojo << 16) | (verde << 8) | azul;  // 16711808 (0xFF8040)

// Extraer componentes
int componenteRojo = (colorRGB >> 16) & 0xFF;  // 255
int componenteVerde = (colorRGB >> 8) & 0xFF;  // 128
int componenteAzul = colorRGB & 0xFF;          // 64

// Modificar un componente (por ejemplo, aumentar el verde)
colorRGB = (colorRGB & ~(0xFF << 8)) | ((verde + 50) << 8);

Optimización de operaciones matemáticas

Los operadores de bits pueden optimizar ciertas operaciones matemáticas:

// Multiplicación y división por potencias de 2
int multiplicarPor8 = numero << 3;  // Más rápido que numero * 8
int dividirPor4 = numero >> 2;      // Más rápido que numero / 4

// Calcular el módulo cuando el divisor es una potencia de 2
int modulo8 = valor & 0b111;  // Equivalente a valor % 8, pero más rápido

// Redondeo a la potencia de 2 más cercana
int redondearAPotenciaDe2(int valor) {
    valor--;
    valor |= valor >> 1;
    valor |= valor >> 2;
    valor |= valor >> 4;
    valor |= valor >> 8;
    valor |= valor >> 16;
    return valor + 1;
}

Algoritmos de hash

Los operadores de bits son fundamentales en algoritmos de hash:

// Implementación simplificada de función hash
int calcularHash(String texto) {
    int hash = 0;
    for (int i = 0; i < texto.length(); i++) {
        hash = (hash << 5) - hash + texto.charAt(i);
    }
    return hash;
}

Compresión de datos

Los operadores de bits pueden usarse para técnicas básicas de compresión:

// Almacenar dos valores de 4 bits en un solo byte
byte comprimirDosValores(int valor1, int valor2) {
    return (byte)((valor1 & 0xF) << 4 | (valor2 & 0xF));
}

// Extraer los valores
int extraerPrimerValor(byte comprimido) {
    return (comprimido >> 4) & 0xF;
}

int extraerSegundoValor(byte comprimido) {
    return comprimido & 0xF;
}

Consideraciones importantes

Al trabajar con operadores de bits, hay que tener en cuenta algunas consideraciones:

  • Promoción de tipos: Las operaciones de bits en tipos más pequeños que int (como byte o short) producen resultados de tipo int
  • Precedencia de operadores: Los operadores de bits tienen precedencia diferente entre sí y con respecto a otros operadores
  • Portabilidad: El comportamiento de los operadores de bits puede variar en sistemas con diferentes representaciones de números
  • Legibilidad: El código que usa operadores de bits puede ser difícil de entender; es recomendable documentarlo bien
// Ejemplo de promoción de tipos
byte b1 = 10;
byte b2 = 20;
// byte resultado = b1 & b2;  // Error: el resultado es int
byte resultado = (byte)(b1 & b2);  // Correcto con casting explícito

Operadores de manipulación de objetos

Los operadores de manipulación de objetos en Java permiten trabajar con referencias a objetos, verificar tipos y crear nuevas instancias. Estos operadores son fundamentales para la programación orientada a objetos, ya que facilitan la interacción con objetos y la gestión de su ciclo de vida.

Operador de asignación de referencia (=)

El operador de asignación = cuando se utiliza con objetos, asigna una referencia al objeto, no copia el objeto en sí:

// Creación de un objeto
StringBuilder sb1 = new StringBuilder("Hola");

// Asignación de referencia (no crea un nuevo objeto)
StringBuilder sb2 = sb1;

// Modificar sb2 también afecta a sb1 porque ambos apuntan al mismo objeto
sb2.append(" mundo");
System.out.println(sb1);  // Muestra "Hola mundo"
System.out.println(sb2);  // Muestra "Hola mundo"

Es importante entender que las variables de objetos en Java contienen referencias (similar a punteros) a objetos en el heap, no los objetos en sí mismos:

// Dos referencias independientes a objetos String distintos
String str1 = new String("Java");
String str2 = new String("Java");

// Las referencias son diferentes aunque el contenido sea igual
System.out.println(str1 == str2);  // false (referencias diferentes)
System.out.println(str1.equals(str2));  // true (contenido igual)

Operador de creación de objetos (new)

El operador new crea una nueva instancia de una clase y devuelve una referencia a ella:

// Creación de objetos con new
Date fechaActual = new Date();  // Crea un objeto Date con la fecha actual
ArrayList<String> lista = new ArrayList<>();  // Crea una lista vacía
Scanner scanner = new Scanner(System.in);  // Crea un objeto Scanner

El operador new realiza varias acciones: 1. Asigna memoria para el nuevo objeto

2. Inicializa los campos del objeto con valores predeterminados

3. Llama al constructor especificado

4. Devuelve una referencia al objeto creado

// Diferentes constructores para la misma clase
StringBuilder sb1 = new StringBuilder();  // Constructor sin argumentos
StringBuilder sb2 = new StringBuilder("Texto inicial");  // Constructor con String
StringBuilder sb3 = new StringBuilder(50);  // Constructor con capacidad inicial

Operador de verificación de tipo (instanceof)

El operador instanceof verifica si un objeto es una instancia de un tipo específico (clase o interfaz), devolviendo un valor booleano:

Object obj = "Hola mundo";

// Verificar el tipo de un objeto
boolean esString = obj instanceof String;  // true
boolean esNumber = obj instanceof Number;  // false
boolean esComparable = obj instanceof Comparable;  // true (String implementa Comparable)

Este operador es útil para realizar comprobaciones de tipo seguras antes de realizar conversiones:

public void procesarObjeto(Object obj) {
    if (obj instanceof String) {
        String texto = (String) obj;  // Conversión segura
        System.out.println("Longitud del texto: " + texto.length());
    } else if (obj instanceof List) {
        List<?> lista = (List<?>) obj;  // Conversión segura
        System.out.println("Tamaño de la lista: " + lista.size());
    }
}

En Java moderno (desde Java 16), se puede combinar instanceof con una asignación de variable (pattern matching):

// Pattern matching con instanceof
if (obj instanceof String texto) {
    // La variable 'texto' ya está disponible y tipada como String
    System.out.println("Texto en mayúsculas: " + texto.toUpperCase());
} else if (obj instanceof List<?> lista && !lista.isEmpty()) {
    // 'lista' está disponible y se verifica que no esté vacía
    System.out.println("Primer elemento: " + lista.get(0));
}

Operador de conversión de tipo (cast)

El operador de conversión de tipo o cast permite convertir una referencia de un tipo a otro compatible:

Object objString = "Ejemplo de texto";

// Conversión de tipo (cast)
String texto = (String) objString;
System.out.println(texto.toUpperCase());  // EJEMPLO DE TEXTO

El cast es necesario cuando se quiere convertir una referencia de un tipo más general a uno más específico:

// Jerarquía: Object > Number > Integer
Object obj = Integer.valueOf(42);
Number num = (Number) obj;  // Cast de Object a Number
Integer entero = (Integer) num;  // Cast de Number a Integer

Si se intenta realizar un cast a un tipo incompatible, se lanza una ClassCastException en tiempo de ejecución:

Object objEntero = Integer.valueOf(100);
// String texto = (String) objEntero;  // Lanza ClassCastException

Para evitar estas excepciones, se recomienda usar instanceof antes de realizar el cast:

if (objEntero instanceof String) {
    String texto = (String) objEntero;
    // Procesar texto...
} else {
    System.out.println("El objeto no es un String");
}

Operador punto (.) para acceso a miembros

El operador punto (.) permite acceder a los miembros (campos y métodos) de un objeto:

String mensaje = "Hola mundo";

// Acceso a métodos
int longitud = mensaje.length();
String mayusculas = mensaje.toUpperCase();
boolean contieneMundo = mensaje.contains("mundo");

// Acceso encadenado
int primeraLetra = mensaje.trim().toLowerCase().charAt(0);

Este operador también se utiliza para acceder a miembros estáticos de una clase:

// Acceso a constantes estáticas
double pi = Math.PI;
int valorMaximo = Integer.MAX_VALUE;

// Acceso a métodos estáticos
double raizCuadrada = Math.sqrt(25);
String binario = Integer.toBinaryString(42);

Operador de acceso a array ([])

El operador de acceso a array ([]) permite acceder a elementos específicos de un array:

// Creación de un array
String[] nombres = {"Ana", "Carlos", "Elena", "David"};

// Acceso a elementos
String primerNombre = nombres[0];  // "Ana"
String ultimoNombre = nombres[nombres.length - 1];  // "David"

// Modificación de elementos
nombres[1] = "Carmen";

Este operador también se utiliza para crear arrays:

// Creación de arrays
int[] numeros = new int[5];  // Array de 5 enteros inicializados a 0
String[] palabras = new String[3];  // Array de 3 referencias a String inicializadas a null

Operador de referencia a método (::)

Desde Java 8, el operador de referencia a método (::) permite referenciar métodos o constructores sin invocarlos:

// Referencia a método estático
Function<String, Integer> convertidor = Integer::parseInt;
int numero = convertidor.apply("42");  // 42

// Referencia a método de instancia
String texto = "Hola mundo";
Predicate<String> verificador = texto::contains;
boolean contiene = verificador.test("mundo");  // true

// Referencia a constructor
Supplier<ArrayList<String>> creadorLista = ArrayList::new;
ArrayList<String> nuevaLista = creadorLista.get();

Este operador es útil en programación funcional y con streams:

List<String> nombres = Arrays.asList("Ana", "Carlos", "Elena", "David");

// Usando referencia a método
nombres.forEach(System.out::println);

// Ordenar usando referencia a método
nombres.sort(String::compareToIgnoreCase);

Operador ternario condicional (? :)

El operador ternario permite asignar valores a variables basándose en una condición:

// Asignación condicional
String mensaje = (edad >= 18) ? "Adulto" : "Menor";

// Equivalente a:
String mensaje;
if (edad >= 18) {
    mensaje = "Adulto";
} else {
    mensaje = "Menor";
}

Este operador también funciona con referencias a objetos:

// Obtener un objeto u otro basado en una condición
List<String> lista = condicion ? new ArrayList<>() : new LinkedList<>();

// Evitar NullPointerException
String texto = (cadena != null) ? cadena : "";
int longitud = (cadena != null) ? cadena.length() : 0;

Operador de resolución de ámbito (::) para constructores

El operador :: también se puede utilizar para referenciar constructores:

// Referencia a constructor sin argumentos
Supplier<StringBuilder> creadorSB = StringBuilder::new;
StringBuilder sb = creadorSB.get();

// Referencia a constructor con argumentos
Function<String, StringBuilder> creadorSBConTexto = StringBuilder::new;
StringBuilder sbConTexto = creadorSBConTexto.apply("Texto inicial");

// Referencia a constructor de array
Function<Integer, String[]> creadorArray = String[]::new;
String[] nuevoArray = creadorArray.apply(10);  // Array de 10 Strings

Ejemplos prácticos de manipulación de objetos

Clonación de objetos

Para crear copias independientes de objetos, se pueden utilizar varios enfoques:

// 1. Usando el método clone() (si la clase implementa Cloneable)
Date fecha1 = new Date();
Date fecha2 = (Date) fecha1.clone();  // Crea una copia independiente

// 2. Usando constructores de copia
ArrayList<String> lista1 = new ArrayList<>();
lista1.add("elemento");
ArrayList<String> lista2 = new ArrayList<>(lista1);  // Constructor de copia

// 3. Usando serialización (para clonación profunda)
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(objetoOriginal);
oos.flush();
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
ObjetoComplejo copia = (ObjetoComplejo) ois.readObject();

Comparación de objetos

Existen diferentes formas de comparar objetos en Java:

String str1 = "abc";
String str2 = "abc";
String str3 = new String("abc");

// 1. Comparación de referencias (==)
boolean mismaReferencia = (str1 == str2);  // true (debido al pool de strings)
boolean mismaReferencia2 = (str1 == str3);  // false (referencias diferentes)

// 2. Comparación de contenido (equals)
boolean mismoContenido = str1.equals(str3);  // true (mismo contenido)

// 3. Comparación de orden natural (compareTo)
String a = "abc";
String b = "def";
int resultado = a.compareTo(b);  // Negativo porque 'a' va antes que 'b'

Uso de instanceof con jerarquías de clases

El operador instanceof es útil para trabajar con jerarquías de clases:

// Jerarquía: Animal <- Mamifero <- Perro/Gato
public void procesarAnimal(Animal animal) {
    if (animal instanceof Mamifero) {
        System.out.println("Es un mamífero");
        
        if (animal instanceof Perro perro) {
            perro.ladrar();
        } else if (animal instanceof Gato gato) {
            gato.maullar();
        }
    }
}

Creación dinámica de objetos

Se pueden crear objetos dinámicamente usando reflection:

// Crear un objeto por su nombre de clase
try {
    Class<?> clase = Class.forName("java.util.ArrayList");
    Object objeto = clase.getDeclaredConstructor().newInstance();
    
    // Verificar el tipo
    if (objeto instanceof List) {
        List<?> lista = (List<?>) objeto;
        System.out.println("Se creó una lista vacía");
    }
} catch (Exception e) {
    e.printStackTrace();
}

Encadenamiento de métodos (method chaining)

El encadenamiento de métodos permite operaciones fluidas sobre objetos:

// Sin encadenamiento
StringBuilder sb = new StringBuilder();
sb.append("Hola");
sb.append(" ");
sb.append("mundo");
String resultado = sb.toString();

// Con encadenamiento
String resultadoEncadenado = new StringBuilder()
    .append("Hola")
    .append(" ")
    .append("mundo")
    .toString();

Consideraciones sobre rendimiento y buenas prácticas

Evitar comparaciones innecesarias con null

// Enfoque menos eficiente
if (objeto != null) {
    if (objeto instanceof String) {
        // Procesar String
    }
}

// Más eficiente (instanceof ya maneja null)
if (objeto instanceof String) {
    // Procesar String
}

Minimizar el uso de cast explícito

// Evitar múltiples casts
if (obj instanceof List) {
    List<?> lista = (List<?>) obj;
    if (!lista.isEmpty() && lista.get(0) instanceof String) {
        String primerElemento = (String) lista.get(0);
        // Procesar...
    }
}

// Mejor con pattern matching
if (obj instanceof List<?> lista && 
    !lista.isEmpty() && 
    lista.get(0) instanceof String primerElemento) {
    // Procesar directamente con primerElemento...
}

Preferir interfaces sobre implementaciones concretas

// Menos flexible
ArrayList<String> lista = new ArrayList<>();

// Más flexible (programar hacia interfaces)
List<String> lista = new ArrayList<>();

Evitar la creación innecesaria de objetos

// Ineficiente (crea muchos objetos String)
String resultado = "";
for (int i = 0; i < 1000; i++) {
    resultado += i;
}

// Más eficiente
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append(i);
}
String resultado = sb.toString();

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende Java online

Ejercicios de esta lección Operadores

Evalúa tus conocimientos de esta lección Operadores con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Clases abstractas

Test

Streams: reduce()

Test

Streams: flatMap()

Test

Llamada y sobrecarga de funciones

Puzzle

Métodos referenciados

Test

Métodos de la clase String

Código

Representación de Fecha

Puzzle

Operadores lógicos

Test

Tipos de datos

Código

Estructuras de iteración

Puzzle

Streams: forEach()

Test

Objetos

Puzzle

Funciones lambda

Test

Uso de Scanner

Puzzle

CRUD en Java de modelo Customer sobre un ArrayList

Proyecto

Tipos de variables

Puzzle

Streams: collect()

Puzzle

Operadores aritméticos

Puzzle

Interfaz funcional Consumer

Test

API java.nio 2

Puzzle

API Optional

Test

Interfaz funcional Function

Test

Encapsulación

Test

Interfaces

Código

Uso de API Optional

Puzzle

Representación de Hora

Test

Herencia básica

Test

Clases y objetos

Código

Interfaz funcional Supplier

Puzzle

HashMap

Puzzle

Sobrecarga de métodos

Test

Polimorfismo de tiempo de ejecución

Puzzle

OOP en Java

Proyecto

Creación de Streams

Test

Streams: min max

Puzzle

Métodos avanzados de la clase String

Puzzle

Polimorfismo de tiempo de compilación

Test

Excepciones

Puzzle

Herencia avanzada

Puzzle

Estructuras de selección

Test

Uso de interfaces

Test

HashSet

Test

Objeto Scanner

Test

Streams: filter()

Puzzle

Operaciones de Streams

Puzzle

Interfaz funcional Predicate

Puzzle

Streams: sorted()

Test

Configuración de entorno

Test

CRUD en Java de modelo Customer sobre un HashMap

Proyecto

Uso de variables

Test

Clases

Test

Streams: distinct()

Puzzle

Streams: count()

Test

ArrayList

Test

Datos de referencia

Test

Interfaces funcionales

Puzzle

Métodos básicos de la clase String

Test

Instalación

Test

Funciones

Código

Estructuras de control

Código

Herencia de clases

Código

Streams: map()

Puzzle

Funciones y encapsulamiento

Test

Streams: match

Test

Gestión de errores y excepciones

Código

Datos primitivos

Puzzle

Todas las lecciones de Java

Accede a todas las lecciones de Java y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Instalación De Java

Introducción Y Entorno

Configuración De Entorno Java

Introducción Y Entorno

Ecosistema Jakarta Ee De Java

Introducción Y Entorno

Tipos De Datos

Sintaxis

Variables

Sintaxis

Operadores

Sintaxis

Estructuras De Control

Sintaxis

Funciones

Sintaxis

Excepciones

Programación Orientada A Objetos

Clases Y Objetos

Programación Orientada A Objetos

Encapsulación

Programación Orientada A Objetos

Herencia

Programación Orientada A Objetos

Clases Abstractas

Programación Orientada A Objetos

Interfaces

Programación Orientada A Objetos

Sobrecarga De Métodos

Programación Orientada A Objetos

Polimorfismo

Programación Orientada A Objetos

La Clase Scanner

Programación Orientada A Objetos

Métodos De La Clase String

Programación Orientada A Objetos

Listas

Framework Collections

Conjuntos

Framework Collections

Mapas

Framework Collections

Funciones Lambda

Programación Funcional

Interfaz Funcional Consumer

Programación Funcional

Interfaz Funcional Predicate

Programación Funcional

Interfaz Funcional Supplier

Programación Funcional

Interfaz Funcional Function

Programación Funcional

Métodos Referenciados

Programación Funcional

Creación De Streams

Programación Funcional

Operaciones Intermedias Con Streams: Map()

Programación Funcional

Operaciones Intermedias Con Streams: Filter()

Programación Funcional

Operaciones Intermedias Con Streams: Distinct()

Programación Funcional

Operaciones Finales Con Streams: Collect()

Programación Funcional

Operaciones Finales Con Streams: Min Max

Programación Funcional

Operaciones Intermedias Con Streams: Flatmap()

Programación Funcional

Operaciones Intermedias Con Streams: Sorted()

Programación Funcional

Operaciones Finales Con Streams: Reduce()

Programación Funcional

Operaciones Finales Con Streams: Foreach()

Programación Funcional

Operaciones Finales Con Streams: Count()

Programación Funcional

Operaciones Finales Con Streams: Match

Programación Funcional

Api Optional

Programación Funcional

Api Java.nio 2

Entrada Y Salida (Io)

Api Java.time

Api Java.time

Accede GRATIS a Java y certifícate

Certificados de superación de Java

Supera todos los ejercicios de programación del curso de Java y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender el uso de operadores aritméticos básicos
  • Aplicar operadores de incremento y decremento en Java
  • Utilizar operadores compuestos para simplificar el código
  • Gestionar precedencia de operadores y evitar desbordamientos