Java
Tutorial Java: Funciones
Java funciones: declaración y uso práctico. Domina la declaración y uso de funciones en Java con ejemplos prácticos y detallados.
Aprende Java y certifícateDefinición de una función y sus distintas partes
En Java, las funciones se implementan como métodos que pertenecen a una clase. Un método es un bloque de código que realiza una tarea específica y puede ser invocado (llamado) cuando sea necesario. Los métodos son fundamentales en la programación Java, ya que permiten organizar el código en unidades lógicas y reutilizables.
Anatomía de un método en Java
Un método en Java se compone de varias partes esenciales que definen su comportamiento, accesibilidad y funcionamiento:
[modificadores] [tipo_retorno] [nombre_método]([parámetros]) [excepciones] {
// Cuerpo del método
[sentencias]
[return valor;]
}
Analicemos cada una de estas partes:
Modificadores de acceso
Los modificadores de acceso determinan desde dónde se puede acceder al método:
public
: El método es accesible desde cualquier clase.protected
: El método es accesible dentro del mismo paquete y subclases.private
: El método solo es accesible dentro de la misma clase.- Sin modificador (default): El método es accesible solo dentro del mismo paquete.
Además de los modificadores de acceso, existen otros modificadores que afectan el comportamiento:
static
: El método pertenece a la clase, no a instancias específicas.final
: El método no puede ser sobrescrito por subclases.abstract
: El método no tiene implementación y debe ser implementado por subclases.synchronized
: El método está sincronizado para hilos.
Tipo de retorno
El tipo de retorno especifica qué tipo de dato devuelve el método:
- Puede ser cualquier tipo de dato primitivo (
int
,double
,boolean
, etc.) - Puede ser cualquier tipo de referencia (clases, interfaces, arrays)
void
indica que el método no devuelve ningún valor
public int sumar(int a, int b) {
return a + b; // Devuelve un entero
}
public void mostrarMensaje(String mensaje) {
System.out.println(mensaje); // No devuelve nada
}
Nombre del método
El nombre del método identifica la función y se utiliza para invocarla. En Java, se siguen estas convenciones:
- Comienza con una letra minúscula
- Utiliza camelCase para nombres compuestos
- Debe ser descriptivo sobre lo que hace el método
- Generalmente se utilizan verbos
Ejemplos de nombres adecuados:
calcularTotal()
esNumeroPositivo()
obtenerNombreCompleto()
Lista de parámetros
Los parámetros son variables que recibe el método para realizar su tarea. Se definen entre paréntesis después del nombre del método:
public void saludar(String nombre, int edad) {
System.out.println("Hola " + nombre + ", tienes " + edad + " años.");
}
Si el método no recibe parámetros, se dejan los paréntesis vacíos:
public void mostrarFechaActual() {
System.out.println(java.time.LocalDate.now());
}
Declaración de excepciones
Se pueden declarar las excepciones que el método puede lanzar usando la palabra clave throws
:
public void leerArchivo(String ruta) throws IOException {
// Código para leer un archivo
}
Cuerpo del método
El cuerpo del método contiene las instrucciones que se ejecutan cuando se llama al método. Se define entre llaves {}
y puede incluir:
- Declaraciones de variables locales
- Estructuras de control (if, switch, bucles)
- Llamadas a otros métodos
- Sentencia
return
(si el método no es void)
public double calcularAreaCirculo(double radio) {
// Variable local
final double PI = 3.14159;
// Cálculo
double area = PI * radio * radio;
// Retorno del resultado
return area;
}
Sentencia return
La sentencia return se utiliza para:
- Devolver un valor del tipo especificado en la declaración del método
- Finalizar la ejecución del método
public boolean esMayorDeEdad(int edad) {
if (edad >= 18) {
return true; // Termina el método y devuelve true
}
return false; // Termina el método y devuelve false
}
Para métodos void
, la sentencia return
es opcional y solo se usa para terminar la ejecución:
public void procesarSiPositivo(int numero) {
if (numero <= 0) {
return; // Termina el método si el número no es positivo
}
// Este código solo se ejecuta si el número es positivo
System.out.println("Procesando: " + numero);
}
Ejemplos completos de métodos
Veamos algunos ejemplos completos que ilustran diferentes tipos de métodos:
Método simple sin parámetros:
public void mostrarBienvenida() {
System.out.println("¡Bienvenido a nuestra aplicación!");
}
Método con parámetros y valor de retorno:
public double calcularPromedio(double[] numeros) {
if (numeros.length == 0) {
return 0.0;
}
double suma = 0;
for (double numero : numeros) {
suma += numero;
}
return suma / numeros.length;
}
Método estático de utilidad:
public static String formatearFecha(LocalDate fecha) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
return fecha.format(formatter);
}
Método con manejo de excepciones:
public int dividir(int dividendo, int divisor) throws ArithmeticException {
if (divisor == 0) {
throw new ArithmeticException("No se puede dividir por cero");
}
return dividendo / divisor;
}
Buenas prácticas al definir métodos
- Principio de responsabilidad única: Cada método debe realizar una sola tarea bien definida.
- Nombres descriptivos: El nombre debe indicar claramente lo que hace el método.
- Métodos cortos: Se recomienda que los métodos no sean demasiado largos (idealmente menos de 20-30 líneas).
- Documentación: Utilizar comentarios Javadoc para documentar el propósito, parámetros y valor de retorno.
- Validación de parámetros: Verificar que los parámetros recibidos sean válidos.
/**
* Calcula el área de un triángulo usando la fórmula de Herón.
*
* @param a Longitud del primer lado
* @param b Longitud del segundo lado
* @param c Longitud del tercer lado
* @return El área del triángulo
* @throws IllegalArgumentException Si los lados no forman un triángulo válido
*/
public double calcularAreaTriangulo(double a, double b, double c) {
// Validación de parámetros
if (a <= 0 || b <= 0 || c <= 0) {
throw new IllegalArgumentException("Los lados deben ser positivos");
}
if (a + b <= c || a + c <= b || b + c <= a) {
throw new IllegalArgumentException("Los lados no forman un triángulo válido");
}
// Cálculo del semiperímetro
double s = (a + b + c) / 2;
// Fórmula de Herón
return Math.sqrt(s * (s - a) * (s - b) * (s - c));
}
Funciones con parámetros: cero parámetros, un parámetro, varios parámetros
Los parámetros permiten que los métodos reciban datos externos para realizar sus operaciones.
Métodos sin parámetros
Los métodos sin parámetros (o con cero parámetros) se definen con paréntesis vacíos y no reciben ningún dato externo para realizar su tarea. Estos métodos suelen:
- Realizar operaciones basadas únicamente en el estado interno del objeto
- Devolver información constante o calculada internamente
- Ejecutar acciones que no requieren datos de entrada
public class Reloj {
private int hora;
private int minuto;
// Constructor que inicializa con la hora actual
public Reloj() {
LocalTime ahora = LocalTime.now();
this.hora = ahora.getHour();
this.minuto = ahora.getMinute();
}
// Método sin parámetros que devuelve la hora formateada
public String obtenerHoraActual() {
return String.format("%02d:%02d", hora, minuto);
}
// Método sin parámetros que incrementa un minuto
public void avanzarMinuto() {
minuto++;
if (minuto >= 60) {
minuto = 0;
hora = (hora + 1) % 24;
}
}
}
Los métodos sin parámetros también son comunes para implementar getters que devuelven el valor de atributos:
public class Producto {
private String nombre;
private double precio;
// Constructor
public Producto(String nombre, double precio) {
this.nombre = nombre;
this.precio = precio;
}
// Getters sin parámetros
public String getNombre() {
return nombre;
}
public double getPrecio() {
return precio;
}
}
Métodos con un parámetro
Los métodos con un solo parámetro reciben un único dato para realizar su operación. Estos métodos son ideales cuando:
- Se necesita realizar una operación sobre un único valor
- Se quiere modificar un atributo del objeto con un nuevo valor
- Se busca validar o transformar un dato específico
public class Calculadora {
// Método con un parámetro que calcula el cuadrado
public int calcularCuadrado(int numero) {
return numero * numero;
}
// Método con un parámetro que verifica si es primo
public boolean esPrimo(int numero) {
if (numero <= 1) {
return false;
}
for (int i = 2; i <= Math.sqrt(numero); i++) {
if (numero % i == 0) {
return false;
}
}
return true;
}
}
Los setters son ejemplos típicos de métodos con un parámetro:
public class Usuario {
private String nombre;
private String email;
// Setters con un parámetro
public void setNombre(String nombre) {
this.nombre = nombre;
}
public void setEmail(String email) {
// Validación simple antes de asignar
if (email != null && email.contains("@")) {
this.email = email;
} else {
throw new IllegalArgumentException("Email inválido");
}
}
}
Métodos con varios parámetros
Los métodos con varios parámetros permiten operaciones más complejas que requieren varios datos de entrada. Estos métodos son útiles cuando:
- Se necesitan varios valores para realizar un cálculo
- Se quieren modificar múltiples atributos a la vez
- Se busca comparar o combinar diferentes valores
public class GestorReservas {
// Método con tres parámetros
public boolean verificarDisponibilidad(String habitacion, LocalDate fechaInicio, LocalDate fechaFin) {
// Verificar si la habitación está disponible en el rango de fechas
if (fechaInicio.isAfter(fechaFin)) {
throw new IllegalArgumentException("La fecha de inicio debe ser anterior a la fecha fin");
}
// Lógica para verificar disponibilidad
return consultarBaseDeDatos(habitacion, fechaInicio, fechaFin);
}
// Método auxiliar (simulado)
private boolean consultarBaseDeDatos(String habitacion, LocalDate inicio, LocalDate fin) {
// Simulación de consulta
return Math.random() > 0.3; // 70% de probabilidad de disponibilidad
}
// Método con cuatro parámetros
public String generarCodigoReserva(String nombreCliente, String habitacion,
LocalDate fechaInicio, int duracion) {
String iniciales = obtenerIniciales(nombreCliente);
String fechaStr = fechaInicio.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
return iniciales + "-" + habitacion + "-" + fechaStr + "-" + duracion;
}
private String obtenerIniciales(String nombre) {
// Lógica para obtener iniciales
return nombre.substring(0, Math.min(2, nombre.length())).toUpperCase();
}
}
Orden y agrupación de parámetros
Cuando se trabaja con múltiples parámetros, es importante considerar:
- Orden lógico: Colocar los parámetros en un orden intuitivo
- Agrupación conceptual: Parámetros relacionados deben ir juntos
- Consistencia: Mantener el mismo orden en métodos relacionados
// Orden lógico: primero el origen, luego el destino
public double calcularDistancia(Punto origen, Punto destino) {
return Math.sqrt(Math.pow(destino.x - origen.x, 2) +
Math.pow(destino.y - origen.y, 2));
}
// Consistencia en el orden de parámetros
public void dibujarLinea(Punto inicio, Punto fin, Color color, float grosor) {
// Código para dibujar
}
public void dibujarRectangulo(Punto esquinaSuperior, Punto esquinaInferior,
Color color, float grosor) {
// Código para dibujar
}
Parámetros con valores por defecto
Java no soporta directamente parámetros con valores por defecto como otros lenguajes, pero se pueden implementar mediante:
- Sobrecarga de métodos: Definir múltiples versiones del mismo método con diferentes parámetros
- Patrón Builder: Para casos con muchos parámetros opcionales
Ejemplo de sobrecarga de métodos:
public class Notificador {
// Versión completa con todos los parámetros
public void enviarMensaje(String destinatario, String asunto,
String cuerpo, boolean alta_prioridad) {
// Lógica para enviar mensaje
System.out.println("Enviando a: " + destinatario);
System.out.println("Asunto: " + asunto);
System.out.println("Cuerpo: " + cuerpo);
System.out.println("Prioridad alta: " + alta_prioridad);
}
// Versión con valor por defecto para prioridad (false)
public void enviarMensaje(String destinatario, String asunto, String cuerpo) {
enviarMensaje(destinatario, asunto, cuerpo, false);
}
// Versión con valores por defecto para asunto y prioridad
public void enviarMensaje(String destinatario, String cuerpo) {
enviarMensaje(destinatario, "Sin asunto", cuerpo, false);
}
}
Validación de parámetros
Es una buena práctica validar los parámetros recibidos para garantizar que cumplen con los requisitos esperados:
public class GestorArchivos {
public void guardarDatos(String ruta, byte[] datos) {
// Validación de parámetros
if (ruta == null || ruta.isEmpty()) {
throw new IllegalArgumentException("La ruta no puede estar vacía");
}
if (datos == null || datos.length == 0) {
throw new IllegalArgumentException("No hay datos para guardar");
}
// Lógica para guardar los datos
try {
Files.write(Path.of(ruta), datos);
} catch (IOException e) {
throw new RuntimeException("Error al guardar el archivo", e);
}
}
}
Parámetros inmutables
Para evitar efectos secundarios inesperados, se recomienda tratar los parámetros como inmutables:
public class ProcesadorTexto {
// Incorrecto: modifica el parámetro
public void procesarListaIncorrecto(List<String> palabras) {
palabras.removeIf(palabra -> palabra.length() < 3);
// Esto modifica la lista original
}
// Correcto: no modifica el parámetro original
public List<String> procesarListaCorrecto(List<String> palabras) {
// Crear una nueva lista para no modificar la original
List<String> resultado = new ArrayList<>(palabras);
resultado.removeIf(palabra -> palabra.length() < 3);
return resultado;
}
}
Parámetros de tipo primitivo vs. referencia
Es importante entender la diferencia entre pasar parámetros de tipo primitivo y de tipo referencia:
- Los tipos primitivos (
int
,double
,boolean
, etc.) se pasan por valor - Los tipos de referencia (objetos, arrays) se pasan por referencia al valor del objeto
public class DemostradorParametros {
public void demostrarPasoPorValor() {
int numero = 10;
System.out.println("Antes de llamar al método: " + numero);
modificarPrimitivo(numero);
System.out.println("Después de llamar al método: " + numero);
// Imprime 10, el valor original no cambia
}
private void modificarPrimitivo(int valor) {
valor = valor * 2; // Esta modificación no afecta a la variable original
}
public void demostrarPasoPorReferencia() {
List<String> frutas = new ArrayList<>();
frutas.add("Manzana");
System.out.println("Antes de llamar al método: " + frutas);
modificarLista(frutas);
System.out.println("Después de llamar al método: " + frutas);
// La lista original se modifica
}
private void modificarLista(List<String> lista) {
lista.add("Banana"); // Esta modificación afecta a la lista original
}
}
Consideraciones de rendimiento
Cuando se trabaja con métodos que reciben muchos parámetros o parámetros de gran tamaño, es importante considerar:
- Número de parámetros: Demasiados parámetros pueden indicar que el método hace demasiadas cosas
- Objetos grandes: Pasar objetos grandes por referencia es más eficiente que copiarlos
- Inmutabilidad: Los objetos inmutables son seguros para pasar por referencia
// Demasiados parámetros - podría refactorizarse
public void configurarConexion(String servidor, int puerto, String usuario,
String contraseña, boolean ssl, int timeout,
String protocolo, Map<String, String> opciones) {
// Implementación
}
// Mejor enfoque: usar un objeto de configuración
public void configurarConexion(ConfiguracionConexion config) {
// Implementación usando config.getServidor(), config.getPuerto(), etc.
}
// Clase para encapsular los parámetros
public class ConfiguracionConexion {
private String servidor;
private int puerto;
// Resto de atributos, getters y setters
}
Ejemplos prácticos de uso de parámetros
Ejemplo 1: Calculadora con diferentes tipos de parámetros
public class CalculadoraAvanzada {
// Sin parámetros - devuelve un valor constante
public double obtenerPI() {
return Math.PI;
}
// Un parámetro primitivo
public double calcularRaizCuadrada(double numero) {
if (numero < 0) {
throw new IllegalArgumentException("No se puede calcular la raíz de un número negativo");
}
return Math.sqrt(numero);
}
// Dos parámetros primitivos
public double elevarAPotencia(double base, double exponente) {
return Math.pow(base, exponente);
}
// Un parámetro de tipo array
public double calcularPromedio(double[] numeros) {
if (numeros == null || numeros.length == 0) {
throw new IllegalArgumentException("El array no puede estar vacío");
}
double suma = 0;
for (double num : numeros) {
suma += num;
}
return suma / numeros.length;
}
// Un parámetro de tipo objeto
public double calcularPerimetro(Rectangulo rectangulo) {
return 2 * (rectangulo.getAncho() + rectangulo.getAlto());
}
}
// Clase auxiliar
class Rectangulo {
private double ancho;
private double alto;
public Rectangulo(double ancho, double alto) {
this.ancho = ancho;
this.alto = alto;
}
public double getAncho() { return ancho; }
public double getAlto() { return alto; }
}
Ejemplo 2: Procesador de texto con diferentes parámetros
public class ProcesadorTexto {
// Sin parámetros
public String generarTextoAleatorio() {
String[] palabras = {"Java", "es", "un", "lenguaje", "de", "programación", "versátil"};
StringBuilder resultado = new StringBuilder();
for (int i = 0; i < 5; i++) {
int indice = (int) (Math.random() * palabras.length);
resultado.append(palabras[indice]).append(" ");
}
return resultado.toString().trim();
}
// Un parámetro String
public int contarPalabras(String texto) {
if (texto == null || texto.isEmpty()) {
return 0;
}
return texto.split("\\s+").length;
}
// Dos parámetros: String y boolean
public String formatearTexto(String texto, boolean mayusculas) {
if (texto == null) {
return "";
}
return mayusculas ? texto.toUpperCase() : texto.toLowerCase();
}
// Tres parámetros: String, String, int
public String reemplazarYLimitar(String texto, String buscar, int longitudMaxima) {
if (texto == null) {
return "";
}
String resultado = texto.replace(buscar, "***");
if (resultado.length() > longitudMaxima) {
return resultado.substring(0, longitudMaxima) + "...";
}
return resultado;
}
}
Paso por valor y paso por referencia
En Java, la forma en que se pasan los argumentos a los métodos afecta directamente al comportamiento de los programas.
Fundamentos del paso de parámetros en Java
Java utiliza exclusivamente el mecanismo de paso por valor para todos sus parámetros. Sin embargo, esto puede resultar confuso porque el comportamiento varía dependiendo de si trabajamos con tipos primitivos o tipos de referencia:
- Para tipos primitivos: se pasa una copia del valor
- Para tipos de referencia: se pasa una copia de la referencia (no el objeto en sí)
Esta distinción es crucial y explica por qué a veces parece que Java utiliza paso por referencia cuando en realidad no es así.
Paso por valor con tipos primitivos
Cuando se pasa un tipo primitivo (int
, double
, boolean
, etc.) a un método, Java crea una copia del valor y la pasa al método. Cualquier modificación que se realice a este parámetro dentro del método no afectará a la variable original.
public class DemostradorPrimitivos {
public static void main(String[] args) {
int numero = 10;
System.out.println("Antes de llamar al método: " + numero);
modificarNumero(numero);
System.out.println("Después de llamar al método: " + numero);
// Imprime 10 - el valor original no cambia
}
public static void modificarNumero(int valor) {
valor = valor * 2; // Esta modificación solo afecta a la copia local
System.out.println("Dentro del método: " + valor); // Imprime 20
}
}
En este ejemplo, aunque dentro del método modificarNumero()
el valor se duplica, la variable original numero
en el método main()
permanece inalterada. Esto demuestra el paso por valor: el método recibe una copia independiente del valor original.
Paso por valor con tipos de referencia
Aquí es donde surge la confusión. Cuando se pasa un objeto (un tipo de referencia como String
, ArrayList
, clases personalizadas, etc.), Java pasa por valor una copia de la referencia al objeto, no el objeto en sí.
Esto significa que:
- El método recibe una copia de la referencia que apunta al mismo objeto
- Se pueden modificar las propiedades del objeto a través de esta referencia
- No se puede hacer que la referencia apunte a un objeto diferente (desde la perspectiva del llamador)
Veamos un ejemplo:
public class DemostradorReferencias {
public static void main(String[] args) {
// Creamos un ArrayList
ArrayList<String> frutas = new ArrayList<>();
frutas.add("Manzana");
System.out.println("Antes de llamar al método: " + frutas);
// Llamamos al método que modifica el contenido
modificarContenido(frutas);
System.out.println("Después de modificar contenido: " + frutas);
// Imprime [Manzana, Plátano] - el contenido SÍ cambia
// Llamamos al método que intenta cambiar la referencia
reemplazarReferencia(frutas);
System.out.println("Después de reemplazar referencia: " + frutas);
// Imprime [Manzana, Plátano] - la referencia NO cambia
}
public static void modificarContenido(ArrayList<String> lista) {
lista.add("Plátano"); // Modifica el objeto al que apunta la referencia
}
public static void reemplazarReferencia(ArrayList<String> lista) {
// Creamos un nuevo ArrayList y hacemos que la referencia local apunte a él
lista = new ArrayList<>();
lista.add("Naranja");
lista.add("Pera");
// Estos cambios no afectan a la referencia original en main()
}
}
Este ejemplo ilustra la diferencia clave:
- En
modificarContenido()
, se modifica el objeto al que apunta la referencia, y estos cambios son visibles fuera del método. - En
reemplazarReferencia()
, se intenta hacer que la referencia apunte a un nuevo objeto, pero este cambio solo afecta a la copia local de la referencia, no a la referencia original enmain()
.
Casos especiales y consideraciones
El caso de String
String
en Java es inmutable, lo que significa que no se puede modificar una vez creado. Esto puede llevar a confusión:
public static void main(String[] args) {
String nombre = "Juan";
modificarString(nombre);
System.out.println(nombre); // Imprime "Juan", no "Juan Pérez"
}
public static void modificarString(String texto) {
texto = texto + " Pérez"; // Crea un nuevo objeto String
}
Aunque String
es un tipo de referencia, su inmutabilidad hace que se comporte de manera similar a los tipos primitivos en este contexto.
Arrays como caso especial
Los arrays en Java son objetos, por lo que se pasan por valor la referencia:
public static void main(String[] args) {
int[] numeros = {1, 2, 3};
modificarContenidoArray(numeros);
System.out.println(Arrays.toString(numeros)); // Imprime [10, 2, 3]
reemplazarArray(numeros);
System.out.println(Arrays.toString(numeros)); // Sigue imprimiendo [10, 2, 3]
}
public static void modificarContenidoArray(int[] array) {
array[0] = 10; // Modifica el contenido del array
}
public static void reemplazarArray(int[] array) {
array = new int[]{4, 5, 6}; // Solo cambia la referencia local
}
Objetos inmutables
Java tiene varios tipos de referencia inmutables además de String
, como Integer
, Double
, BigDecimal
, etc. Estos objetos, una vez creados, no pueden cambiar su estado interno:
public static void main(String[] args) {
Integer numero = 10;
modificarInteger(numero);
System.out.println(numero); // Imprime 10, no 20
}
public static void modificarInteger(Integer valor) {
valor = valor + 10; // Crea un nuevo objeto Integer
}
Simulando paso por referencia en Java
Aunque Java no tiene paso por referencia verdadero, se pueden simular algunos de sus efectos:
Usando objetos contenedores
Se puede crear una clase simple que contenga el valor que queremos modificar:
public class Contenedor<T> {
private T valor;
public Contenedor(T valor) {
this.valor = valor;
}
public T getValor() {
return valor;
}
public void setValor(T valor) {
this.valor = valor;
}
}
public static void main(String[] args) {
Contenedor<Integer> contador = new Contenedor<>(10);
duplicarValor(contador);
System.out.println(contador.getValor()); // Imprime 20
}
public static void duplicarValor(Contenedor<Integer> cont) {
cont.setValor(cont.getValor() * 2);
}
Usando arrays de un solo elemento
Otra técnica común es usar un array de un solo elemento:
public static void main(String[] args) {
int[] contador = {10};
duplicarValor(contador);
System.out.println(contador[0]); // Imprime 20
}
public static void duplicarValor(int[] cont) {
cont[0] = cont[0] * 2;
}
Usando objetos mutables
Para tipos más complejos, se pueden usar clases propias con métodos que modifiquen su estado:
public class Contador {
private int valor;
public Contador(int valorInicial) {
this.valor = valorInicial;
}
public int getValor() {
return valor;
}
public void incrementar() {
valor++;
}
public void duplicar() {
valor *= 2;
}
}
public static void main(String[] args) {
Contador c = new Contador(10);
procesarContador(c);
System.out.println(c.getValor()); // Imprime 21
}
public static void procesarContador(Contador contador) {
contador.duplicar();
contador.incrementar();
}
Implicaciones y buenas prácticas
Rendimiento
El paso por valor puede tener implicaciones de rendimiento:
- Para tipos primitivos: la copia es generalmente rápida ya que son valores pequeños
- Para tipos de referencia: solo se copia la referencia (un valor pequeño), no el objeto completo
Para objetos muy grandes, no hay que preocuparse por el costo de copiar todo el objeto, ya que solo se copia la referencia.
Inmutabilidad y efectos secundarios
Entender el paso por valor/referencia es crucial para gestionar los efectos secundarios en nuestro código:
// Enfoque con efectos secundarios
public void procesarDatos(List<Cliente> clientes) {
clientes.removeIf(cliente -> cliente.getSaldo() < 0);
// La lista original se modifica
}
// Enfoque sin efectos secundarios (más predecible)
public List<Cliente> filtrarClientesConSaldoPositivo(List<Cliente> clientes) {
List<Cliente> resultado = new ArrayList<>(clientes);
resultado.removeIf(cliente -> cliente.getSaldo() < 0);
return resultado;
// La lista original no se modifica
}
El segundo enfoque es generalmente preferible porque:
- Es más predecible y fácil de razonar sobre su comportamiento
- Facilita las pruebas unitarias
- Reduce los errores causados por modificaciones inesperadas
Documentación clara
Es importante documentar claramente cuando un método modifica sus parámetros:
/**
* Ordena la lista proporcionada en orden ascendente.
*
* @param lista La lista a ordenar (será modificada por este método)
*/
public void ordenarLista(List<Integer> lista) {
Collections.sort(lista);
}
Ejemplos prácticos de uso
Ejemplo 1: Procesamiento de datos financieros
public class ProcesadorFinanciero {
/**
* Calcula el total de una factura añadiendo impuestos.
* Este método no modifica el objeto original.
*/
public Factura calcularTotalConImpuestos(Factura factura, double tasaImpuesto) {
// Creamos una nueva factura para no modificar la original
Factura resultado = new Factura(factura);
double subtotal = factura.getSubtotal();
double impuestos = subtotal * tasaImpuesto;
resultado.setImpuestos(impuestos);
resultado.setTotal(subtotal + impuestos);
return resultado;
}
/**
* Aplica un descuento a todos los ítems de la factura.
* Este método modifica el objeto factura original.
*/
public void aplicarDescuento(Factura factura, double porcentajeDescuento) {
for (Item item : factura.getItems()) {
double precioConDescuento = item.getPrecio() * (1 - porcentajeDescuento/100);
item.setPrecio(precioConDescuento);
}
// Recalculamos el subtotal
double nuevoSubtotal = factura.getItems().stream()
.mapToDouble(item -> item.getPrecio() * item.getCantidad())
.sum();
factura.setSubtotal(nuevoSubtotal);
}
}
class Factura {
private List<Item> items;
private double subtotal;
private double impuestos;
private double total;
// Constructor de copia
public Factura(Factura original) {
this.items = new ArrayList<>(original.items);
this.subtotal = original.subtotal;
this.impuestos = original.impuestos;
this.total = original.total;
}
// Resto de la implementación...
}
class Item {
private String nombre;
private double precio;
private int cantidad;
// Getters y setters...
}
Ejemplo 2: Manipulación de imágenes
public class ProcesadorImagen {
/**
* Aplica un filtro de escala de grises a la imagen.
* Este método modifica la imagen original.
*/
public void aplicarFiltroGrises(int[][] pixeles) {
int altura = pixeles.length;
int anchura = pixeles[0].length;
for (int y = 0; y < altura; y++) {
for (int x = 0; x < anchura; x++) {
int rgb = pixeles[y][x];
int r = (rgb >> 16) & 0xFF;
int g = (rgb >> 8) & 0xFF;
int b = rgb & 0xFF;
// Fórmula para convertir a escala de grises
int gris = (int)(0.299 * r + 0.587 * g + 0.114 * b);
// Crear nuevo valor RGB con el mismo valor para R, G y B
int nuevoRgb = (gris << 16) | (gris << 8) | gris;
pixeles[y][x] = nuevoRgb;
}
}
}
/**
* Crea una versión redimensionada de la imagen original.
* Este método no modifica la imagen original.
*/
public int[][] redimensionar(int[][] imagenOriginal, int nuevaAnchura, int nuevaAltura) {
int[][] nuevaImagen = new int[nuevaAltura][nuevaAnchura];
int alturaOriginal = imagenOriginal.length;
int anchuraOriginal = imagenOriginal[0].length;
for (int y = 0; y < nuevaAltura; y++) {
for (int x = 0; x < nuevaAnchura; x++) {
// Cálculo simple de correspondencia de píxeles (interpolación más básica)
int xOriginal = x * anchuraOriginal / nuevaAnchura;
int yOriginal = y * alturaOriginal / nuevaAltura;
nuevaImagen[y][x] = imagenOriginal[yOriginal][xOriginal];
}
}
return nuevaImagen;
}
}
Resumen de conceptos clave
- Java utiliza exclusivamente paso por valor para todos los tipos de datos.
- Para tipos primitivos, se pasa una copia del valor, y las modificaciones dentro del método no afectan a la variable original.
- Para tipos de referencia, se pasa una copia de la referencia (no el objeto), lo que permite modificar el objeto pero no cambiar a qué objeto apunta la referencia original.
- Los objetos inmutables como
String
no pueden ser modificados, por lo que se comportan de manera similar a los tipos primitivos. - Se pueden simular algunos aspectos del paso por referencia usando objetos contenedores, arrays de un elemento o clases mutables.
- Es una buena práctica evitar modificar los parámetros de entrada cuando sea posible, para crear código más predecible y fácil de mantener.
Varargs
Los varargs (argumentos variables) son una característica de Java que permite a los métodos aceptar un número variable de argumentos del mismo tipo. Esta funcionalidad, introducida en Java 5, simplifica el código al eliminar la necesidad de crear múltiples versiones sobrecargadas de un método o pasar arrays explícitamente.
Sintaxis y declaración
Para declarar un método que acepta varargs, se utiliza la sintaxis con tres puntos (...
) después del tipo de dato:
public void metodo(TipoDato... parametro) {
// Implementación
}
Dentro del método, el parámetro varargs se trata como un array del tipo especificado:
public void imprimirNumeros(int... numeros) {
for (int numero : numeros) {
System.out.print(numero + " ");
}
System.out.println();
}
Este método puede ser invocado con cualquier número de argumentos enteros:
imprimirNumeros(); // No imprime nada
imprimirNumeros(5); // Imprime: 5
imprimirNumeros(1, 2, 3); // Imprime: 1 2 3
imprimirNumeros(7, 8, 9, 10); // Imprime: 7 8 9 10
Reglas y restricciones
Al trabajar con varargs, es importante conocer estas reglas:
- Un método puede tener solo un parámetro varargs.
- El parámetro varargs debe ser el último en la lista de parámetros.
- Técnicamente, se puede pasar un array en lugar de argumentos individuales.
// Correcto: varargs como único parámetro
public void metodo1(String... textos) { }
// Correcto: varargs como último parámetro
public void metodo2(int id, String nombre, double... valores) { }
// Incorrecto: varargs no es el último parámetro
public void metodo3(String... textos, int numero) { }
// Incorrecto: múltiples parámetros varargs
public void metodo4(int... numeros, String... textos) { }
Uso con otros tipos de parámetros
Los varargs se pueden combinar con parámetros regulares, siempre que el parámetro varargs sea el último:
public void registrarUsuario(String nombre, int edad, String... intereses) {
System.out.println("Usuario: " + nombre + ", Edad: " + edad);
System.out.println("Intereses: " + String.join(", ", intereses));
}
Este método se puede invocar de varias formas:
registrarUsuario("Ana", 28); // Sin intereses
registrarUsuario("Carlos", 35, "Música"); // Un interés
registrarUsuario("Elena", 42, "Deportes", "Viajes", "Fotografía"); // Varios intereses
Varargs y sobrecarga de métodos
Cuando se trabaja con sobrecarga de métodos y varargs, pueden surgir ambigüedades. Java selecciona el método más específico:
public class DemoVarargs {
// Método con un parámetro específico
public void mostrar(String texto) {
System.out.println("Método con String: " + texto);
}
// Método con varargs
public void mostrar(String... textos) {
System.out.println("Método con varargs: " + Arrays.toString(textos));
}
public static void main(String[] args) {
DemoVarargs demo = new DemoVarargs();
demo.mostrar("Hola"); // Llama al método con un parámetro String
demo.mostrar("Hola", "Mundo"); // Llama al método con varargs
demo.mostrar(); // Llama al método con varargs (array vacío)
}
}
En este ejemplo, cuando se llama a mostrar("Hola")
, Java elige el método más específico, que es el que toma un único String
, no el método varargs.
Paso de arrays a métodos varargs
Se puede pasar un array directamente a un método varargs:
public void sumarNumeros(int... numeros) {
int suma = 0;
for (int num : numeros) {
suma += num;
}
System.out.println("Suma: " + suma);
}
// Uso con argumentos individuales
sumarNumeros(1, 2, 3); // Suma: 6
// Uso con un array
int[] miArray = {4, 5, 6};
sumarNumeros(miArray); // Suma: 15
Sin embargo, hay que tener cuidado con las ambigüedades cuando se mezclan arrays y varargs en sobrecarga de métodos:
// Este método toma un array como parámetro
public void procesar(int[] numeros) {
System.out.println("Método con array");
}
// Este método usa varargs
public void procesar(int... numeros) {
System.out.println("Método con varargs");
}
// ¡Error de compilación! Ambigüedad entre los dos métodos
En este caso, ambos métodos tienen la misma firma desde la perspectiva del compilador, lo que causa un error.
Casos de uso prácticos
Métodos de utilidad para concatenación
public class StringUtils {
public static String concatenar(String delimitador, String... textos) {
if (textos.length == 0) {
return "";
}
StringBuilder resultado = new StringBuilder(textos[0]);
for (int i = 1; i < textos.length; i++) {
resultado.append(delimitador).append(textos[i]);
}
return resultado.toString();
}
}
// Uso
String resultado = StringUtils.concatenar(", ", "Java", "Python", "C++");
System.out.println(resultado); // Java, Python, C++
Operaciones matemáticas flexibles
public class CalculadoraFlexible {
public static int sumar(int... numeros) {
int resultado = 0;
for (int num : numeros) {
resultado += num;
}
return resultado;
}
public static int maximo(int... numeros) {
if (numeros.length == 0) {
throw new IllegalArgumentException("No se proporcionaron números");
}
int max = numeros[0];
for (int i = 1; i < numeros.length; i++) {
if (numeros[i] > max) {
max = numeros[i];
}
}
return max;
}
}
// Uso
int suma = CalculadoraFlexible.sumar(5, 10, 15, 20); // 50
int maximo = CalculadoraFlexible.maximo(8, 3, 12, 7); // 12
Registro y depuración
public class Logger {
public enum Nivel { INFO, WARNING, ERROR }
public static void log(Nivel nivel, String mensaje, Object... detalles) {
StringBuilder sb = new StringBuilder();
sb.append("[").append(nivel).append("] ");
sb.append(mensaje);
if (detalles.length > 0) {
sb.append(" - Detalles: ");
for (int i = 0; i < detalles.length; i++) {
if (i > 0) {
sb.append(", ");
}
sb.append(detalles[i]);
}
}
System.out.println(sb.toString());
}
}
// Uso
Logger.log(Logger.Nivel.INFO, "Aplicación iniciada");
Logger.log(Logger.Nivel.WARNING, "Espacio en disco bajo", "Disponible: 15%", "Ruta: /var/logs");
Logger.log(Logger.Nivel.ERROR, "Conexión fallida", "Servidor: db.ejemplo.com", "Puerto: 5432", "Timeout: 30s");
Implementación de formato personalizado
public class Formateador {
public static String formatear(String formato, Object... args) {
// Versión simplificada de String.format()
StringBuilder resultado = new StringBuilder();
int argIndex = 0;
for (int i = 0; i < formato.length(); i++) {
char c = formato.charAt(i);
if (c == '%' && i + 1 < formato.length() && formato.charAt(i + 1) == 's') {
if (argIndex < args.length) {
resultado.append(args[argIndex]);
argIndex++;
} else {
resultado.append("%s"); // No hay suficientes argumentos
}
i++; // Saltar el siguiente carácter ('s')
} else {
resultado.append(c);
}
}
return resultado.toString();
}
}
// Uso
String mensaje = Formateador.formatear("Hola %s, tienes %s mensajes nuevos.", "María", 5);
System.out.println(mensaje); // Hola María, tienes 5 mensajes nuevos.
Consideraciones de rendimiento
Aunque los varargs son convenientes, hay algunas consideraciones de rendimiento a tener en cuenta:
- Creación de arrays: Cada llamada a un método varargs crea un nuevo array, lo que implica una pequeña sobrecarga.
- Métodos críticos: Para métodos que se llaman con mucha frecuencia en bucles ajustados, considerar versiones sobrecargadas específicas para los casos más comunes.
// Versión con varargs (conveniente pero menos eficiente para casos simples)
public int sumar(int... numeros) {
int suma = 0;
for (int n : numeros) suma += n;
return suma;
}
// Versiones sobrecargadas para casos comunes (más eficientes)
public int sumar(int a, int b) {
return a + b;
}
public int sumar(int a, int b, int c) {
return a + b + c;
}
// La versión varargs se usa para 4 o más argumentos
Varargs con genéricos
Al combinar varargs con genéricos, se debe tener cuidado con las advertencias de seguridad de tipos:
// Genera una advertencia de "unsafe" debido a la combinación de varargs y genéricos
public <T> List<T> crearLista(T... elementos) {
return Arrays.asList(elementos);
}
Para suprimir esta advertencia, se puede usar la anotación @SafeVarargs
:
@SafeVarargs
public final <T> List<T> crearLista(T... elementos) {
return Arrays.asList(elementos);
}
Esta anotación solo se puede aplicar a métodos que:
- No pueden ser sobrescritos (métodos
private
,static
,final
o constructores) - No modifican el array varargs
Ejemplos avanzados
Implementación de un constructor de consultas SQL
public class ConsultaSQL {
private String tabla;
private List<String> condiciones = new ArrayList<>();
public ConsultaSQL(String tabla) {
this.tabla = tabla;
}
public ConsultaSQL donde(String condicion, Object... parametros) {
String condicionFormateada = condicion;
for (Object param : parametros) {
// Reemplazar el primer ? con el valor del parámetro
String valorSeguro = param instanceof String ?
"'" + ((String) param).replace("'", "''") + "'" :
String.valueOf(param);
condicionFormateada = condicionFormateada.replaceFirst("\\?", valorSeguro);
}
condiciones.add(condicionFormateada);
return this;
}
public String construir() {
StringBuilder sql = new StringBuilder("SELECT * FROM ");
sql.append(tabla);
if (!condiciones.isEmpty()) {
sql.append(" WHERE ");
sql.append(String.join(" AND ", condiciones));
}
return sql.toString();
}
}
// Uso
ConsultaSQL consulta = new ConsultaSQL("usuarios")
.donde("edad > ?", 18)
.donde("ciudad = ?", "Madrid")
.donde("activo = ?", true);
String sql = consulta.construir();
System.out.println(sql);
// SELECT * FROM usuarios WHERE edad > 18 AND ciudad = 'Madrid' AND activo = true
Implementación de un sistema de eventos
public class SistemaEventos {
// Interfaz funcional para manejar eventos
@FunctionalInterface
public interface ManejadorEvento {
void manejar(String evento, Object... datos);
}
private Map<String, List<ManejadorEvento>> manejadores = new HashMap<>();
// Registrar un manejador para un tipo de evento
public void registrar(String tipoEvento, ManejadorEvento manejador) {
manejadores.computeIfAbsent(tipoEvento, k -> new ArrayList<>())
.add(manejador);
}
// Disparar un evento con datos variables
public void disparar(String tipoEvento, Object... datos) {
List<ManejadorEvento> eventosManejadores = manejadores.get(tipoEvento);
if (eventosManejadores != null) {
for (ManejadorEvento manejador : eventosManejadores) {
manejador.manejar(tipoEvento, datos);
}
}
}
}
// Uso
SistemaEventos eventos = new SistemaEventos();
// Registrar manejadores
eventos.registrar("login", (evento, datos) -> {
System.out.println("Usuario " + datos[0] + " ha iniciado sesión desde " + datos[1]);
});
eventos.registrar("compra", (evento, datos) -> {
System.out.println("Nueva compra: Producto=" + datos[0] + ", Cantidad=" + datos[1] +
", Total=" + datos[2]);
});
// Disparar eventos
eventos.disparar("login", "usuario123", "192.168.1.5");
eventos.disparar("compra", "Laptop", 1, 1299.99);
Buenas prácticas con varargs
- Usar varargs para simplificar APIs: Cuando un método puede aceptar un número variable de argumentos del mismo tipo, los varargs hacen que la API sea más limpia.
- Validar el contenido: Aunque se permita un número variable de argumentos, a menudo se necesita validar que haya al menos uno o que cumplan ciertas condiciones.
public double calcularPromedio(double... numeros) {
if (numeros.length == 0) {
throw new IllegalArgumentException("Se requiere al menos un número");
}
double suma = 0;
for (double num : numeros) {
suma += num;
}
return suma / numeros.length;
}
- Documentar: Indicar en la documentación que el método acepta un número variable de argumentos y cualquier restricción aplicable.
/**
* Encuentra el valor máximo entre los números proporcionados.
*
* @param numeros Uno o más números entre los que buscar el máximo
* @return El valor máximo encontrado
* @throws IllegalArgumentException Si no se proporciona ningún número
*/
public static int maximo(int... numeros) {
// Implementación
}
- Considerar versiones sobrecargadas para casos comunes: Para métodos de uso frecuente, proporcionar versiones específicas para 1, 2 o 3 argumentos puede mejorar el rendimiento.
- Evitar modificar el array varargs: Aunque técnicamente es posible, modificar el array varargs puede llevar a comportamientos inesperados si el llamador reutiliza el array.
Ejercicios de esta lección Funciones
Evalúa tus conocimientos de esta lección Funciones con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Clases abstractas
Streams: reduce()
Streams: flatMap()
Llamada y sobrecarga de funciones
Métodos referenciados
Métodos de la clase String
Representación de Fecha
Operadores lógicos
Tipos de datos
Estructuras de iteración
Streams: forEach()
Objetos
Funciones lambda
Uso de Scanner
CRUD en Java de modelo Customer sobre un ArrayList
Tipos de variables
Streams: collect()
Operadores aritméticos
Interfaz funcional Consumer
API java.nio 2
API Optional
Interfaz funcional Function
Encapsulación
Interfaces
Uso de API Optional
Representación de Hora
Herencia básica
Clases y objetos
Interfaz funcional Supplier
HashMap
Sobrecarga de métodos
Polimorfismo de tiempo de ejecución
OOP en Java
Creación de Streams
Streams: min max
Métodos avanzados de la clase String
Polimorfismo de tiempo de compilación
Excepciones
Herencia avanzada
Estructuras de selección
Uso de interfaces
HashSet
Objeto Scanner
Streams: filter()
Operaciones de Streams
Interfaz funcional Predicate
Streams: sorted()
Configuración de entorno
CRUD en Java de modelo Customer sobre un HashMap
Uso de variables
Clases
Streams: distinct()
Streams: count()
ArrayList
Datos de referencia
Interfaces funcionales
Métodos básicos de la clase String
Instalación
Funciones
Estructuras de control
Herencia de clases
Streams: map()
Funciones y encapsulamiento
Streams: match
Gestión de errores y excepciones
Datos primitivos
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
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
- Identificar las partes componentes de un método en Java: modificadores, tipo de retorno, nombre del método, parámetros, excepciones, y cuerpo del método
- Comprender cómo los modificadores de acceso influyen en la visibilidad de los métodos desde distintas clases y paquetes
- Diferenciar entre tipos de retorno y cuándo usar el tipo void
- Aprender cómo definir y usar los parámetros en los métodos
- Implementar el manejo de excepciones en las definiciones de métodos
- Aplicar correctamente la sentencia return para finalizar un método y devolver un valor
- Escribir ejemplos prácticos que implementen la buena práctica de diseño de métodos según el principio de responsabilidad única