UT02 - Introducción a Java

Conexión legislativa

En esta Unidad de Trabajo nos centraremos en PROG-CE-01.d, PROG-CE-01.e, PROG-CE-01.f, PROG-CE-01.g, PROG-CE-01.h, PROG-CE-01.i:


Introducción

Java es un lenguaje de programación de alto nivel, orientado a objetos y de propósito general. Una de sus características principales es su modelo de ejecución híbrido: el código fuente no se traduce directamente a código máquina, sino a un formato intermedio denominado bytecode (.class), el cual es ejecutado por la Máquina Virtual de Java (JVM). Esto permite que el mismo programa funcione en cualquier sistema operativo sin necesidad de recompilarlo ("Write Once, Run Anywhere"). De esta forma, combina la portabilidad y flexibilidad de los lenguajes interpretados con el alto rendimiento y la detección temprana de errores de los lenguajes compilados.

La sintaxis de Java está fuertemente inspirada en C y C++. Sin embargo, elimina elementos complejos y propensos a errores, como el manejo directo de punteros o la gestión manual de memoria, delegando esta última al Recolector de Basura (garbage collector).

Se trata de un lenguaje con tipado fuerte y estático:


El programa principal

A continuación se muestra un ejemplo de un programa muy sencillo escrito en Java:

package miPrimerPrograma;

public class HolaMundo {

    public static void main(String[] args) {
        // Código
    }
    
}

Al empezar a programar nos encontraremos con una serie de conceptos que no se terminan de entender hasta más adelante. En este fragmento de código tan pequeño tenemos: la declaración de un paquete, la creación de una clase, la definición de un método, niveles de visibilidad de clase y de método, un modificador de método, un tipo de método, palabras reservadas, paso de argumentos...

De momento no es necesario profundizar en el significado técnico de cada uno de estos conceptos. Es suficiente con saber que replicaremos una y otra vez esta estructura, modificando // Código con un conjunto de instrucciones, cada vez más rico y complejo, que iremos aprendiendo durante el curso.

A modo de resumen práctico, basta con recordar las siguientes reglas sintácticas indispensables:

  1. El nombre del archivo: debe coincidir exactamente con el nombre de la clase pública (HolaMundo.java), respetando estrictamente las mayúsculas y minúsculas (case-sensitive).
  2. El punto de entrada: la JVM buscará siempre el método public static void main(String[] args) para iniciar la ejecución. Todo lo que queramos que ocurra al arrancar el programa deberá estar dentro de sus llaves.
  3. Bloques de código: se delimitan mediante llaves {}. Cada apertura debe tener su correspondiente cierre.
  4. Fin de sentencia: cada instrucción individual debe finalizar obligatoriamente con un punto y coma ;. Su omisión provocará un error de compilación.

Palabras reservadas

Java tiene una lista muy extensa de palabras reservadas. Las palabras reservadas son términos forman parte del propio lenguaje y que tienen un significado especial y predefinido para el compilador. Debido a esto, no pueden ser utilizadas como identificadores. Entre las palabras reservadas más comunes, tenemos:
- Tipos de datos primitivos: boolean, byte, short, int, long, float, double, char.
- Estructura del programa: class, package, import, public, private, protected, static, void.
- Control de flujo: if, else, switch, case, default, for, while, do, break, continue, return.

Además, existen 3 valores literales que, sin formar parte del conjunto de palabras reservadas, tampoco pueden utilizarse como identificador, ya que tienen un significado especial predefinido en el lenguaje: true, false y null.


Variables

Las variables son representaciones, mediante identificadores (nombres), de valores que pueden cambiar durante la ejecución de un programa. Esos valores se asignan mediante el operador de asignación (=). Un ejemplo puede ser:

n = 42

En este caso, el identificador de la variable es a y su valor es 42. Este valor puede cambiar durante la ejecución del programa:

n = 42
a = 7
n = n / a

Es fundamental utilizar variables si queremos "guardar" valores para utilizarlos más adelante. Si solamente hiciéramos operaciones con los datos sin asignar los resultados a las variables, estos se perderían. Por ejemplo:

6 * 7

El resultado de esta operación se perderá para siempre una vez termine su ejecución. Por tanto, debemos guardarlo en algún sitio.

