Fundamentos
Métodos
Firmas, sobrecarga, paso por valor y métodos que hacen una sola cosa.
Desde Java 8
Este capítulo cubre Java 8. Estás leyendo en .
Un método es un contrato con nombre
public static int maximo(int a, int b) {
return a >= b ? a : b;
}Cuatro piezas: modificadores (public static), tipo de retorno (int), nombre (maximo) y
parámetros (int a, int b). Si no devuelve nada, el tipo es void.
La firma de un método es el nombre más los tipos de sus parámetros, y nada más. El tipo de
retorno no forma parte de la firma, los nombres de los parámetros tampoco, y throws no
cuenta. Esa definición es la que decide qué es una sobrecarga y qué es un choque:
int calcular(int x) { return x; }
long calcular(int x) { return x; } // no compila: misma firma, distinto retornoPaso por valor, siempre
Java pasa siempre por valor. Lo que pasa es que el valor de un objeto es su referencia:
static void cambiar(int numero) {
numero = 99; // cambia la copia local
}
static void anadir(StringBuilder texto) {
texto.append(" mundo"); // cambia el objeto al que apunta la referencia
}
static void reemplazar(StringBuilder texto) {
texto = new StringBuilder("otro"); // cambia la copia de la referencia
}int n = 1;
cambiar(n); // n sigue valiendo 1
StringBuilder texto = new StringBuilder("hola");
anadir(texto); // "hola mundo"
reemplazar(texto); // sigue siendo "hola mundo": solo se movió la copiaQué viaja en la llamada
-
01
Llamador
StringBuilder textoSu variable no se toca nunca. -
02
Copia
la referenciaSe inicializa el parámetro con el valor recibido. -
03
Objeto
"hola"Compartido: si lo mutan, lo ves.
final en un parámetro impide reasignarlo dentro del método y no cambia nada para quien llama.
Es documentación para el que lee, no un mecanismo de protección.
Sobrecarga: tres pasadas del compilador
Cuando hay varios métodos con el mismo nombre, el compilador elige en tres fases y se queda con la primera que encuentre candidatos:
- Coincidencia estricta, sin boxing: solo cuentan la identidad y el ensanchamiento de tipos.
- Coincidencia laxa: ya se permite boxing y unboxing.
- Métodos de aridad variable (
varargs).
static void f(long valor) { System.out.println("long"); }
static void f(Integer valor) { System.out.println("Integer"); }
static void f(int... valores) { System.out.println("varargs"); }
f(1); // long1 es un int, que puede ir a long sin boxing, así que gana la primera fase. Si solo
existieran f(Integer) y f(int...), ganaría f(Integer) por la fase 2, y los varargs
quedarían de último recurso. Ese orden es la razón de que añadir una sobrecarga con varargs
nunca rompa las llamadas existentes.
Cuando dos candidatos empatan en la fase ganadora, no hay preferencia: es un error de
compilación. El caso clásico es f(null), que encaja en cualquier tipo referencia:
static void g(String s) { }
static void g(Integer i) { }
g(null); // no compila: referencia ambiguavarargs: azúcar sobre un array
static int suma(int... valores) {
int total = 0;
for (int valor : valores) {
total += valor;
}
return total;
}
System.out.println(suma()); // 0
System.out.println(suma(1, 2, 3)); // 6
System.out.println(suma(new int[] {4, 5})); // 9Dentro del método, valores es un int[] normal. Tres consecuencias prácticas: el parámetro
varargs va siempre al final, solo puede haber uno, y no se puede declarar junto a otra
sobrecarga que reciba el array equivalente, porque tienen la misma firma:
static void h(int... valores) { }
static void h(int[] valores) { } // no compila: firma repetidamain(String[] args) es exactamente ese caso escrito a mano.
static o de instancia
Un método static pertenece a la clase y no tiene this: no puede leer campos de instancia ni
llamar a métodos de instancia sin un objeto.
class Contador {
private int total;
void sumar(int cantidad) {
total += cantidad; // campo de instancia
}
static int doble(int valor) {
return valor * 2; // no toca estado
}
}La regla práctica: si el método no necesita el estado del objeto, que sea static. Y si el
estado cambia, el método de instancia es el que tiene que existir.
Guardas antes que anidamiento
Un método se lee de arriba abajo. Las comprobaciones van primero y salen pronto:
static double promedio(List<Integer> notas) {
Objects.requireNonNull(notas, "notas");
if (notas.isEmpty()) {
return 0;
}
int suma = 0;
for (int nota : notas) {
suma += nota;
}
return (double) suma / notas.size();
}La alternativa con if anidados obliga a leer hasta el final para descubrir los casos
especiales. Objects.requireNonNull lanza NullPointerException con tu mensaje y sirve de
contrato explícito en la primera línea del método.
Recursión: cuando el problema se contiene a sí mismo
static long factorial(int n) {
if (n <= 1) {
return 1; // caso base: sin él, no termina
}
return n * factorial(n - 1);
}Todo método recursivo necesita dos cosas: un caso base y un argumento que se acerque a él. Cada
llamada ocupa un marco en la pila, así que una recursión de un millón de niveles termina en
StackOverflowError antes que en resultado. Para recorrer estructuras de datos profundas,
iteración o una pila explícita.
Nombres y tamaño
- Un método hace una cosa, y su nombre la dice:
calcularTotal, noprocesar. - El nombre empieza por verbo; si empieza por sustantivo, probablemente debería ser un getter o un campo.
- Los parámetros se leen en la llamada:
enviar(factura, true)no dice nada,enviar(factura, URGENTE)sí. - Si un método necesita seis parámetros, casi siempre hay un objeto escondido ahí dentro.
- Un método de treinta líneas se lee; uno de doscientas pantallas no.
Qué te llevas
- La firma es el nombre más los tipos de los parámetros: el retorno no cuenta.
- Java pasa siempre por valor; en los objetos, el valor es la referencia.
- La sobrecarga se resuelve en tres fases: estricta, con boxing, y al final
varargs. - Dos candidatos empatados son un error de compilación, como en
g(null). varargses un array con azúcar, va al final y choca con la firma del array equivalente.staticcuando no hay estado; de instancia cuando lo hay.- Las guardas al principio dejan el camino feliz sin anidamiento.
- La recursión necesita caso base y avance hacia él, y consume pila.