🏗️ ¿Qué es un constructor en Java?
Un constructor es un bloque de código especial dentro de una clase Java que se ejecuta automáticamente cada vez que se crea un objeto mediante la palabra clave new. Su propósito fundamental es inicializar el estado del objeto, asignando valores a sus atributos y ejecutando cualquier lógica necesaria para que el objeto esté listo para ser utilizado desde el primer momento.
A diferencia de los métodos regulares, un constructor no tiene tipo de retorno (ni siquiera void) y su nombre debe coincidir exactamente con el nombre de la clase, respetando mayúsculas y minúsculas. Estas dos características son las que permiten al compilador de Java distinguir un constructor de un método ordinario.
public class Coche {
private String marca;
private int velocidadMaxima;
// Esto ES un constructor: mismo nombre que la clase, sin tipo de retorno
public Coche(String marca, int velocidadMaxima) {
this.marca = marca;
this.velocidadMaxima = velocidadMaxima;
}
}
// Uso: el constructor se invoca con new
Coche miCoche = new Coche("Toyota", 220);
Cuando la JVM (Java Virtual Machine) ejecuta new Coche("Toyota", 220), suceden tres cosas en secuencia: primero se reserva memoria en el heap para el nuevo objeto, después se ejecuta el constructor que inicializa los atributos marca y velocidadMaxima, y finalmente se devuelve una referencia al objeto recién creado que se almacena en la variable miCoche.
null, 0, false).📐 Sintaxis y reglas fundamentales
La sintaxis de un constructor en Java sigue un patrón preciso que todo programador debe dominar. A continuación se presenta la estructura general y las reglas que el compilador aplica de forma estricta.
[modificador_acceso] NombreDeLaClase([parámetros]) {
// Cuerpo del constructor: inicialización de atributos
}
📋 Reglas que el compilador aplica
| Regla | Descripción | Ejemplo |
|---|---|---|
| Nombre idéntico a la clase | El constructor debe llamarse exactamente igual que la clase, incluyendo mayúsculas | class Persona → public Persona() |
| Sin tipo de retorno | No se declara void, int, ni ningún otro tipo | ✅ Persona() · ❌ void Persona() |
| Modificadores de acceso | Admite public, protected, private o acceso de paquete (sin modificador) | private Persona() para Singleton |
No puede ser static | Los constructores pertenecen a instancias, no a la clase | ❌ static Persona() |
No puede ser final | Los constructores no se heredan, por lo que final no tiene sentido | ❌ final Persona() |
No puede ser abstract | Un constructor siempre debe tener implementación | ❌ abstract Persona() |
| Puede lanzar excepciones | Se puede declarar throws para validaciones | Persona(int edad) throws IllegalArgumentException |
public void Coche(), Java lo interpreta como un método regular llamado Coche que devuelve void, no como un constructor. El objeto se creará con los valores por defecto y tu «constructor» nunca se ejecutará automáticamente.🔧 Constructor por defecto
Cuando escribes una clase sin declarar ningún constructor, el compilador de Java genera automáticamente un constructor por defecto (default constructor). Este constructor no recibe parámetros y no ejecuta ninguna lógica adicional — simplemente permite crear instancias de la clase con new.
// Clase sin constructor explícito
public class Punto {
private int x;
private int y;
// Java genera implícitamente: public Punto() { }
}
// Funciona porque existe el constructor por defecto implícito
Punto origen = new Punto(); // x = 0, y = 0
El constructor por defecto generado automáticamente tiene la misma visibilidad que la clase. Si la clase es public, el constructor será public; si la clase tiene acceso de paquete, el constructor también.
public class Punto {
private int x;
private int y;
// Constructor parametrizado → el por defecto YA NO EXISTE
public Punto(int x, int y) {
this.x = x;
this.y = y;
}
}
// ❌ Error de compilación: no existe Punto()
Punto origen = new Punto();
// ✅ Correcto: usa el constructor que sí existe
Punto p = new Punto(5, 10);
🔹 Valores por defecto de los atributos
Cuando un constructor no asigna valores explícitos a los atributos, Java les asigna sus valores por defecto según su tipo:
| Tipo | Valor por defecto |
|---|---|
int, short, byte, long | 0 |
float, double | 0.0 |
boolean | false |
char | '\u0000' (carácter nulo) |
Referencias a objetos (String, arrays, etc.) | null |
⚙️ Constructor parametrizado
Un constructor parametrizado recibe uno o más argumentos que permiten personalizar el estado inicial del objeto en el momento de su creación. Es el tipo de constructor más utilizado en programación profesional, ya que garantiza que los objetos se creen con datos significativos desde el primer instante.
public class CuentaBancaria {
private String titular;
private double saldo;
private String numeroCuenta;
public CuentaBancaria(String titular, double saldoInicial, String numeroCuenta) {
if (titular == null || titular.isBlank()) {
throw new IllegalArgumentException("El titular no puede estar vacío");
}
if (saldoInicial < 0) {
throw new IllegalArgumentException("El saldo inicial no puede ser negativo");
}
this.titular = titular;
this.saldo = saldoInicial;
this.numeroCuenta = numeroCuenta;
}
public String getTitular() { return titular; }
public double getSaldo() { return saldo; }
public String getNumeroCuenta() { return numeroCuenta; }
}
// Creación del objeto con datos específicos
CuentaBancaria cuenta = new CuentaBancaria("María García", 1500.00, "ES76-0182-1234");
🔄 Sobrecarga de constructores
Al igual que los métodos, los constructores en Java admiten sobrecarga (overloading): una misma clase puede tener múltiples constructores siempre que difieran en el número, tipo u orden de sus parámetros. Esto ofrece flexibilidad al usuario de la clase, permitiéndole crear objetos de distintas formas según la información disponible.
public class Producto {
private String nombre;
private double precio;
private int stock;
private String categoria;
// Constructor completo
public Producto(String nombre, double precio, int stock, String categoria) {
this.nombre = nombre;
this.precio = precio;
this.stock = stock;
this.categoria = categoria;
}
// Constructor sin categoría (valor por defecto)
public Producto(String nombre, double precio, int stock) {
this(nombre, precio, stock, "Sin categoría");
}
// Constructor mínimo: solo nombre y precio
public Producto(String nombre, double precio) {
this(nombre, precio, 0, "Sin categoría");
}
@Override
public String toString() {
return nombre + " - " + precio + "€ (stock: " + stock + ")";
}
}
// Las tres formas de crear un Producto son válidas:
Producto p1 = new Producto("Portátil", 899.99, 50, "Electrónica");
Producto p2 = new Producto("Ratón", 29.99, 200);
Producto p3 = new Producto("Cable HDMI", 12.50);
Observa cómo los constructores más simples delegan en el constructor completo usando this(...). Esto evita duplicar la lógica de inicialización y asegura que toda validación se centralice en un único punto.
🔑 La palabra clave this en constructores
La palabra reservada this tiene dos usos fundamentales dentro de los constructores de Java, y dominar ambos es esencial para escribir código limpio y profesional.
🔹 Uso 1: Diferenciar atributos de parámetros
Cuando un parámetro del constructor tiene el mismo nombre que un atributo de la clase, this permite referirse explícitamente al atributo del objeto actual, eliminando la ambigüedad:
public class Rectangulo {
private double ancho;
private double alto;
public Rectangulo(double ancho, double alto) {
this.ancho = ancho; // this.ancho = atributo; ancho = parámetro
this.alto = alto; // this.alto = atributo; alto = parámetro
}
}
🔹 Uso 2: Llamar a otro constructor de la misma clase
La invocación this(...) permite que un constructor delegue en otro constructor de la misma clase. Esta llamada debe ser la primera instrucción del cuerpo del constructor:
public class Rectangulo {
private double ancho;
private double alto;
// Constructor principal
public Rectangulo(double ancho, double alto) {
if (ancho <= 0 || alto <= 0) {
throw new IllegalArgumentException("Las dimensiones deben ser positivas");
}
this.ancho = ancho;
this.alto = alto;
}
// Constructor para cuadrados: delega en el principal
public Rectangulo(double lado) {
this(lado, lado); // ← Debe ser la PRIMERA instrucción
}
// Constructor por defecto: cuadrado unitario
public Rectangulo() {
this(1.0, 1.0);
}
public double area() { return ancho * alto; }
}
Rectangulo r1 = new Rectangulo(10, 5); // 10×5
Rectangulo r2 = new Rectangulo(7); // 7×7 (cuadrado)
Rectangulo r3 = new Rectangulo(); // 1×1 (unitario)
this() y super() en el mismo constructor, ya que ambas deben ser la primera instrucción. Tampoco puedes crear llamadas circulares (A llama a B y B llama a A): el compilador lo detecta y genera un error.🔗 Encadenamiento de constructores
El encadenamiento de constructores (constructor chaining) es el patrón profesional que consiste en que los constructores más simples delegan en constructores más completos mediante this(...), formando una cadena que termina en un constructor principal que contiene toda la lógica de inicialización y validación.
public class Pedido {
private String cliente;
private String producto;
private int cantidad;
private double descuento;
private boolean urgente;
// Constructor PRINCIPAL (toda la lógica aquí)
public Pedido(String cliente, String producto, int cantidad,
double descuento, boolean urgente) {
if (cliente == null || cliente.isBlank()) {
throw new IllegalArgumentException("Cliente requerido");
}
if (cantidad <= 0) {
throw new IllegalArgumentException("Cantidad debe ser positiva");
}
if (descuento < 0 || descuento > 100) {
throw new IllegalArgumentException("Descuento debe estar entre 0 y 100");
}
this.cliente = cliente;
this.producto = producto;
this.cantidad = cantidad;
this.descuento = descuento;
this.urgente = urgente;
}
// Sin urgencia
public Pedido(String cliente, String producto, int cantidad, double descuento) {
this(cliente, producto, cantidad, descuento, false);
}
// Sin descuento ni urgencia
public Pedido(String cliente, String producto, int cantidad) {
this(cliente, producto, cantidad, 0.0, false);
}
// Pedido unitario
public Pedido(String cliente, String producto) {
this(cliente, producto, 1, 0.0, false);
}
}
Este patrón garantiza que la validación de datos se realice en un único punto. Si mañana necesitas añadir una nueva regla de negocio (por ejemplo, que el descuento máximo sea 50% para ciertos productos), solo modificas el constructor principal y todos los demás se benefician automáticamente.
📋 Constructor copia
Java no proporciona un constructor copia de forma nativa como C++, pero es una práctica habitual implementarlo manualmente. Un constructor copia recibe como parámetro un objeto de la misma clase y crea una nueva instancia independiente con los mismos valores en sus atributos.
public class Direccion {
private String calle;
private String ciudad;
private String codigoPostal;
// Constructor parametrizado
public Direccion(String calle, String ciudad, String codigoPostal) {
this.calle = calle;
this.ciudad = ciudad;
this.codigoPostal = codigoPostal;
}
// Constructor copia
public Direccion(Direccion otra) {
this(otra.calle, otra.ciudad, otra.codigoPostal);
}
// Getters...
public String getCalle() { return calle; }
public String getCiudad() { return ciudad; }
}
// Uso del constructor copia
Direccion original = new Direccion("Gran Vía 1", "Madrid", "28013");
Direccion copia = new Direccion(original);
// Son objetos independientes: modificar uno no afecta al otro
System.out.println(original == copia); // false (referencias distintas)
System.out.println(original.getCalle().equals(copia.getCalle())); // true (mismo valor)
ArrayList o HashMap.import java.util.ArrayList;
import java.util.List;
public class Carrito {
private String cliente;
private List<String> productos;
public Carrito(String cliente) {
this.cliente = cliente;
this.productos = new ArrayList<>();
}
// Constructor copia PROFUNDA: crea nueva lista
public Carrito(Carrito otro) {
this.cliente = otro.cliente;
this.productos = new ArrayList<>(otro.productos); // Nueva lista con mismos elementos
}
public void agregarProducto(String producto) {
productos.add(producto);
}
public List<String> getProductos() { return productos; }
}
Carrito c1 = new Carrito("Ana");
c1.agregarProducto("Teclado");
c1.agregarProducto("Monitor");
Carrito c2 = new Carrito(c1); // Copia profunda
c2.agregarProducto("Ratón"); // Solo afecta a c2
System.out.println(c1.getProductos()); // [Teclado, Monitor]
System.out.println(c2.getProductos()); // [Teclado, Monitor, Ratón]
🧬 Constructores y herencia: super()
Los constructores no se heredan en Java. Una clase hija no dispone automáticamente de los constructores de su clase padre. Sin embargo, toda clase hija debe invocar un constructor de la clase padre para inicializar la parte heredada del objeto. Esto se hace mediante super(...).
▶️ Llamada implícita a super()
Si el constructor de la clase hija no incluye una llamada explícita a super(...), el compilador inserta automáticamente super() sin argumentos como primera instrucción. Esto solo funciona si la clase padre tiene un constructor sin parámetros:
public class Animal {
private String nombre;
public Animal() {
this.nombre = "Sin nombre";
}
public Animal(String nombre) {
this.nombre = nombre;
}
public String getNombre() { return nombre; }
}
public class Perro extends Animal {
private String raza;
// Java inserta automáticamente super() → llama a Animal()
public Perro(String raza) {
// super(); ← insertado implícitamente
this.raza = raza;
}
// Llamada explícita al constructor con parámetro
public Perro(String nombre, String raza) {
super(nombre); // ← Llama a Animal(String nombre)
this.raza = raza;
}
}
Perro p1 = new Perro("Labrador"); // nombre = "Sin nombre", raza = "Labrador"
Perro p2 = new Perro("Rex", "Pastor Alemán"); // nombre = "Rex", raza = "Pastor Alemán"
▶️ Orden de ejecución con herencia
En una jerarquía de herencia, los constructores se ejecutan de padre a hijo. Primero se completa la inicialización de la clase más alta en la jerarquía (en última instancia, Object), y después se ejecutan los constructores de las clases derivadas en orden descendente:
public class A {
public A() { System.out.println("Constructor de A"); }
}
public class B extends A {
public B() { System.out.println("Constructor de B"); }
}
public class C extends B {
public C() { System.out.println("Constructor de C"); }
}
new C();
// Salida:
// Constructor de A
// Constructor de B
// Constructor de C
🚫 Errores frecuentes con constructores
A continuación se documentan los errores más comunes que cometen tanto principiantes como programadores con experiencia al trabajar con constructores en Java.
❌ Error 1: Añadir tipo de retorno al constructor
public class Persona {
private String nombre;
// ❌ INCORRECTO: esto NO es un constructor, es un método
public void Persona(String nombre) {
this.nombre = nombre;
}
}
Persona p = new Persona("Ana"); // Error: no existe constructor con String
❌ Error 2: Olvidar declarar constructor sin parámetros tras crear uno parametrizado
public class Sensor {
private String tipo;
public Sensor(String tipo) {
this.tipo = tipo;
}
}
// ❌ Error de compilación: no existe Sensor()
Sensor s = new Sensor();
// ✅ Solución: añadir constructor sin parámetros explícitamente
// public Sensor() { this("genérico"); }
❌ Error 3: No llamar a super() cuando la clase padre no tiene constructor vacío
public class Vehiculo {
private String matricula;
// Único constructor: requiere matrícula
public Vehiculo(String matricula) {
this.matricula = matricula;
}
}
public class Moto extends Vehiculo {
private int cilindrada;
// ❌ Error: el compilador inserta super() pero Vehiculo() no existe
public Moto(int cilindrada) {
this.cilindrada = cilindrada;
}
// ✅ Correcto: llamada explícita a super(String)
// public Moto(String matricula, int cilindrada) {
// super(matricula);
// this.cilindrada = cilindrada;
// }
}
❌ Error 4: Encadenamiento circular de constructores
public class Dato {
// ❌ Error de compilación: recursive constructor invocation
public Dato() {
this(0);
}
public Dato(int valor) {
this(); // Llama al anterior, que llama a este → bucle infinito
}
}
✅ Buenas prácticas profesionales
Las siguientes recomendaciones provienen de la experiencia acumulada en proyectos Java empresariales y de las guías de estilo de referencia como Effective Java de Joshua Bloch.
| Práctica | Descripción |
|---|---|
| Validar siempre los parámetros | Comprueba null, rangos, formatos. Lanza IllegalArgumentException o NullPointerException con mensaje descriptivo. |
| Usar encadenamiento con this() | Centraliza la lógica de inicialización en un constructor principal. Los demás delegan en él. |
| Preferir constructor sobre setters para campos obligatorios | Los campos esenciales deben establecerse en el constructor, no mediante setters que podrían no invocarse. |
Declarar atributos como final cuando sea posible | Un atributo final debe inicializarse en el constructor y no puede cambiar después. Esto favorece la inmutabilidad. |
| Considerar el patrón Builder para muchos parámetros | Si un constructor tiene más de 4-5 parámetros, el patrón Builder ofrece mejor legibilidad y flexibilidad. |
| No realizar trabajo pesado en el constructor | Evita conexiones a bases de datos, llamadas a red o lecturas de ficheros. Usa métodos de inicialización separados. |
| Documentar con Javadoc | Los constructores públicos deben tener Javadoc que describa los parámetros y las excepciones posibles. |
final y la clase no tiene setters), los constructores son la única forma de establecer el estado del objeto. Esto es un patrón de diseño muy valorado en programación concurrente porque los objetos inmutables son inherentemente thread-safe.🏢 Ejemplo integrador: sistema de gestión de empleados
Este ejemplo reúne todos los conceptos vistos en el artículo: constructor por defecto, parametrizado, sobrecarga, encadenamiento con this(), herencia con super(), constructor copia y validación de datos.
// ─── Clase base: Empleado ───────────────────────────────────
public class Empleado {
private final String nombre;
private final String dni;
private double salarioBase;
private String departamento;
// Constructor principal con validación
public Empleado(String nombre, String dni, double salarioBase, String departamento) {
if (nombre == null || nombre.isBlank()) {
throw new IllegalArgumentException("El nombre no puede estar vacío");
}
if (dni == null || !dni.matches("\\d{8}[A-Z]")) {
throw new IllegalArgumentException("DNI inválido: debe ser 8 dígitos + letra");
}
if (salarioBase < 0) {
throw new IllegalArgumentException("El salario no puede ser negativo");
}
this.nombre = nombre;
this.dni = dni;
this.salarioBase = salarioBase;
this.departamento = (departamento != null) ? departamento : "Sin asignar";
}
// Constructor sin departamento
public Empleado(String nombre, String dni, double salarioBase) {
this(nombre, dni, salarioBase, "Sin asignar");
}
// Constructor copia
public Empleado(Empleado otro) {
this(otro.nombre, otro.dni, otro.salarioBase, otro.departamento);
}
// Getters
public String getNombre() { return nombre; }
public String getDni() { return dni; }
public double getSalarioBase() { return salarioBase; }
public String getDepartamento() { return departamento; }
public double calcularSalarioMensual() {
return salarioBase;
}
@Override
public String toString() {
return String.format("%s (DNI: %s) - Dpto: %s - Salario: %.2f€",
nombre, dni, departamento, calcularSalarioMensual());
}
}
// ─── Clase hija: Gerente ────────────────────────────────────
public class Gerente extends Empleado {
private double bonificacion;
private int empleadosACargo;
public Gerente(String nombre, String dni, double salarioBase,
String departamento, double bonificacion, int empleadosACargo) {
super(nombre, dni, salarioBase, departamento);
if (bonificacion < 0) {
throw new IllegalArgumentException("La bonificación no puede ser negativa");
}
this.bonificacion = bonificacion;
this.empleadosACargo = empleadosACargo;
}
public Gerente(String nombre, String dni, double salarioBase, String departamento) {
this(nombre, dni, salarioBase, departamento, 0.0, 0);
}
@Override
public double calcularSalarioMensual() {
return getSalarioBase() + bonificacion;
}
@Override
public String toString() {
return super.toString() + String.format(" [Gerente - %d empleados a cargo]",
empleadosACargo);
}
}
// ─── Clase hija: Becario ────────────────────────────────────
public class Becario extends Empleado {
private int horasSemanales;
private String universidad;
public Becario(String nombre, String dni, double salarioBase,
String departamento, int horasSemanales, String universidad) {
super(nombre, dni, salarioBase, departamento);
if (horasSemanales <= 0 || horasSemanales > 40) {
throw new IllegalArgumentException("Horas semanales deben estar entre 1 y 40");
}
this.horasSemanales = horasSemanales;
this.universidad = (universidad != null) ? universidad : "No especificada";
}
public Becario(String nombre, String dni, int horasSemanales, String universidad) {
this(nombre, dni, 600.0, "Formación", horasSemanales, universidad);
}
@Override
public double calcularSalarioMensual() {
return getSalarioBase() * horasSemanales / 40.0;
}
@Override
public String toString() {
return super.toString() + String.format(" [Becario - %dh/semana - %s]",
horasSemanales, universidad);
}
}
// ─── Programa principal ─────────────────────────────────────
public class Main {
public static void main(String[] args) {
Empleado emp = new Empleado("Laura Martín", "12345678A", 2400.0, "Desarrollo");
Gerente ger = new Gerente("Carlos Ruiz", "87654321B", 4500.0, "Dirección", 1200.0, 15);
Becario bec = new Becario("Sofía López", "11223344C", 20, "Universidad Complutense");
// Constructor copia
Empleado copiaEmp = new Empleado(emp);
System.out.println(emp);
System.out.println(ger);
System.out.println(bec);
System.out.println("Copia: " + copiaEmp);
System.out.println("¿Son el mismo objeto? " + (emp == copiaEmp)); // false
}
}
Este ejemplo muestra cómo una jerarquía de clases bien diseñada utiliza constructores encadenados, validación en cada nivel, delegación con super() y el polimorfismo en calcularSalarioMensual() para producir código profesional, mantenible y extensible.
📝 Ejercicios resueltos
Ejercicio 1: Clase Libro con múltiples constructores
Crea una clase Libro con los atributos titulo (String), autor (String), paginas (int) y isbn (String). Implementa tres constructores: uno completo, uno sin ISBN (valor por defecto "000-0-00-000000-0") y uno que solo reciba título y autor (con páginas = 0). Usa encadenamiento de constructores. Incluye un constructor copia.
Ejercicio 2: Jerarquía Figura → Circulo → Cilindro
Crea la clase base Figura con un atributo color (String). Luego crea Circulo que extienda Figura añadiendo radio (double). Finalmente, crea Cilindro que extienda Circulo añadiendo altura (double). Cada clase debe llamar a super() correctamente. Incluye un método volumen() en Cilindro.
Ejercicio 3: Clase inmutable Coordenada con constructor copia
Diseña una clase Coordenada totalmente inmutable (todos los atributos final, sin setters) con atributos x, y y z (double). Implementa un constructor completo, uno para coordenadas 2D (z = 0), uno para el origen (0, 0, 0) y un constructor copia. Añade un método distanciaA(Coordenada otra) que calcule la distancia euclidiana en 3D.
❓ Preguntas frecuentes sobre Constructores en Java
Las dudas más comunes respondidas de forma clara y directa.
💬 Foro de discusión
¿Tienes dudas sobre Constructores en Java? Comparte tu pregunta con la comunidad.
Todavía no hay mensajes. ¡Sé el primero en participar!