A nivel de arquitectura del computador, las variables no son más que espacios reservados en la memoria RAM donde se almacenan datos durante la ejecución del programa. A diferencia de lenguajes como Python o JavaScript, Java es un lenguaje de tipado estático y fuerte, y toda variable debe ser declarada obligatoriamente con un tipo de dato antes de ser utilizada. El tipo de dato define qué clase de información puede almacenar y cuánta memoria ocupará.

ut02-variable-ram.webp500
Fuente: dev.to

Debido a esto, cuantas más variables utilicemos en nuestro programa, más memoria RAM consumirá. Por esta razón, es fundamental elegir adecuadamente el tipo de dato para cada variable, optimizando tanto el consumo de memoria como la precisión de las operaciones.

La creación de variables en Java consta de 2 fases: declaración e inicialización.

  1. Durante la declaración se le indica al compilador el tipo de dato y el identificador de la variable. En ese momento se reserva el espacio correspondiente en la memoria RAM, aunque la variable no contenga aún un valor útil asignado. Pueden declararse varias variables al mismo tiempo separándolas con comas ,.
  2. En la inicialización se le asigna un valor a la variable mediante el operador de asignación.
Ejemplo de uso de variables

Aunque aún no se hayan introducido los tipos de datos, conviene ir poniendo algún ejemplo concreto con la sintaxis de Java.

int n;       // Declaración
n = 42;      // Inicialización
int m, p;    // Declaración de 2 variables

Las variables también pueden inicializarse en el momento de la declaración:

int n = 42;  // Declaración e inicialización

La palabra int es el tipo de dato, y n es el identificador.


Constantes

Las constantes son un caso especial de variables cuyo valor permanece inmutable una vez se le asigna. Cualquier dato que no vaya a cambiar nunca es susceptible a guardarse en una constante. Algunos ejemplos son el número pi (3.141592...) , el código de "recurso no encontrado" en un servidor (404), etc.

La declaración de una constante es similar a la de las variables, pero se le añade la palabra reservada final;

final tipo nombreConstante;
final int MAYORIA_EDAD = 18;

También puede declararse primero y asignarle un valor después.

final int MAYORIA_EDAD;
MAYORIA_EDAD = 18;

En cualquier caso, luego no podrá modificarse su valor:

final int MAYORIA_EDAD;
MAYORIA_EDAD = 18;
MAYORIA_EDAD = 21; // ERROR

Identificadores

El identificador es el nombre que le damos a un elemento del programa. Hasta ahora sólo hemos hablado de los identificadores para nombrar variables, pero más adelante veremos que también se usan para nombrar paquetes, clases y métodos.

En Java, para que un identificador sea válido desde el punto de vista del compilador, debe cumplir estrictamente las siguientes reglas:

Ejemplos de identificadores

  • Válidos: edadUsuario, total_suma, $precio, nota1, _contador.
  • Inválidos: 1nota (empieza por un número), total-suma (el guión medio no está permitido), class (palabra reservada), precio final (no se permiten espacios)

Convenios de nombres

Aunque la sintaxis de Java permite nombres como $_, existe un estándar de facto en la comunidad de desarrollo. Seguir estos convenios no es estrictamente obligatorio para que el programa compile, pero sí facilita mucho la legibilidad, coherencia y profesionalidad del código fuente.

Según el elemento del programa que se esté nombrando, existen las siguientes convenciones:

Además de todo esto, conviene elegir nombres claros, expresivos y representativos. Es preferible utilizar un nombre descriptivo como precioProductoConIVA frente a otros nombres genéricos como p, x o variable1.


Tipos de datos

En términos generales, los tipos de datos se dividen en 2 grandes categorías según cómo se almacenan en memoria y manipulan en código:

  1. Tipos primitivos: almacenan directamente el valor puro en la memoria RAM. Son elementos básicos, integrados en el lenguaje.
  2. Tipos de referencia (u objetos): almacenan en memoria RAM la dirección de memoria (referencia) en la que se ubica realmente el objeto. Incluyen clases (como String), interfaces, arrays y enumerados.
Referencia y objeto

Este tipo de dato lo veremos más adelante, así como la diferencia entre la zona de RAM donde se almacenan las referencias (el Stack) frente a donde se almacenan los objetos (el Heap).

Tipos primitivos

Como ya se ha mencionado, Java es un lenguaje de tipado estático y fuerte. El compilador necesita conocer con exactitud la naturaleza de los datos que se van a procesar porque, en el momento de la compilación, debe asegurarse de que todas las operaciones realizadas sobre esa variable sean compatibles con su tipo, así como reservar el espacio de memoria RAM correspondiente.

No todos los tipos primitivos ocupan el mismo espacio en la memoria RAM, y es importante conocer tanto su tamaño como los rangos de valores para usarlos de manera eficiente según las necesidades del programa.

Un tipo de dato primitivo define 3 aspectos fundamentales:

Categoría Tipo Tamaño (bits) Rango de valores Valor por defecto
Enteros byte 8 [−128,127] 0
short 16 [−32768,32767] 0
int 32 [−2147483648,2147483647] 0
long 64 [−263,263−1] 0L
Decimales float 32 [±1.4⋅10−45,±3.4⋅1038] (6-7 dígitos de precisión) 0f
double 64 [±4.9⋅10−324,±1.8⋅10308] (15 dígitos de precisión) 0.0d
Carácter char 16 Carácter Unicode. [0,65535] ('\u0000' a '\uffff') '\u0000'
Booleano boolean 1 true o false false

Aunque boolean solamente necesite 1 bit para representar todo su rango de valores, normalmente el compilador reserva 1 byte entero debido a cómo funciona la alineación de memoria en la arquitectura de los procesadores modernos.

Representación de la información

En otra sección, estamos trabajando en ello

Desbordamiento de memoria

El compilador de Java conoce los rangos de valores para cada tipo y detectará, en el momento de las asignaciones a variables, cuando nos salgamos de ese rango para que elijamos otro tipo de dato:

byte byteGrande = 128;
Type mismatch

Esto provoca un error de compilación porque "no se puede convertir de int a byte". Aún así, en este caso nos valdría con utilizar short.

Sin embargo, el compilador no siempre puede predecir si una operación concreta va a provocar que una variable se salga de su rango de valores durante la ejecución del programa. Cuando el resultado de una operación aritmética supera el límite máximo representable por un tipo de dato, en lugar de "explotar" (las operaciones aritméticas no provocan excepciones), se produce un fenómeno conocido como desbordamiento (overflow). Si el resultado cae por debajo del límite mínimo, se denomina subdesbordamiento (underflow).

// Overflow
byte byteGrande = 127;
byteGrande++;

// Underflow
byte bytePequenyo = -128
bytePequenyo--;

El valor de byteGrande pasará a ser -128, y el valor de bytePequenyo pasará a ser 127. Esto sucede por la manera en que se opera a nivel binario. Sin entrar en muchos detalles:

Esta es otra de las razones por las que es indispensable elegir tipos de datos adecuados.

Literales

Un literal es la representación sintáctica directa y explícita de un valor fijo dentro del código fuente. A diferencia de las variables, cuyo valor varía durante la ejecución, el valor de un literal es constante y conocido desde el momento de la compilación.

25;       // 25 es un literal entero
19.99;    // 19.99 es un literal decimal
'A';      // 'A' es un literal de carácter
true;     // true es un literal booleano

Tipo objeto

Los tipos objeto nos permiten hacer mucho más que simplemente aplicar operaciones aritméticas (aunque, por debajo, todo se reduce a este tipo de operaciones). Su valor por defecto es null, que significa que es "una referencia que no apunta a nada en memoria".

Como se ha comentado, estos los trabajaremos más adelante, aunque sí que hay uno concreto que, por motivos pedagógicos, conviene adelantar: String.

El tipo de dato String

El tipo String en Java representa una secuencia de caracteres. A diferencia de los tipos de datos primitivos (como int o char), String no es un tipo primitivo, sino un tipo objeto (de referencia) perteneciente a la clase java.lang.String.

Sin embargo, debido a su uso masivo en prácticamente cualquier programa, Java otorga a String un trato especial por parte del compilador, dotándolo de facilidades sintácticas que lo hacen parecer un primitivo sin dejar de ser un objeto.

Creación e inmutabilidad de las cadenas

A diferencia de otros objetos que requieren obligatoriamente la palabra clave new, un String se puede crear directamente mediante literales encerrados entre comillas dobles ("...").

String nombre = "Juan"; // Creación mediante literal
String mensaje = new String("Hola"); // Creación explícita (desaconsejada generalmente)
La propiedad clave: Inmutabilidad

En Java, los objetos de la clase String son inmutables. Una vez que un objeto String se crea en la memoria RAM, su contenido no puede modificarse jamás.

Cuando realizas una operación que "modifica" una cadena, la JVM no altera el valor original, sino que crea un nuevo objeto String en memoria con el resultado y actualiza la variable para que apunte a la nueva dirección.

String texto = "Hola";
texto = texto + " Mundo"; // No modifica "Hola". Crea un nuevo String "Hola Mundo" en memoria RAM.

Concatenación, operador +

Java aprovecha el operador + para facilitar la concatenación de cadenas al utilizar el tipo String. Si al menos uno de los operandos de una suma es una cadena de texto, el compilador convertirá automáticamente el resto de operandos a String y unirá las cadenas.

String usuario = "Ignacio";
int edad = 34;

// El número entero 34 se convierte implícitamente a "34"
String resultado = usuario + " tiene " + edad + " años."; 
// Resultado: "Ignacio tiene 34 años."
Cuidado con el orden en las expresiones

El operador + se evalúa de izquierda a derecha. Esto puede causar resultados inesperados si se mezclan sumas aritméticas con concatenación de cadenas

System.out.println("Resultado: " + 5 + 5); // Imprime: "Resultado: 55" (Concatena 5 y luego 5)
System.out.println("Resultado: " + (5 + 5)); // Imprime: "Resultado: 10" (Suma entre paréntesis primero)
System.out.println(5 + 5 + " es el resultado"); // Imprime: "10 es el resultado" (Suma 5+5=10 primero)

Entrada y salida de datos

Para entender bien estos conceptos (o la manera en la que funcionan) necesitamos avanzar bastante pero, una vez más, por motivos pedagógicos, es necesario adelantarlos.

Para interactuar con el usuario a través de la consola, cualquier programa necesita 2 mecanismos básicos: mostrar información en pantalla (salida de datos) y recibir información desde el teclado (entrada de datos).

Salida de datos por consola

En Java, la salida estándar por consola se gestiona mediante el objeto System.out, y dispone de 3 métodos principales para ello, según el formato que le queramos dar a la información que queremos mostrar.

System.out.println()

Muestra el mensaje en consola y añade automáticamente un salto de línea al final.

System.out.println("Hola, mundo");
System.out.println("Este mensaje aparece en una nueva línea");

Imprimirá:

Hola, mundo
Este mensaje aparece en una nueva línea
Atajos en IDEs

Esta instrucción de salida de datos es la más utilizada, y la mayoría de IDEs tienen un atajo predefinido para escribirla: syso o sysout.
ut02-atajo-syso.png

System.out.print()

Muestra el mensaje en consola sin añadir salto de línea.

System.out.print("Hola, mundo");
System.out.println("...");

Imprimirá:

Hola, mundo...

System.out.printf()

Este es algo más complejo, lo estudiaremos más adelante.

Entrada de datos por consola

Para leer lo que el usuario escribe en el teclado, la forma más sencilla en este punto es utilizar la clase java.util.Scanner.

Puesto que Scanner no forma parte del paquete por defecto (java.lang), es obligatorio importarlo al inicio del archivo fuente, justo debajo de la línea que indica el paquete y encima del nombre de la clase.

package es.daw.ut02;

import java.util.Scanner;

public class LecturaTeclado {
	...
IDEs al rescate

Cuando necesitemos utilizar algo que no forme parte del paquete por defecto, los IDEs nos ofrecerán asistencia para añadir automáticamente la línea de importación, e incluso nos sugerirán alternativas a lo que estamos buscando.
ut02-ide-importar.png
Al pulsar Enter, el IDE automáticamente añadirá la línea de importación donde corresponde.

La importancia de los paquetes

Ahora mismo no es un problema, pero más adelante trabajaremos con algunos elementos "polisémicos": clases que comparten exactamente el mismo nombre pero pertenecen a paquetes distintos y tienen comportamientos diferentes. Un ejemplo clásico es Date: existe en java.util.Date y en java.sql.Date. Si nos limitamos a aceptar la sugerencia del IDE sin fijarnos en el paquete al que pertenece, podemos importar la clase incorrecta y provocar errores de compilación o de lógica inesperados.
ut02-ide-importar-date.png

Una vez importemos Scanner, necesitamos crear una instancia. De nuevo, esto es un concepto más complejo, pero es algo así como "asignar un valor a una variable". Para ello necesitamos indicarle el "origen de los datos" que, en este caso, es System.in.

Scanner sc = new Scanner(System.in);

A continuación, cada vez que queramos pedirle datos al usuario, sólo tendremos que hacer lo siguiente:

De momento debemos quedarnos con esta "fórmula mágica", más adelante explicaremos mejor lo que está pasando detrás del telón.

Liberación de recursos

Una vez que no necesitemos leer más datos, es una buena práctica llamar a sc.close() para liberar los recursos del sistema operativo asociados al flujo de entrada y evitar fugas de recursos (resource leaks).

Peligro con System.in

Al cerrar un Scanner vinculado a System.in, se cierra el flujo de la consola para toda la aplicación. Si intentas volver a leer del teclado más adelante con un nuevo Scanner, el programa fallará. Por ello, en aplicaciones de consola se suele mantener un único Scanner abierto durante todo el programa y se cierra justo al finalizar.

De esta manera, una interacción típica con el usuario podría ser la siguiente:

Scanner sc = new Scanner(System.in);

System.out.println("Hola, usuario, ¿cómo te llamas?");
String nombre = sc.nextLine();

System.out.println("Bienvenido " + nombre + ", cierra antes de terminar.");
sc.close();
Operaciones Síncronas y Bloqueantes

La lectura de datos con Scanner es una operación síncrona y bloqueante.

  • Bloqueante: la ejecución del programa se congela por completo en la línea donde se invoca el método de lectura (nextLine(), nextInt(), etc.) y no continuará con la siguiente línea de código hasta que el usuario escriba en la consola y pulse la tecla Enter.
  • Síncrona: sigue un flujo secuencial estricto y predecible, por lo que el programador sabe exactamente en qué punto del flujo de control se detendrá la ejecución y en qué momento se dispondrá del dato para operar con él.

Más adelante estudiaremos la ejecución asíncrona, donde el programa continúa funcionando sin detenerse a esperar la respuesta del usuario o de un recurso externo.


Expresiones

Una expresión es una combinación de variables, constantes, literales, operadores y llamadas a métodos que el compilador evalúa siguiendo reglas sintácticas y de precedencia para producir y devolver un único valor.

Los elementos que componen una expresión son:

Según la naturaleza del dato resultante tras su evaluación, podemos clasificar las expresiones en:

Operaciones básicas

Java dispone de multitud de operadores con los que se pueden crear expresiones utilizando como operandos variables, constantes, literales y otras expresiones

Asignación

=

La operación más básica que hay en Java, y que se utiliza de manera indispensable para la asignación de valores a variables.

Para realizar esta operación utilizamos el operador de asignación (=). La sintaxis básica exige que en el lado izquierdo de la igualdad se ubique la variable donde se desea almacenar el dato, y en el lado derecho la expresión o valor a evaluar: $$variable = expresión;$$

byte edad;
edad = 25; // Asignación de un literal

int a = 10;
int b = a; // Asignación del valor de otra variable
Evaluación de derecha a izquierda

La expresión situada a la derecha se evalúa en su totalidad antes de guardar el resultado en la variable de la izquierda. Esto permite hacer modificaciones sobre la propia variable.

int contador = 5;
contador = contador + 1;

La parte de la derecha (la expresión) se evalúa también en cierto orden, respetando la precedencia de los operadores, que veremos más adelante.

Asignaciones encadenadas

El operador de asignación devuelve el propio valor que acaba de ser asignado. Gracias a esta característica es posible encadenar múltiples asignaciones dentro de una misma sentencia.

int x, y, z;
x = y = z = 100;

El valor viaja desde el extremo derecho hacia la variable más a la izquierda.

¿Sería esto correcto?

int x, y, z;
x = y + 10 = z = 100;

Hay otros operadores de asignación avanzados, pero primero conviene ver los operadores aritméticos.

Operadores aritméticos

+, -, *, /, %, ++, --

Los operadores aritméticos se pueden dividir en 2 categorías: binarios y unarios. Los binarios requieren de 2 operandos. Los unarios requieren solamente de 1.

Operadores aritméticos binarios

Requieren de 2 operandos y corresponden a las operaciones matemáticas estándar sobre datos numéricos o el tipo char.

Operador Nombre Ejemplo Descripción
+ Suma a + b Suma 2 valores numéricos
- Resta a - b Resta el operando derecho al izquierdo
* Multiplicación a * b Multiplica 2 operandos
/ División a / b Devuelve el cociente de la división
% Módulo (resto) a % b Devuelve el resto de la división
int a = 10;
int b = 3;

int suma = a + b;     // 13
int resta = a - b;    // 7
int producto = a * b; // 30
int cociente = a / b; // 3 (División entera: descarta los decimales)
int resto = a % b;    // 1 (Resto de 10 / 3)
División entera frente a división real

Si ambos operandos de una división son de tipo entero, Java ejecuta una división entera y trunca completamente la parte decimal (sin redondear). Para lograr una división con decimales, necesitamos que al menos uno de los operandos sea un número decimal.

System.out.println(5 / 2);  // Imprime 2
System.out.println(5 / 2f); // Imprime 2.5

Operadores aritméticos unarios

Actúan sobre un único operando para modificar su signo o alterar su valor en 1.

Operador Nombre Ejemplo Descripción
+ Más unario +a Confirma el signo positivo (implícito y redundante)
- Menos unario -a Invierte el signo del valor numérico
++ Incremento a++/++a Suma 1 al valor actual de la variable
-- Decremento a--/--a Resta 1 al valor actual de la variable

Notación prefija frente a postfija (++a vs a++)

La posición de los operadores de incremento o decremento respecto a la variable cambia radicalmente el orden en que se evalúa la expresión:

int x = 5;
int y = ++x; // Primero incrementa x a 6, luego asigna el valor 6 a y
System.out.println("x: " + x + ", y: " + y); // Imprime: x: 6, y: 6

int a = 5;
int b = a++; // Primero asigna el valor actual de a (5) a b, y LUEGO incrementa a a 6
System.out.println("a: " + a + ", b: " + b); // Imprime: a: 6, b: 5

Asignación compuesta

+=, -=, *=, /=, %=

Ahora que hemos visto los operadores aritméticos, podemos ver los operadores de asignación compuesta (o combinada).

Es muy habitual necesitar modificar el valor de una variable operando sobre su propio valor (por ejemplo, para actualizar un contador o un total: puntos = puntos + 10). Para simplificar esta sintaxis, Java combina en un único operador la operación aritmética y la asignación:

Operador Ejemplo Equivalencia
+= puntos += 10 puntos = puntos + 10
-= stock -= 1 stock = stock - 1
*= precio *= 1.21 precio = precio * 1.21
/= total /= 2 total = total / 2
%= resto %= 2 resto = resto % 2
int total = 100;

total += 50;  // Equivalente a: total = total + 50;  (total pasa a ser 150)
total *= 2;   // Equivalente a: total = total * 2;   (total pasa a ser 300)
total -= 100; // Equivalente a: total = total - 100; (total pasa a ser 200)

Operadores relacionales

==, !=, >, <, >=, <=

Los operadores relacionales (o de comparación) se utilizan para evaluar la relación entre dos operandos. El resultado siempre, SIEMPRE, SIEMPRE es un valor booleano: true o false.

Operador Nombre Ejemplo Condición para devolver true
== Igual a a == b a y b tienen que tener exactamente el mismo valor
!= Distinto de a != b a y b tienen valores diferentes
> Mayor que a > b a es estrictamente mayor que b
< Menor que a < b a es estrictamente menor que b
>= Mayor o igual que a >= b a es mayor o igual que b
<= Menor o igual que a <= b a es menor o igual que b
int x = 10;
int y = 20;

boolean esMayor = x > y;         // false
boolean esDiferente = x != y;    // true
boolean esIgual = (x + 10) == y; // true (10 + 10 es igual a 20)

Operadores lógicos

&&, ||, !

Los operadores lógicos (o booleanos) permiten combinar o invertir condiciones evaluadas con operandos booleanos (true o false). Son fundamentales para construir expresiones complejas en estructuras condicionales y bucles.

Operador Nombre Expresión Descripción
&& AND (Y lógico) A && B Devuelve true si ambos operandos son true
|| OR (O lógico) A || B Devuelve true si al menos un operando es true
! NOT (negación) !A Invierte el valor booleano (true pasa a false y viceversa)

Tablas de verdad

Las tablas de verdad definen de forma exhaustiva el resultado devuelto por cada operador lógico en función de todas las combinaciones posibles de sus operandos.

Operador &&

Devuelve true únicamente cuando ambos operandos son verdaderos.

A B A && B
false false false
false true false
true false false
true true true

Operador ||

Devuelve true si al menos uno de los operandos es verdadero. Solo devuelve false si ambos son falsos.

A B A || B
false false false
false true true
true false true
true true true

Operador !

Es un operador unario que invierte el estado booleano del operando sobre el que actúa.

A !A
false true
true false
int edad = 20;
boolean tieneCarnet = true;

// AND: Ambas condiciones deben cumplirse
boolean puedeConducir = (edad >= 18) && tieneCarnet; // true

// OR: Basta con que una condición sea verdadera
boolean esFinDeSemana = false;
boolean esFestivo = true;
boolean noHayClase = esFinDeSemana || esFestivo; // true

// NOT: Invierte el valor
boolean esMayor = true;
boolean esMenor = !esMayor; // false

Operador ternario

?:

Es el único operador de Java que necesita 3 operandos. Este operador devuelve un valor que se selecciona entre dos posibles. La selección dependerá de la evaluación de una expresión relacional o lógica que, como hemos visto, sólo puede tomar dos valores: true o false.

Su sintaxis es: $$expresiónCondicional\space ? \space expresiónSiVerdadero : expresiónSiFalso$$
El flujo de evaluación es el siguiente:

  1. Se evalúa la expresiónCondicional. Esta evaluación devolverá true o false.
  2. Si el resultado es true, devuelve el valor de expresiónSiVerdadero.
  3. Si el resultado es false, devuelve el valor de expresiónSiFalso.

Este operador es una versión compacta de una expresión condicional, que veremos más adelante.

int edad = 20;

// Si edad >= 18 es true, asigna "Mayor de edad"; si es false, asigna "Menor de edad"
String estado = (edad >= 18) ? "Mayor de edad" : "Menor de edad";

int a = 15;
int b = 42;

// Devuelve el valor máximo entre dos números
int maximo = (a > b) ? a : b; // Asigna 42
Compatibilidad de tipos

Ambas expresiones (expresiónSiVerdadero y expresiónSiFalso) deben devolver valores que sean compatibles con el tipo de dato de la variable a la que se le va a asignar el resultado.


Precedencia de operadores

En una expresión, la precedencia establece qué operaciones se realizan antes. Con igualdad de precedencia, se realizan de izquierda a derecha. Por ejemplo:

Alteración de la precedencia

Podemos alterar el orden natural de precedencia utilizando paréntesis, obligando a que algunas operaciones se resuelvan antes que otras. Por ejemplo: en (3 + 6) * 7 se realiza antes la suma que la multiplicación, resultando en 9 * 7, igual a 63.

Tabla de precedencia (de mayor a menor jerarquía)

Categoría Operadores
Paréntesis y miembros ()
Unarios (postfijos) expr++ expr--
Unarios (prefijos y casting) ++expr --expr + - ! (tipo)
Multiplicativos * / %
Aditivos + -
Relacionales < <= > >=
Igualdad == !=
Lógico AND &&
Lógico OR ||
Ternario ?:
Asignación = += -= *= /= %=

Conversión de tipos

Como ya hemos visto, todas las variables tienen asociado un tipo. Cuando se realiza una operación o asignación entre datos de diferentes tipos, Java exige que ambos sean compatibles. La conversión de tipos (type casting) es el mecanismo que permite transformar un valor de un tipo de dato a otro.

Existen tres formas principales de realizar conversiones de tipo en Java:

  1. Conversión implícita (promoción).
  2. Conversión explícita (casting).
  3. Conversión entre cadenas (String) y tipos numéricos (clases envolventes). Estas las veremos más adelante

Conversión implícita (promoción)

Se produce de forma automática por el compilador cuando se asigna un valor de un tipo con menor rango o precisión a una variable de un tipo con mayor rango o espacio en memoria. Esto también sucede durante la evaluación de las expresiones, como ya hemos visto.

Como no existe riesgo de pérdida de información, Java realiza la conversión sin necesidad de sintaxis especial.

Rango menor⟶Rango mayor

La jerarquía de promoción es la siguiente:

byte⟶short⟶int⟶long⟶float⟶doublechar⟶short
int numeroEntero = 100;
double numeroReal = numeroEntero; // Conversión implícita: int (32 bits) a double (64 bits)

System.out.println(numeroReal); // Imprime: 100.0

numeroEntero = numeroReal; // ERROR, no se puede hacer una conversión implícita por riesgo a perder información

char letra = 65; // Código Unicode/ASCII: 65
int codigoAscii = letra;
System.out.println(letra); // Imprime: 'A'
System.out.println(codigoAscii); // Imprime: 65

Conversión explícita (casting)

Ocurre cuando indicamos explícitamente el cambio de tipo que queremos lograr. Normalmente se utiliza para convertir un valor de un tipo con mayor rango o precisión a un tipo con menor espacio en memoria.

Dado que existe riesgo de pérdida de información o desbordamiento, el compilador no realiza esta conversión automáticamente e impondrá un error de compilación (Type mismatch). Para forzar la conversión, el programador debe indicar explícitamente el tipo destino encerrado entre paréntesis (tipo) delante de la expresión.

Sintaxis: variableDestino = (tipoDestino) expresion;
float pi = 3.14159f;
int piEntero = (int) pi; // Casting explícito de float a int

System.out.println(piEntero); // Imprime: 3 (se TRUNCA la parte decimal, no se redondea)
Peligros del casting explícito

  1. Pérdida de precisión: al pasar de un número decimal (double/float) a uno entero (int/long), la parte decimal se descarta por completo.
  2. Desbordamiento (overflow): si el valor a convertir supera el rango máximo representable por el tipo destino, los bits superiores se descartan, produciendo un valor inesperado
int numeroGrande = 130;
byte bytePequenyo = (byte) numeroGrande; // El rango de byte es [-128, 127]
System.out.println(bytePequenyo); // Imprime: -126 (Desbordamiento)


Comentarios

Aunque ya se han estado utilizando en los diferentes ejemplos, es necesario hablar de los comentarios antes de terminar esta unidad.

Los comentarios son anotaciones escritas dentro del código fuente que el compilador ignora por completo durante el proceso de compilación. No afectan al rendimiento ni a la ejecución del programa, pero son indispensables para explicar la lógica de algoritmos complejos, indicar decisiones de diseño y facilitar el mantenimiento en equipo.

En Java existen 3 tipos de comentarios: de 1 línea, multilínea y de documentación

Comentarios de 1 línea

Comienzan con una doble barra diagonal (//). Todo el texto situado a la derecha de las dos barras hasta el final de la línea es ignorado por el compilador. Se utilizan para explicaciones breves e puntuales sobre instrucciones específicas.

int limiteUsuarios = 100; // Define la capacidad máxima del servidor

Comentarios multilínea

Se delimitan entre /* al inicio y */ al final. Permiten abarcar varias líneas de texto seguidas. Son ideales para redactar explicaciones más extensas, descripciones de algoritmos o para desactivar temporalmente bloques enteros de código. Es una mala práctica utilizarlos para "conservar código antiguo" (diógenes del programador). Siguiendo una metodología adecuada de desarrollo de software, el código antiguo pertenece a las versiones antiguas de un mismo proyecto

/*
 * Cálculo de la media de las tareas:
 * media aritmética de las notas de ambas tareas
 */
double media = calcularMedia(tarea1, tarea2);

Comentarios de documentación ( Javadoc)

Se delimitan entre /** al inicio y */ al final. Tienen un propósito especial: permiten generar documentación técnica estandarizada en formato HTML de forma automática utilizando la herramienta oficial javadoc de JDK.

Se colocan justo encima de las clases, interfaces, atributos o métodos, y suelen incluir etiquetas especiales (como @param, @return, @throws). Lo veremos más adelante.

/**
 * Calcula la media aritmética de dos notas enteras.
 * 
 * @param nota1 Primera nota obtenida (debe ser mayor o igual a 0).
 * @param nota2 Segunda nota obtenida (debe ser mayor o igual a 0).
 * @return La media aritmética en formato decimal (double).
 */
public double calcularMedia(int nota1, int nota2) {
    return (double) (nota1 + nota2) / 2;
}

Buenas prácticas sobre el uso de comentarios

  1. El código debe explicarse por sí mismo: utiliza nombres claros para variables (int edadUsuario en lugar de int e). No es necesario comentar lo que ya es evidente.
    • Buena práctica:
      int edadUsuario;
      
    • Mala práctica:
      int e; // Edad del usuario
      
  2. Comenta el "por qué", no el "qué": explica los motivos de una decisión, no la sintaxis básica.
    • Buena práctica:
      // Ajuste de 1 segundo para compensar posibles latencias en la red
      int tiempoEspera = timeout + 1000;
      
    • Mala práctica:
      // Sumamos 1000 a la variable timeout
      int tiempoEspera = timeout + 1000;
      
  3. Mantén los comentarios actualizados: un comentario que contradice el código es peor que no tener un comentario.
    • Buena práctica:
      // Descuento del 20% para los usuarios VIP
      double precioFinal = precioBase * 0.8;
      
    • Mala práctica: el código cambió al 20%, pero el comentario quedó desactualizado.
      // Descuento del 15% para los usuarios VIP
      double precioFinal = precioBase * 0.8;