Todas las entradas de: santiagohiguera

Acerca de santiagohiguera

Santiago Higuera de Frutos: Profesor Permanente Laboral en el Departamento de Ingeniería Telemática y Electrónica de la Universidad Politécnica de Madrid

Clases abstractas e interfaces en Java: lo que un objeto es frente a lo que puede hacer

Clases abstractas e interfaces: de qué estamos hablando

Cuando diseñamos una jerarquía de clases en Java, en ocasiones necesitamos definir un tipo padre que no tenga sentido instanciar directamente, sino que sirva como modelo para las clases que deriven de él. Java ofrece dos herramientas distintas para esto: las clases abstractas y las interfaces. Aunque a primera vista pueden parecer intercambiables, tienen diferencias importantes que conviene entender bien antes de elegir entre ellas.

Clases abstractas

Una clase abstracta es una clase que no puede instanciarse directamente. Se declara con la palabra clave abstract y puede contener:

  • Atributos de instancia (estado).
  • Constructores.
  • Métodos concretos (con implementación).
  • Métodos abstractos (sin implementación), que las subclases están obligadas a implementar.

El ejemplo que ya conoces de clase es la jerarquía de figuras geométricas:

public abstract class Figura {
   private String color;           // ← atributo de instancia

   public Figura(String color) {   // ← constructor
       this.color = color;
  }

   public String getColor() {      // ← método concreto
       return color;
  }

   public abstract double area();        // ← método abstracto
   public abstract double perimetro();   // ← método abstracto
}

Las subclases deben implementar obligatoriamente todos los métodos abstractos:

public class Circulo extends Figura {
   private double radio;

   public Circulo(String color, double radio) {
       super(color);
       this.radio = radio;
  }

   @Override
   public double area() {
       return Math.PI * radio * radio;
  }

   @Override
   public double perimetro() {
       return 2 * Math.PI * radio;
  }
}

Si una subclase no implementa todos los métodos abstractos, Java obliga a declararla también como abstract. Hay que tener en cuenta además que, en Java, una clase solo puede extender una única clase padre, sea esta abstracta o concreta. Esta restricción, conocida como herencia simple, tendrá importancia cuando hagamos la comparación con los interfaces.

Interfaces

Un interfaz es una declaración de comportamiento sin implementación. Se declara con la palabra clave interface y en su forma clásica solo puede contener:

  • Métodos abstractos (implícitamente son public abstract).
  • Constantes (implícitamente son public static final).

Una clase implementa un interfaz con la palabra clave implements:

public interface Dibujable {
   void dibujar();         // ← implícitamente public abstract
}

public interface Redimensionable {
   void redimensionar(double factor); // ← implícit. public abstract
}

Una misma clase puede implementar varias interfaces a la vez, lo que no es posible con la herencia de clases:

class Circulo extends Figura implements Dibujable, Redimensionable {
   private double radio;

   public Circulo(String color, double radio) {
       super(color);
       this.radio = radio;
  }

   @Override
public double area() { return Math.PI * radio * radio; }

   @Override
public double perimetro() {return 2 * Math.PI * radio; }

   @Override
public void dibujar()     {
System.out.println("Dibujando círculo");
}

   @Override
public void redimensionar(double factor) {
       this.radio *= factor;
  }
}

📎 Java 8 y los métodos default: a partir de Java 8, las interfaces pueden incluir métodos con implementación, declarados con la palabra clave default. Esto redujo parte de la distancia histórica entre interfaces y clases abstractas. Sin embargo, las interfaces siguen sin poder tener atributos de instancia ni constructores, lo que mantiene vigente la distinción fundamental entre ambas.

La tabla comparativa de partida

Clase abstractaInterfaz
InstanciaciónNo se puede instanciar directamenteNo se puede instanciar directamente
Atributos de instanciaNo
ConstructoresNo
Métodos concretosSolo con default (Java 8+)
Métodos abstractosSí (todos, por defecto)
Herencia/implementaciónUna sola clase padreMúltiples interfaces
Relación que modela“es-un” con estado compartidoContrato de comportamiento

Con estas dos herramientas claras, la pregunta es cuándo elegir cada una. Eso es lo que veremos en los siguientes apartados.


Herencia de implementación y herencia de tipos

Aunque son mecanismos distintos, tanto extender una clase abstracta como implementar un interfaz son formas de herencia en Java y es útil distinguirlas por su nombre:

  • Herencia de implementación (class inheritance): cuando una clase extiende una clase abstracta (o concreta). La subclase hereda tanto el contrato (los métodos) como la implementación (el código) de la clase padre.
  • Herencia de tipos (interface inheritance): cuando una clase implementa un interfaz. La clase hereda únicamente el contrato, sin ninguna implementación.

Ambas formas de herencia comparten una propiedad fundamental: el dynamic dispatch o asociación dinámica. Esto significa que, cuando llamamos a un método a través de una referencia del tipo padre, Java decide en tiempo de ejecución qué implementación ejecutar según el tipo real del objeto. El resultado es el mismo independientemente de si el tipo padre es una clase abstracta o un interfaz:

// Con clase abstracta
Figura f1 = new Circulo("rojo", 5.0);
Figura f2 = new Rectangulo("azul", 4.0, 3.0);
f1.area();   // ejecuta Circulo.area()
f2.area();   // ejecuta Rectangulo.area()

// Con interfaz
Dibujable d1 = new Circulo("rojo", 5.0);
Dibujable d2 = new Rectangulo("azul", 4.0, 3.0);
d1.dibujar();   // ejecuta Circulo.dibujar()
d2.dibujar();   // ejecuta Rectangulo.dibujar()

En ambos casos, el código que llama al método no sabe ni necesita saber qué tipo concreto tiene el objeto. Esa es la esencia del polimorfismo y funciona igual con los dos mecanismos.

Esta equivalencia es importante: la elección entre clase abstracta e interfaz no afecta al polimorfismo. Lo que cambia son las capacidades del tipo padre (si puede tener estado, constructores, lógica compartida) y las restricciones que impone sobre las clases que lo utilizan. Esos son precisamente los criterios que veremos a continuación.


La restricción más importante: herencia simple

Java permite que una clase extienda una sola clase padre. Esto es la herencia simple y se aplica tanto a clases concretas como a clases abstractas. En cambio, una clase puede implementar múltiples interfaces sin ninguna restricción.

Esta asimetría tiene consecuencias prácticas inmediatas. Veamos un ejemplo concreto.

Supongamos que estamos desarrollando un videojuego y tenemos la siguiente jerarquía:

public abstract class Personaje {
   protected String nombre;
   protected int vida;

   public Personaje(String nombre, int vida) {
       this.nombre = nombre;
       this.vida   = vida;
  }

   public abstract void atacar();
}

Ahora queremos crear un Mago que extienda Personaje. Sin problema:

public class Mago extends Personaje {
   public Mago(String nombre) {
       super(nombre, 100);
  }

   @Override
   public void atacar() {
       System.out.println(nombre + " lanza un hechizo.");
  }
}

Pero resulta que en nuestro juego algunos personajes pueden volar y otros pueden nadar. Queremos que el Mago pueda hacer ambas cosas. Si intentamos resolverlo con clases abstractas, el diseño se bloquea:

public abstract class Volador {
   public abstract void volar();
}

public abstract class Nadador {
   public abstract void nadar();
}

// ¡Esto no compila! Java no permite extender dos clases a la vez.
public class Mago extends Personaje, Volador, Nadador {
  ...
}

Java no lo permite. Hemos “gastado” la herencia en Personaje, y no podemos extender nada más. Con interfaces, el problema desaparece:

public interface Volador {
   void volar();
}

public interface Nadador {
   void nadar();
}

// Esto sí compila: una clase padre y los interfaces que necesitemos.
public class Mago extends Personaje implements Volador, Nadador {

   public Mago(String nombre) {
       super(nombre, 100);
  }

   @Override
   public void atacar() {
       System.out.println(nombre + " lanza un hechizo.");
  }

   @Override
   public void volar() {
       System.out.println(nombre + " vuela sobre las nubes.");
  }

   @Override
   public void nadar() {
       System.out.println(nombre + " nada bajo el agua.");
  }
}

El patrón es claro: la clase abstracta define lo que un objeto es (un Personaje, con su estado y su lógica común), mientras que los interfaces definen lo que un objeto puede hacer (volar, nadar, dibujarse, serializarse, compararse). Esta distinción es la clave para entender cuándo usar cada herramienta.


Cuándo sí tiene sentido una clase abstracta

La clase abstracta es la herramienta adecuada cuando el tipo padre necesita algo más que declarar un contrato:

  • Cuando tiene un estado propio que compartir con las subclases.
  • Cuando hay un constructor con una lógica común que no tiene sentido duplicar.
  • Cuando se quiere que las clases derivadas sigan los pasos de un algoritmo que deben ejecutarse siempre en el mismo orden.

Cuando hay estado compartido entre las subclases

Si varias subclases necesitan los mismos atributos, lo natural es declararlos en la clase abstracta y dejar que las subclases los hereden. Un interfaz no puede hacer esto porque no admite atributos de instancia.

En el ejemplo de figuras geométricas que ya conoces, todas las figuras tienen un color. Declararlo en la clase abstracta evita repetirlo en cada subclase:

public abstract class Figura {
   private String color;

   public Figura(String color) {
       this.color = color;
  }

   public String getColor() {
       return color;
  }

   public abstract double area();
   public abstract double perimetro();
}

Si Figura fuera un interfaz, cada subclase tendría que gestionar el color por su cuenta, duplicando código innecesariamente.

Cuando hay un constructor con lógica común

Relacionado con lo anterior: si la inicialización de los atributos compartidos requiere alguna lógica, como validaciones, transformaciones o inicializaciones complejas, esa lógica puede centralizarse en el constructor de la clase abstracta y reutilizarse mediante super() en todas las subclases.

public abstract class Figura {
   private String color;

   public Figura(String color) {
       if (color == null || color.isEmpty()) {
           throw new IllegalArgumentException(
"El color no puede estar vacío.");
      }
       this.color = color;
  }
}

Todas las subclases que llamen a super(color) se benefician automáticamente de esa validación sin tener que repetirla.

Cuando se quiere definir un algoritmo con pasos fijos

Este es el caso de uso más característico de las clases abstractas: el patrón Template Method. La idea es que la clase abstracta define la estructura de un algoritmo en un método concreto, pero delega algunos de sus pasos a las subclases mediante métodos abstractos.

Imaginemos que queremos modelar el proceso de preparar una bebida caliente. Los pasos son siempre los mismos: hervir agua, preparar la bebida, servir. Sin embargo, el paso de preparar la bebida varía según si es té o café:

public abstract class BebidaCaliente {
   private String nombre;

   public BebidaCaliente(String nombre) {
       this.nombre = nombre;
  }

   // Método plantilla: define el algoritmo completo
   public final void preparar() {
       hervirAgua();
       prepararBebida(); // ← variable, lo implementa cada subclase
       servir();
  }

   private void hervirAgua() {
       System.out.println("Hirviendo agua...");
  }

   protected abstract void prepararBebida(); // ← cada subclase

   private void servir() {
       System.out.println("Sirviendo " + nombre + ".");
  }
}

public class Cafe extends BebidaCaliente {
   public Cafe() {
       super("café");
  }

   @Override
   protected void prepararBebida() {
       System.out.println("Añadiendo café molido y filtrando.");
  }
}

public class Te extends BebidaCaliente {
   public Te() {
       super("té");
  }

   @Override
   protected void prepararBebida() {
       System.out.println("Añadiendo la bolsita de té y dejando reposar.");
  }
}

El uso sería:

BebidaCaliente bebida = new Cafe();
bebida.preparar();

Salida:

Hirviendo agua...
Añadiendo café molido y filtrando.
Sirviendo café.

Un interfaz no puede implementar este patrón en su forma clásica, porque no puede tener un método concreto que llame a métodos abstractos propios con garantía de orden y control. La clase abstracta es aquí la única herramienta adecuada.


Cuándo el interfaz es claramente superior

El interfaz es la herramienta adecuada cuando lo que queremos definir es un contrato de comportamiento sin imponer ninguna jerarquía ni compartir estado. Hay tres situaciones en las que el interfaz gana claramente a la clase abstracta:

  • Cuando una clase necesita cumplir varios contratos a la vez.
  • Cuando queremos añadir comportamiento a clases ya existentes.
  • Cuando queremos definir tipos para composición.

Cuando una clase necesita cumplir varios contratos a la vez

Ya lo vimos en la sección anterior: Java solo permite extender una clase, pero permite implementar múltiples interfaces. Siempre que una clase necesite comprometerse con más de un comportamiento independiente, el interfaz es la única opción viable.

Este es el caso más frecuente en la práctica y el argumento más inmediato para preferir interfaces a clases abstractas cuando no hay estado que compartir.

Cuando queremos añadir comportamiento a clases ya existentes

Supongamos que tenemos una clase Factura que ya extiende otra clase y no podemos cambiar su jerarquía. Si queremos que Factura sea comparable, para poder ordenar facturas por importe, por ejemplo, basta con implementar el interfaz Comparable:

public class Factura extends Documento implements Comparable<Factura> {
   private double importe;

   public Factura(double importe) {
       this.importe = importe;
  }

   @Override
   public int compareTo(Factura otra) {
       return Double.compare(this.importe, otra.importe);
  }
}

Con una clase abstracta esto sería imposible: Factura ya tiene su clase padre y no puede tener otra. El interfaz permite añadir capacidades a una clase existente sin tocar su jerarquía: encajar un interfaz en una clase que ya existe es sencillo; encajar una clase abstracta en una jerarquía ya establecida es, en general, imposible. A este mecanismo, Bloch lo llamó retroadaptación (retrofitting).

Cuando queremos definir tipos para composición

En ocasiones una clase no necesita ser algo, sino poder hacer algo. Y ese “poder hacer” puede combinarse con otras capacidades de forma flexible. En estos casos, el interfaz es la herramienta natural.

Imaginemos que estamos modelando vehículos que pueden desplazarse de diferentes maneras. Podríamos definir el comportamiento de desplazamiento como un interfaz:

public interface ModoDeDesplazamiento {
   void desplazarse();
}

public class Rodadura implements ModoDeDesplazamiento {
   @Override
   public void desplazarse() {
       System.out.println("Rodando por la carretera.");
  }
}

public class Navegacion implements ModoDeDesplazamiento {
   @Override
   public void desplazarse() {
       System.out.println("Navegando por el agua.");
  }
}

public class Vehiculo {
   private ModoDeDesplazamiento modo;

   public Vehiculo(ModoDeDesplazamiento modo) {
       this.modo = modo;
  }

   public void setModo(ModoDeDesplazamiento modo) {
       this.modo = modo;
  }

   public void desplazarse() {
       modo.desplazarse();
  }
}

El Vehiculo no sabe ni le importa si rueda o navega: solo conoce el interfaz ModoDeDesplazamiento y delega en él. Esto permite cambiar el comportamiento en tiempo de ejecución sin modificar la clase Vehiculo:

Vehiculo anfibio = new Vehiculo(new Rodadura());
anfibio.desplazarse();           // Rodando por la carretera.
anfibio.setModo(new Navegacion());
anfibio.desplazarse();           // Navegando por el agua.

ModoDeDesplazamiento podría haber sido una clase abstracta, pero entonces Rodadura y Navegacion habrían gastado su única herencia en ella, bloqueando cualquier otra jerarquía. Como interfaz, no impone ninguna restricción y el diseño queda abierto a cualquier combinación futura.

📎 La regla práctica de Bloch: en Effective Java, Joshua Bloch resume la preferencia por los interfaces con una pregunta sencilla: ¿necesita el tipo padre tener estado o lógica compartida? Si la respuesta es no, usa un interfaz. Si la respuesta es sí, considera una clase abstracta, o mejor aún, combina ambas herramientas con el patrón de implementación esquelética, que se comenta más adelante y seguramente desarrollaremos en un artículo posterior.

📎 Patrón Strategy: el diseño utilizado en el ejemplo tiene nombre propio en la literatura del software. Se llama patrón Strategy y es uno de los 23 patrones del libro de la Banda de los Cuatro (GoF). Lo reconocerás siempre por sus tres elementos: un interfaz que define el comportamiento intercambiable (la estrategia), varias clases que lo implementan (las estrategias concretas) y una clase que las usa delegando en ellas (el contexto). En nuestro ejemplo, ModoDeDesplazamiento es la estrategia, Rodadura y Navegacion son las estrategias concretas, y Vehiculo es el contexto.


Guía de decisión

¿El tipo padre necesita atributos de instancia o constructores con lógica común?

├── SÍ → Usa una CLASE ABSTRACTA
│      │
│      └── ¿Además necesitas cumplir varios contratos?
│            │
│            ├── SÍ → CLASE ABSTRACTA + INTERFACES adicionales
│            │
│            └── NO → Usa solo CLASE ABSTRACTA

└── NO → ¿Necesitas un algoritmo con pasos fijos y variables?
      │
      ├── SÍ → Usa una CLASE ABSTRACTA (patrón Template Method)
      │
      └── NO → Usa un INTERFAZ
            │
            └── ¿Implementación con partes comunes reutilizables?
                  │
                  ├── SÍ → INTERFAZ + CLASE ABSTRACTA esquelética
                  │
                  └── NO → Usa solo el INTERFAZ

📎 Nota: la combinación INTERFAZ + CLASE ABSTRACTA esquelética es un patrón especialmente potente que merece explicación detallada. Lo desarrollaremos en un artículo posterior.

📎 El patrón en la API de Java: si estás estudiando las colecciones de Java, ya has visto este patrón en acción. List es el interfaz que define el contrato, AbstractList es la clase abstracta esquelética que implementa la parte común y ArrayList es la implementación concreta. Lo mismo ocurre con Map y AbstractMap, o con Set y AbstractSet. Es uno de los patrones de diseño más utilizados en la propia API de Java.


Conclusión

Las clases abstractas y los interfaces son dos herramientas distintas para un problema similar: definir un tipo padre que sirva de modelo para otras clases. Elegir bien entre ellas no es una cuestión de estilo sino de diseño.

La regla general es sencilla: usa una clase abstracta cuando el tipo padre necesite compartir estado o lógica común con sus subclases, y usa un interfaz cuando solo necesites definir un contrato de comportamiento. En la práctica, ambas herramientas se complementan con frecuencia: el interfaz define el tipo y la clase abstracta proporciona una implementación parcial reutilizable.

Hay una pregunta que conviene hacerse siempre antes de decidir: ¿estoy definiendo lo que un objeto es, o lo que un objeto puede hacer? Si la respuesta es lo primero, la clase abstracta es probablemente la herramienta adecuada. Si es lo segundo, el interfaz es la elección natural.

Una vez clara la diferencia entre estas dos herramientas, el siguiente paso es preguntarse cuándo conviene usar herencia, sea de clase abstracta o de interfaz y cuándo es mejor usar composición. Esa es precisamente la pregunta que abordaremos en el próximo artículo.


Bárbara Liskov y el Principio de Sustitución de Liskov

Bárbara Liskov es una informática y matemática nacida en California en 1939, famosa por sus contribuciones en el mundo de los lenguajes de programación.

La profesora Liskov pertenece a la National Academy of Engineering de los Estados Unidos. En 2004 ganó la Medalla John Von Neumann, premio de ciencias de computación establecido por la dirección del IEEE, por «su contribución fundamental a los lenguajes de programación, metodologías de programación y sistemas distribuidos».

En 2008 se convirtió en la segunda mujer en la historia en ganar el premio Turing, considerado el Nóbel en ciencias de computación, por «su contribución a los fundamentos teóricos y prácticos en el diseño de lenguajes de programación y sistemas, especialmente relacionados con la abstracción de datos, tolerancia a fallos y computación distribuida».

En 2018, la Universidad Politécnica de Madrid le otorgó un doctorado honoris causa.

Además de sus artículos científicos, Bárbara Liskov ha publicado varios libros. De ellos, merece la pena mencionar en este contexto su libro “Program Development in Java: Abstraction, Specification, and Object-Oriented Design“. Se trata de un libro fácil de leer y con el que se puede aprender mucho acerca del diseño de aplicaciones en Java.

Quizás, la contribución a la programación por la que es más conocida es por su Principio de Sustitución de Liskov (Liskov Substitution Principle, LSP). El principio fue introducido por primera vez por Barbara Liskov en una conferencia en 1987, titulada “Data Abstraction and Hierarchy”. El principio fue reformulado y publicado formalmente en un artículo conjunto de Barbara Liskov y Jeannette Wing en 1994. Ese artículo se titula “A Behavioral Notion of Subtyping“, publicado en ACM Transactions on Programming Languages and Systems (TOPLAS), y es donde aparece la definición matemática que se cita habitualmente:

“Let Φ(x) be a property provable about objects x of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T.”

En términos de clases Java, el Principio de Sustitución de Liskov indica que, en cualquier contexto en el que se utilice un objeto de tipo T, tiene que ser posible utilizar en su lugar un objeto de tipo S, siendo S un subtipo de T (ya sea por herencia o por implementación de interfaz), sin que el programa funcione de manera inadecuada.”

El LSP es uno de los principios más importantes en relación con la arquitectura de software. Tiene fama de ser difícil de entender y es muy frecuente ver aplicaciones que lo violan flagrantemente.

En este artículo voy a hacer una introducción básica, explicada de manera sencilla mediante algún ejemplo, sobre cómo aplicar este principio a las jerarquías de clases de nuestros programas escritos en Java. El LSP es mucho más complejo de lo que aquí vamos a mostrar. Quizás en artículos posteriores profundice un poco más en el tema.

Primer ejemplo: jerarquía de archivos

Supongamos que estamos implementando una aplicación que maneja distintos tipos de archivos. Podríamos plantear una clase abstracta Archivo, que sirviera de base a los diferentes tipos de archivos, con los métodos correspondientes a las distintas operaciones que se pueden hacer con los archivos. Entre estos métodos podrían estar los métodos abstractos abrir() e imprimir():

public abstract class Archivo {
public abstract void abrir();
public abstract void imprimir();
   /** Otros métodos **/
}

Ahora, podríamos implementar un par de clases concretas que deriven de Archivo, por ejemplo, ArchivoTexto y ArchivoImagen:

public class ArchivoTexto extends Archivo {
@Override
public void abrir() {
System.out.println("Abriendo ArchivoTexto");
}
@Override
public void imprimir() {
System.out.println("Imprimiendo ArchivoTexto");
}
   /** Otros métodos **/
}

public class ArchivoImagen extends Archivo {
@Override
public void abrir() {
System.out.println("Abriendo ArchivoImagen");
}

@Override
public void imprimir() {
System.out.println("Imprimiendo ArchivoImagen");
}
   /** Otros métodos **/
}

Nuestra aplicación podría tener una clase, a la que hemos llamado GestorArchivos, que dispusiera de distintos métodos para realizar operaciones con los archivos. Uno de esos métodos podría ser el método imprimir(), que recibe un array de archivos y los va abriendo e imprimiendo:

public class GestorArchivos {
public void imprimir(Archivo[] archivos) {
for(Archivo archivo: archivos) {
archivo.abrir();
archivo.imprimir();
}
}
   /** Otros métodos **/
}

El programa cliente, podría ser parecido al siguiente:

public class Main_1 {
public static void main(String[] args) {
GestorArchivos gestor = new GestorArchivos();

Archivo a1 = new ArchivoTexto();
Archivo a2 = new ArchivoImagen();
Archivo[] archivos = {a1, a2};

gestor.imprimir(archivos);
}
}

El diagrama UML de la aplicación es el siguiente:

La salida de este programa sería:

Hasta este momento, todo está correcto. El programa cliente GestorArchivos utiliza los objetos del tipo Archivo a través del array que recibe como argumento en su método imprimir(). Los elementos de ese array pueden ser de cualquier tipo derivado de Archivo y todo funciona correctamente.

La aplicación crece…

Supongamos que la aplicación necesita ahora gestionar un nuevo tipo de archivos: ArchivoAudio. Podríamos derivar el tipo ArchivoAudio de nuestra clase abstracta Archivo. El problema es que este tipo de archivos no se puede imprimir y, por la jerarquía de herencia impuesta, está obligado a implementar el método imprimir().

Podríamos pensar en una solución que consistiera en lanzar una excepción si se intenta imprimir un objeto del tipo ArchivoAudio:

public class ArchivoAudio extends Archivo {
@Override
public void abrir() {
System.out.println("Abriendo ArchivoAudio");
}
@Override
public void imprimir() {
throw new UnsupportedOperationException();
}
}

Esta solución supone un trastorno: si se envía uno de estos archivos al método imprimir() de la clase GestorArchivos, el programa se interrumpiría.

Podríamos razonar de la siguiente manera: podemos modificar el método imprimir() de la clase GestorArchivos de forma que, si recibe un objeto del tipo ArchivoAudio, no intente imprimirlo, para que no se lance la excepción ni se interrumpa el programa:

public class GestorArchivos {
public void imprimir(Archivo[] archivos) {
for (Archivo archivo : archivos) {
if ((archivo instanceof ArchivoAudio) == false) {
archivo.abrir();
archivo.imprimir();
}
}
}
}

Esto es una violación del Principio de Sustitución de Liskov: nuestra jerarquía debería permitir la utilización de cualquier objeto de un subtipo de Archivo en el programa cliente, sin que el cliente se tenga que preocupar de discernir la clase concreta de la que se trata. Imagina que en el futuro se necesita gestionar otros tipos de archivos no imprimibles, por ejemplo archivos de vídeo. Nos veríamos obligados a modificar de nuevo la clase cliente GestorArchivos para incorporar una nueva excepción en todos los métodos en los que fuera necesario.

Esta violación del LSP es una señal de que la jerarquía que hemos planteado no es adecuada para el problema que queremos resolver. La clase base Archivo establece el contrato que deben cumplir las clases derivadas: tener métodos para abrir y para imprimir el archivo. Pero la clase ArchivoAudio incumple dicho contrato: los archivos de audio no se pueden imprimir.

La solución correcta es crear una jerarquía cuyos contratos se cumplan. Podemos partir de una clase abstracta Archivo con un método abrir() y derivar de ella una segunda clase abstracta ArchivoImprimible que incorpora un método imprimir() y sirve de clase base de todos los tipos de archivos que se pueden imprimir.

La solución correcta podría ser la que se refleja en el siguiente esquema:

Con esta nueva jerarquía, podríamos modificar el método imprimir() de GestorArchivos de la siguiente forma:

public class GestorArchivos {
public void imprimir(Archivo[] archivos) {
for (Archivo archivo : archivos) {
if (archivo instanceof ArchivoImprimible) {
ArchivoImprimible imprimible =
(ArchivoImprimible)archivo;
imprimible.abrir();
imprimible.imprimir();
}
}
}
}

Esta solución no viola el LSP. La comprobación que se hace dentro del método imprimir() no es la de si el objeto archivo es de una clase concreta, sino si dispone de cierta capacidad. No obstante, la observación del código permite intuir la solución más limpia: cambiar el tipo de parámetro del método imprimir(), para que solo pueda recibir objetos del tipo ArchivoImprimible:

public class GestorArchivos {
public void imprimir(ArchivoImprimible[] archivos) {
for (ArchivoImprimible archivo : archivos) {
archivo.abrir();
archivo.imprimir();
}
}
}

Ahora, el método imprimir() del programa cliente siempre recibe el tipo de archivos adecuado. No obstante, según las circunstancias de la aplicación, podría ser más conveniente una u otra solución. Las dos son correctas y ninguna de ellas viola el LSP.

La solución de jerarquía planteada también se podría hacer en base a un Interface Imprimible, en lugar de la clase abstracta ArchivoImprimible. Dejo a ejercicio del lector la solución a que daría lugar este otro planteamiento de la jerarquía.

Una violación más sutil del LSP

Siguiendo con el ejemplo anterior, supongamos que hubiera un tipo de archivos que, sí se pueden imprimir, pero necesitan una configuración previa. Pensemos en una hoja de cálculo: antes de imprimir, hay que establecer qué filas y columnas queremos incluir en la impresión. Podríamos implementar la clase HojaDeCalculo de la siguiente forma:

public class HojaDeCalculo extends ArchivoImprimible {

private boolean configurado;

public void configurar() {
configurado=true;
}
@Override
public void imprimir() {
if(configurado==false) {
throw new IllegalStateException();
}
System.out.println("Imprimiendo HojaDeCalculo...");
}
@Override
public void abrir() {
System.out.println("Abriendo HojaDeCalculo");
}
}

La clase HojaDeCalculo, en su método imprimir(), lanza una excepción si no se ha llamado previamente al método configurar(). Podríamos hacer lo siguiente en la clase GestorArchivos:

public class GestorArchivos {
public void imprimir(ArchivoImprimible[] archivos) {
for (ArchivoImprimible archivo : archivos) {
archivo.abrir();

if(archivo instanceof HojaDeCalculo) {
((HojaDeCalculo)archivo).configurar();
}

archivo.imprimir();
}
}
}

Como seguramente has intuido, este código viola el LSP: el programa cliente no tiene que identificar la clase concreta sobre la que está operando. En este caso, ademas de identificar una clase concreta, tiene que saber qué operaciones tiene que hacer en ella para poder usar el método imprimir().

La diferencia con el caso de los archivos imprimibles es que ahora el problema no está en la jerarquía, que podría ser adecuada. El problema es que estamos imponiendo que la clase GestorArchivo tiene que saber cómo operar un tipo de archivos concreto.

Piensa que este tipo de situación se podría dar en otros tipos de archivos: archivos que es necesario comprimir antes de enviar por correo electrónico, archivos que es necesario codificar en JSON o XML antes de enviarlos por Internet o cualquier otra situación. Y el programa cliente no es el que tiene que gestionar todas esas peculiaridades. Si lo hiciera, con cada nuevo tipo de archivo, habría que volver a codificar la clase GestorArchivos.

La solución es que sean las propias clases las que gestionen su estado con las operaciones que tengan que hacer. Podríamos modificar el método imprimir() de la clase HojaDeCalculo para que, si el archivo no está configurado, ella misma llame al método configurar():

@Override
public void imprimir() {
   if(configurado==false) {
       configurar();
  }
   System.out.println("Imprimiendo HojaDeCalculo...");
}

Ahora, el código de GestorArchivos puede operar perfectamente con cualquier ArchivvoImprimible que reciba:

public class GestorArchivos {
public void imprimir(ArchivoImprimible[] archivos) {
for (ArchivoImprimible archivo : archivos) {
archivo.abrir();
archivo.imprimir();
}
}
}

Hasta aquí, esta pequeña introducción al Principio de Sustitución de Liskov. Hay otros aspectos de este principio y otros tipos de violaciones del mismo que seguramente trataremos en otros artículos. Por el momento, basta con que entiendas el planteamiento más simple de la aplicación del LSP.

💡 12 reglas de buenas prácticas de programación (con ejemplos en Java)

Aprender a programar no consiste solo en hacer que un programa funcione.

Un programa puede funcionar… y ser difícil de entender, mantener o ampliar. Por eso, además de aprender sintaxis, es fundamental adoptar una serie de buenas prácticas que nos ayuden a escribir mejor código.

Muchas de estas reglas no son nuevas: aparecen una y otra vez en libros clásicos como:

  • Clean Code — Robert C. Martin
  • Effective Java — Joshua Bloch
  • Refactoring — Martin Fowler
  • The Pragmatic Programmer — Andrew Hunt y David Thomas
  • Code Complete — Steve McConnell

A continuación se presentan 12 reglas fundamentales, con ejemplos en Java.


1. Usa nombres que expresen intención

👉 Un buen nombre debe indicar claramente qué representa o qué hace la variable, el método o la clase, sin necesidad de comentarios.

📚 Referencia: Clean Code

🔸 Variables

❌ Mal:

int x;

✔️ Bien:

int edad;

🔸 Métodos

❌ Mal:

void f() {
   // ...
}

✔️ Bien:

void calcularMedia() {
   // ...
}

🔸 Clases

❌ Mal:

class Datos {
}

✔️ Bien:

class Alumno {
}

📎 Si necesitas un comentario para explicar qué hace una variable o un método, probablemente su nombre no es adecuado.

2. Las funciones deben ser pequeñas y hacer una sola cosa

👉 Una función debe tener una única responsabilidad y ser fácil de entender de un vistazo.

📚 Referencias: “Clean Code“, “Refactoring“.

📌 Relacionado con: SRP (Single Responsibility Principle)

🔸 Ejemplo

❌ Mal:

void procesarAlumno(Alumno alumno) {
   // validar edad
   if (alumno.getEdad() < 0) {
       System.out.println("Edad incorrecta");
  }

   // calcular media de notas
   double media = (alumno.getNota1() + alumno.getNota2()) / 2;

   // guardar en fichero
   System.out.println("Guardando alumno...");
}

✔️ Bien:

void procesarAlumno(Alumno alumno) {
   validarAlumno(alumno);
   double media = calcularMedia(alumno);
   guardarAlumno(alumno, media);
}

void validarAlumno(Alumno alumno) {
   if (alumno.getEdad() < 0) {
       System.out.println("Edad incorrecta");
  }
}

double calcularMedia(Alumno alumno) {
   return (alumno.getNota1() + alumno.getNota2()) / 2;
}

void guardarAlumno(Alumno alumno, double media) {
   System.out.println("Guardando alumno...");
}

📎 Refactorización
El paso de la versión “mal” a la versión “bien” no consiste en añadir funcionalidad, sino en mejorar la estructura del código sin cambiar su comportamiento.
Este proceso se conoce como refactorización (refactoring). En este caso concreto, hemos aplicado la técnica de extracción de métodos (extract method), dividiendo una función grande en varias funciones más pequeñas y especializadas.


⚠️ Señales de alerta

  • Si el nombre de la función contiene “y” (por ejemplo, calcularYGuardar) → probablemente hace más de una cosa
  • Si necesitas poner comentarios dentro de una función → probablemente deberías dividirla en varias funciones más pequeñas
  • Si una función es difícil de explicar en una sola frase → probablemente hace demasiadas cosas

3. Evita comentarios innecesarios

👉 El código debe ser lo suficientemente claro como para no necesitar comentarios que expliquen qué hace.

📚 Referencia: Clean Code

🔸 Ejemplo con variables

❌ Mal:

// Edad
int x;

✔️ Bien:

int edad;

En el primer caso, el comentario es necesario porque el nombre de la variable no aporta información. En el segundo, el propio nombre hace innecesaria cualquier explicación.

🔸 Ejemplo con funciones

❌ Mal:

void procesarAlumno(Alumno alumno) {
   // validar edad
   if (alumno.getEdad() < 0) {
       System.out.println("Edad incorrecta");
  }

   // calcular media
   double media = (alumno.getNota1() + alumno.getNota2()) / 2;

   // guardar alumno
   System.out.println("Guardando alumno...");
}

✔️ Bien:

void procesarAlumno(Alumno alumno) {
   validarAlumno(alumno);
   double media = calcularMedia(alumno);
   guardarAlumno(alumno, media);
}

En el primer caso, los comentarios son necesarios porque el código no es claro. En el segundo, los nombres de los métodos explican exactamente qué ocurre, por lo que los comentarios dejan de ser necesarios.


⚠️ Nota importante

Esto no significa que los comentarios sean malos.

Son útiles para:

  • explicar decisiones de diseño
  • documentar APIs
  • aclarar aspectos no evidentes

Pero no deberían usarse para explicar código que podría ser más claro.


Regla práctica

  • Si un comentario explica qué hace el código → probablemente el código puede mejorar.
  • Si un comentario explica por qué se hace algo → suele ser útil.

4. No repitas código (DRY)

👉 El principio DRY (Don’t Repeat Yourself, «no te repitas») establece que cada pieza de lógica debe tener una única representación en el código. Cuando el mismo cálculo o bloque de instrucciones aparece copiado en varios sitios, cualquier corrección futura obliga a localizar y modificar todas las copias; si se pasa por alto alguna, el programa queda en un estado inconsistente.

📚 Referencia: The Pragmatic Programmer

📌 Acrónimo: DRY (Don’t Repeat Yourself)

🔸 Ejemplo

❌ Mal:

public class Main {
public static void main(String[] args) {
double radio1 = 5.0;
double area1 = 3.14 * radio1 * radio1;
System.out.printf("Area del circulo 1: %.2f%n", area1);

double radio2 = 3.0;
double area2 = 3.14 * radio2 * radio2;
System.out.printf("Area del circulo 2: %.2f%n", area2);

double radio3 = 7.5;
double area3 = 3.14 * radio3 * radio3;
System.out.printf("Area del circulo 3: %.2f%n", area3);
}
}

En este código, el cálculo del área está duplicado en varios sitios. Si en el futuro se decide aumentar la precisión de π o añadir una comprobación sobre el radio, habría que modificar todas las apariciones.

✔️ Mejor:

public class Main {

public static void main(String[] args) {
double radio1 = 5.0;
double area1 = calcularAreaCirculo(radio1);
System.out.printf("Area del circulo 1: %.2f%n", area1);

double radio2 = 3.0;
double area2 = calcularAreaCirculo(radio2);
System.out.printf("Area del circulo 2: %.2f%n", area2);

double radio3 = 7.5;
double area3 = calcularAreaCirculo(radio3);
System.out.printf("Area del circulo 3: %.2f%n", area3);
}

static double calcularAreaCirculo(double radio) {
return Math.PI * radio * radio;
}
}

Ahora, el cálculo está centralizado en un único método. Cualquier cambio se realiza en un solo lugar, lo que reduce errores y facilita el mantenimiento.

Aún así, en main() se repiten tres bloques casi idénticos. Podríamos usar un bucle y simplificar el código:

✔️ Bien:

public class Main {
public static void main(String[] args) {
double[] radios = {5.0, 3.0, 7.5};
for(int i=0; i<radios.length; i++) {
double area = calcularAreaCirculo(radios[i]);
System.out.printf("Area del circulo %d: %.2f%n",
i+1, area);
}
}

static double calcularAreaCirculo(double radio) {
return Math.PI * radio * radio;
}
}

👉 Regla práctica

  • Si repites un mismo cálculo o una misma fórmula → probablemente estás violando DRY
  • Si un cambio obliga a modificar varios sitios → hay duplicación
  • DRY no solo evita repetir código, sino evitar repetir conocimiento

⚠️ Nota importante

En Java, muchas constantes de uso habitual ya están definidas en la biblioteca estándar. Por ejemplo, el número π está disponible como Math.PI, con toda la precisión necesaria. Siempre que sea posible, es preferible usar constantes ya definidas en lugar de redefinirlas.

5. Evita los números mágicos

👉 Los valores constantes deben tener un nombre significativo que explique qué representan y aparecer en el código a través de la constante que los representa, no codificados directamente como números mágicos.

📚 Referencia: Clean Code

🔸 Ejemplo

❌ Mal:

static double calcularPrecioConIva(double precio) {
return precio * 1.21;
}

static double calcularIva(double precio) {
return precio * 0.21;
}

En este código, el valor 0.21 aparece repetido en varias funciones. Si el tipo de IVA cambia, hay que buscar y modificar todas las apariciones, con el riesgo de pasar alguna por alto. Además, alguien que lea el código por primera vez no sabe inmediatamente qué significa ese valor.

✔️ Bien:

static final double IVA = 0.21;

static double calcularPrecioConIva(double precio) {
return precio * (1 + IVA);
}

static double calcularIva(double precio) {
return precio * IVA;
}

Ahora el valor tiene un nombre que explica su significado, y aparece en un único lugar. Si el tipo impositivo cambia, la modificación se hace una sola vez y queda actualizada en todas las partes del programa que lo utilizan.

Regla práctica

  • Si aparece un número «suelto» en el código → probablemente debería ser una constante
  • Si no sabes qué significa un valor sin leer más código → necesita un nombre
  • Las constantes hacen el código más claro y más fácil de mantener

6. Evita listas largas de parámetros

👉 Cuando una función tiene muchos parámetros, suele ser difícil de entender y utilizar correctamente.

📚 Referencia: Effective Java

No hay un número mágico, pero como regla general:

  • 1–2 parámetros → normal
  • 3 parámetros → empieza a ser sospechoso
  • 4 o más → probablemente hay un problema de diseño

🔸 Ejemplo

❌ Mal:

void imprimirUsuario(String nombre, int edad, String email, String telefono) {
System.out.println(nombre + " (" + edad + ")");
...
}

✔️ Bien:

class Usuario {
String nombre;
int edad;
String email;
String telefono;
}

void imprimirUsuario(Usuario usuario) {
System.out.println(usuario.nombre + " (" + usuario.edad + ")");
...
}

En el primer caso, la función recibe muchos datos independientes, lo que la hace más difícil de usar y más propensa a errores (por ejemplo, cambiar el orden de los parámetros). En el segundo, los datos están agrupados en un objeto que representa un concepto del dominio (Usuario), lo que hace el código más claro y más fácil de manejar.

Por ahora los campos de Usuario son accesibles directamente desde fuera de la clase. En la siguiente regla veremos por qué eso puede ser un problema y cómo solucionarlo.

Regla práctica

  • Si una función tiene muchos parámetros → revisa si puedes agruparlos en un objeto
  • Si varios parámetros están relacionados → probablemente deberían formar una clase
  • Si dudas sobre el orden de los parámetros → el diseño puede mejorarse

🔥 Nota avanzada

👉 En algunos casos, cuando es necesario crear objetos con muchos parámetros, se utilizan técnicas como el patrón Builder, que se estudiarán en asignaturas más avanzadas.

7. Prefiere objetos inmutables

👉 Un objeto inmutable es aquel cuyo estado no puede cambiar después de crearse.

📚 Referencia: Effective Java

🔸 Ejemplo

❌ Mal:

class Usuario {
   String nombre;
   int edad;
}
Usuario u = new Usuario();
u.nombre = "Ana";
u.edad = 20;
u.edad = -5; // nadie lo impide

En este código, los campos son accesibles directamente desde fuera de la clase. Cualquier parte del programa puede modificar el estado del objeto en cualquier momento, incluso asignando valores inválidos.

✔️ Bien:

class Usuario {
   private final String nombre;
   private final int edad;

   public Usuario(String nombre, int edad) {
       this.nombre = nombre;
       this.edad = edad;
  }

   public String getNombre() { return nombre; }
   public int getEdad() { return edad; }
}

Ahora los campos son privados y no pueden modificarse tras la construcción. El objeto se crea de una vez con todos sus datos y su estado no cambia.

En la regla anterior agrupábamos datos en un objeto (Usuario). Aquí damos un paso más: hacemos que ese objeto sea seguro y consistente.

Regla práctica

  • Si un objeto no necesita cambiar → hazlo inmutable
  • Si un atributo no debe modificarse → decláralo final
  • Evita exponer atributos directamente → usa encapsulación

⚠️ Nota importante

La inmutabilidad aporta varias ventajas:

  • Evita errores difíciles de detectar
  • Facilita el razonamiento sobre el código
  • Hace el código más seguro en entornos concurrentes

8. Maneja correctamente los errores

👉 Los errores no deben ignorarse: deben detectarse y tratarse de forma adecuada.

📚 Referencia: Effective Java

🔸 Ejemplo

En la regla anterior hicimos que Usuario fuera inmutable. Pero aún es posible construir un objeto con valores inválidos:

❌ Mal:

class Usuario {
private final String nombre;
private final int edad;

public Usuario(String nombre, int edad) {
this.nombre = nombre;
this.edad = edad; // no se valida
}
}
Usuario u = new Usuario("Ana", -5); // se crea sin error

El objeto se construye sin problemas, aunque la edad sea negativa. El error pasa desapercibido.

✔️ Bien:

class Usuario {
private final String nombre;
private final int edad;

public Usuario(String nombre, int edad) {
if (edad < 0) {
throw new IllegalArgumentException("La edad no puede ser negativa");
}
this.nombre = nombre;
this.edad = edad;
}

public String getNombre() { return nombre; }
public int getEdad() { return edad; }
}

Ahora el error se detecta en el momento adecuado y se comunica mediante una excepción. Es imposible construir un Usuario con un estado inválido.

🔸 Ejemplo de uso

❌ Mal:

try {
   Usuario u = new Usuario("Ana", -5);
} catch (Exception e) {
   // ignorar el error
}

✔️ Bien:

try {
   Usuario u = new Usuario("Ana", -5);
} catch (IllegalArgumentException e) {
   System.out.println("Error al crear el usuario: " + e.getMessage());
}

En el primer caso, el error se captura pero se ignora, lo que puede ocultar problemas graves. En el segundo, se gestiona de forma explícita.

Regla práctica

  • No ignores excepciones → siempre haz algo con ellas
  • Usa tipos de excepción específicos (IllegalArgumentException, etc.)
  • Valida los datos lo antes posible, preferiblemente en el constructor

⚠️ Nota importante

Lanzar una excepción no es «fallar», es:

  • detectar un problema antes de que se propague
  • evitar que el objeto quede en un estado incorrecto
  • hacer el error visible en lugar de ocultarlo

Conexión con la regla anterior

En la regla anterior hicimos el objeto Usuario inmutable: sus campos no pueden cambiar tras la construcción. Aquí añadimos validación para garantizar que el objeto nunca se construya en un estado inválido. Las dos reglas juntas aseguran que Usuario sea siempre correcto.

9. Escribe código fácil de leer (KISS)

👉 La solución más simple y clara suele ser la mejor.

📚 Referencia: The Pragmatic Programmer

📌 Acrónimo: KISS (Keep It Simple, Stupid)

🔸 Ejemplo

❌ Mal:

if ((edad >= 18 && edad <= 65 && !tieneDeudas) || esEmpleadoVIP) {
permitirAcceso();
}

✔️ Bien:

boolean edadValida = edad >= 18 && edad <= 65;
boolean clienteSinDeudas = !tieneDeudas;
boolean accesoEspecial = esEmpleadoVIP;

if ((edadValida && clienteSinDeudas) || accesoEspecial) {
permitirAcceso();
}

En el primer caso, la condición es difícil de leer y entender de un vistazo. En el segundo, el uso de variables intermedias permite expresar claramente la intención del código.

Regla práctica

  • Si una expresión es difícil de leer → divídela
  • Usa variables con nombres significativos
  • Prefiere claridad frente a “código compacto”

⚠️ Nota importante

Escribir menos código no siempre significa escribir mejor código. A veces, añadir unas pocas líneas mejora mucho la legibilidad.

Conexión con reglas anteriores

Igual que en reglas anteriores, los nombres (edadValida, clienteSinDeudas) hacen que el código se explique por sí mismo, evitando la necesidad de comentarios.

10. Escribe tests siempre que puedas

👉 Un test permite comprobar automáticamente que una parte del programa funciona como esperamos.

📚 Referencia: Test Driven Development: By Example

📌 Relacionado con: TDD (Test-Driven Development)

🔸 Ejemplo

Supongamos que queremos implementar un método que determine si un alumno está aprobado.

❌ Sin test:

class Evaluacion {
   static boolean estaAprobado(double nota) {
       return nota > 5; // error sutil
  }
}

✔️ Con test:

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;
import org.junit.jupiter.api.Test;

class EvaluacionTest {

   @Test
   void aprobadoConCinco() {
       assertTrue(Evaluacion.estaAprobado(5));
  }

   @Test
   void suspensoConMenosDeCinco() {
       assertFalse(Evaluacion.estaAprobado(4.9));
  }
}

En este caso, el código tiene un error (> 5 en lugar de >= 5) que es fácil de cometer y difícil de detectar a simple vista. Al ejecutar el test y ver que falla, podemos localizar enseguida el error. El test define claramente el comportamiento esperado y permite detectar el error de forma inmediata.

class Evaluacion {
   static boolean estaAprobado(double nota) {
       return nota >= 5;
  }
}

Regla práctica

  • Si una regla tiene límites (≥, ≤, etc.) → es importante testearla
  • Los tests ayudan a detectar errores pequeños pero importantes
  • Un buen test describe claramente el comportamiento esperado

⚠️ Nota importante

Los tests son especialmente útiles cuando:

  • Hay condiciones límite.
  • Hay reglas del dominio (como “aprobado”).
  • El código puede cambiar en el futuro.

Conexión con reglas anteriores

Igual que en la regla DRY, aquí también aparece una regla del sistema (“qué es aprobar”). Los tests permiten asegurar que esa regla se cumple siempre.

11. Refactoriza constantemente

👉 Mejorar el código es parte del desarrollo, no una fase posterior.

📚 Referencia: Refactoring

🔸 Ejemplo

Supongamos que ya tenemos un código que funciona correctamente:

class Evaluacion {
   static boolean estaAprobado(double nota) {
       return nota >= 5;
  }

   static String mensajeResultado(double nota) {
       if (nota >= 5) {
           return "Aprobado";
      } else {
           return "Suspenso";
      }
  }
}

El código funciona, pero hay duplicación.

✔️ Refactorizado:

class Evaluacion {
   static boolean estaAprobado(double nota) {
       return nota >= 5;
  }

   static String mensajeResultado(double nota) {
       if (estaAprobado(nota)) {
           return "Aprobado";
      } else {
           return "Suspenso";
      }
  }
}

El código inicial es correcto, pero repite la misma lógica (nota >= 5) en varios sitios. Al refactorizar, reutilizamos el método estaAprobado(), haciendo el código más claro y evitando duplicación.

Regla práctica

  • Refactorizar no es “arreglar errores”, sino mejorar el código
  • Si ves duplicación → refactoriza
  • Si algo se puede expresar mejor → refactoriza

⚠️ Nota importante

Refactorizar significa:

  • No cambiar el comportamiento.
  • Mejorar la estructura interna.
  • Facilitar futuras modificaciones.

Conexión con reglas anteriores

En esta regla se aplican varias ideas vistas antes:

  • DRY → eliminamos duplicación
  • nombres claros → estaAprobado
  • funciones pequeñas → mejor organización

📎 Un buen programador no solo escribe código que funciona, sino que mejora continuamente el código que ya funciona.

12. Piensa antes de programar

👉 Antes de escribir código, es fundamental entender el problema y diseñar una solución.

📚 Referencia: The Pragmatic Programmer

🔸 Ejemplo

Supongamos que queremos gestionar los datos de varios alumnos y determinar si han aprobado. Sin reflexionar sobre el diseño, es habitual empezar así:

❌ Mal:

String[] nombres  = {"Ana", "Luis", "María"};
double[] notas    = {8.5, 4.0, 9.0};

for (int i = 0; i < nombres.length; i++) {
   if (notas[i] >= 5) {
       System.out.println(nombres[i] + ": Aprobado");
  } else {
       System.out.println(nombres[i] + ": Suspenso");
  }
}

El código funciona para este caso concreto, pero los datos de un alumno están repartidos en arrays distintos. Si añadimos un atributo nuevo (por ejemplo, el email), necesitamos un tercer array y recordar mantener siempre los tres sincronizados. La relación entre los datos no está expresada en el código.

✔️ Bien:

class Alumno {
   String nombre;
   double nota;

   public Alumno(String nombre, double nota) {
       this.nombre = nombre;
       this.nota = nota;
  }

   boolean estaAprobado() {
       return nota >= 5;
  }
}
...

Alumno[] alumnos = {
   new Alumno("Ana",  8.5),
   new Alumno("Luis", 4.0),
   new Alumno("María", 9.0)
};

for (Alumno a : alumnos) {
   String resultado = a.estaAprobado() ? "Aprobado" : "Suspenso";
   System.out.println(a.nombre + ": " + resultado);
}

Al modelar el problema antes de codificarlo, los datos de cada alumno están encapsulados en un objeto y la lógica de negocio («¿está aprobado?») vive junto a los datos a los que pertenece. Si mañana necesitamos guardar también el email de cada alumno, en la versión con arrays tendríamos que añadir un tercer array y mantenerlo sincronizado con los otros dos. En la versión con clase, basta con añadir un campo a Alumno y el resto del código no se ve afectado.

En un programa real aplicaríamos también las reglas 7 y 8: haríamos los campos privados y validaríamos los datos en el constructor.

Regla práctica

  • Antes de programar → identifica los conceptos del problema
  • Agrupa en una clase los datos que representan una misma entidad
  • La lógica que pertenece a un dato debe estar cerca de ese dato

⚠️ Nota importante

Muchos problemas de programación no son de sintaxis, sino de diseño. Dedicar unos minutos a pensar qué entidades intervienen y cómo se relacionan suele simplificar mucho el código resultante.

Conexión con todo el artículo

Esta regla resume todas las anteriores:

  • Nombres claros → pensar qué representan los datos
  • Funciones pequeñas → diseñar responsabilidades
  • DRY → identificar conocimiento común
  • Inmutabilidad → decidir qué debe cambiar

Todas las buenas prácticas empiezan antes de escribir código.

📎 Programar bien no consiste solo en escribir código que funcione, sino en construir soluciones que otros puedan entender, mantener y mejorar. Y todo ello empieza mucho antes de escribir la primera línea de código.

📊 Resumen de reglas

#ReglaAcrónimoReferencia
1Usa nombres que expresen intenciónClean Code
2Funciones pequeñas y con una sola responsabilidadSRPClean Code, Refactoring
3Evita comentarios innecesariosClean Code
4No repitas códigoDRYThe Pragmatic Programmer
5Evita números mágicosClean Code
6Evita listas largas de parámetrosEffective Java
7Prefiere objetos inmutablesEffective Java
8Maneja correctamente los erroresEffective Java
9Escribe código simple y legibleKISSThe Pragmatic Programmer
10Escribe testsTDDTest Driven Development: By example
11Refactoriza constantementeRefactoring
12Piensa antes de programarThe Pragmatic Programmer

Estas 12 reglas resumen algunos de los principios más importantes que aparecen una y otra vez en la práctica profesional.

📎 Estas reglas no son leyes estrictas, pero sí principios que aparecen constantemente en la práctica profesional. No deben memorizarse de forma aislada. En la práctica, suelen aparecer juntas y se refuerzan unas a otras. Aprenderlas desde el principio marca una gran diferencia:

  • el código funciona,
  • se entiende
  • y puede evolucionar

Programar bien no es aplicar reglas de forma mecánica, sino desarrollar el criterio para saber cuándo y cómo aplicarlas. Porque, al final, programar bien no es solo resolver problemas, sino resolverlos de forma que otros puedan entender, mantener y continuar tu trabajo.

📚 12 libros imprescindibles para aprender a programar (bien)

Aprender a programar es relativamente fácil. Aprender a programar bien es otra historia.

Aprender a programar no consiste solo en dominar un lenguaje. De hecho, eso es casi lo de menos. Lo verdaderamente importante es aprender a pensar como programador, a escribir código que otros puedan entender, mantener y evolucionar. Es decir, desarrollar criterio.

En la enseñanza de la programación, es habitual centrar la atención en lenguajes, herramientas o entornos de desarrollo. Sin embargo, estos aspectos son, en gran medida, circunstanciales.

Lo que realmente distingue a un buen programador es su capacidad para escribir código comprensible, diseñar soluciones adecuadas y tomar decisiones fundamentadas.

En este artículo se presenta una selección de doce libros que ayudan precisamente a eso: a ir más allá de la sintaxis y comprender qué significa realmente construir buen software. Son libros que enseñan buenas prácticas, diseño y fundamentos sólidos.

La mayoría son clásicos, pero se complementan con algunas referencias más modernas que reflejan la práctica actual.

📎 A comienzos de los años 2000, varios de los autores mencionados en este artículo participaron en la redacción del Manifiesto Ágil, un breve pero influyente texto que redefinió la forma de entender el desarrollo de software. Frente a enfoques más rígidos y planificados, el manifiesto propone priorizar a las personas y la comunicación, el software funcionando, la colaboración con el cliente y la capacidad de adaptación al cambio.

A partir de estos principios surgieron diversas metodologías y prácticas que han marcado profundamente la ingeniería del software moderna, como Scrum o Extreme Programming. Muchas de las ideas presentes en libros como Clean Code, Refactoring o Test Driven Development están estrechamente relacionadas con esta forma de entender el desarrollo: iterativo, incremental y centrado en la calidad del código.


🧼 Escribir código limpio y mantenible

1. Clean Code

Autor: Robert C. Martin

Si tuviera que recomendar un solo libro, sería este. Probablemente se trata del libro más influyente sobre calidad del código.

Su idea central es sencilla pero profunda: el código debe ser legible y expresar intención. Clean Code enseña a escribir código que se entienda, que sea legible y que transmita intención. Habla de nombres, funciones, comentarios, tests… pero, sobre todo, de disciplina.

Sobre el autor: Robert C. Martin, conocido como “Uncle Bob”, es uno de los firmantes del Manifiesto Ágil y ha sido una figura clave en la difusión de las buenas prácticas en ingeniería del software durante décadas.


2. Code Complete

Autor: Steve McConnell

Una visión sistemática y global del desarrollo de software. Desde detalles de implementación hasta calidad y mantenimiento.

Es más denso que otros libros, pero muy recomendable.

Sobre el autor: McConnell es también autor de Rapid Development y fundador de Construx Software, una consultora centrada en mejorar procesos de desarrollo en empresas. Antes, trabajó en Microsoft y Boeing. Se le atribuye la afirmación de que, de media, cada 1000 líneas de código en producción tienen entre 15 y 50 errores.


3. Refactoring

Autor: Martin Fowler

El término refactoring se refiere a modificar un código sin cambiar su funcionalidad. El principio que guía el libro es que el código no se escribe una sola vez, sino que hay que mejorarlo constantemente.

Este libro enseña cómo transformar código existente sin cambiar su comportamiento, apoyándose en pequeñas mejoras seguras.

Sobre el autor: Martin Fowler es uno de los referentes en arquitectura de software y divulgación técnica, especialmente conocido por su trabajo en patrones de arquitectura empresarial. Fue uno de los firmantes del Manifiesto Ágil.


🏗️ Diseño y arquitectura del software

4. Design Patterns

Autores: Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides.

Este libro es un clásico imprescindible. A partir de su publicación en 1994 se han escrito cientos de libros y artículos tratando o desarrollando los patrones de diseño que se describen en el libro.

Introduce soluciones reutilizables a problemas de diseño en programación orientada a objetos. Aporta un vocabulario común para hablar de arquitectura. Después de leerlo, verás patrones de diseño por todos los lados, cada vez que trabajes en código.

Sobre los autores: conocidos como la “banda de los cuatro” (GoF), varios de ellos participaron en el desarrollo de herramientas clave como Eclipse, lo que demuestra la aplicación práctica de sus ideas.


5. Clean Architecture

Autor: Robert C. Martin

Explica cómo organizar sistemas complejos para que sean mantenibles y resistentes al cambio.

Ideal cuando se da el salto de programas pequeños a aplicaciones reales.

Sobre el autor: además de su faceta técnica, Martin ha sido un activo formador y conferenciante, contribuyendo a establecer estándares de calidad en el desarrollo profesional.


☕ Buenas prácticas aplicadas (Java)

6. Effective Java

Autor: Joshua Bloch

Aquí hago una excepción: un libro centrado en un lenguaje concreto.

Effective Java es una colección de recomendaciones prácticas sobre cómo usar Java correctamente. Pero su verdadero valor es que traduce principios generales (inmutabilidad, encapsulación, diseño de APIs…) a decisiones concretas de código.

Para quien aprende Java, es probablemente el libro más útil de esta lista en el corto plazo.

Sobre el autor: Joshua Bloch fue uno de los principales arquitectos de la biblioteca estándar de Java. Durante su etapa en Sun Microsystems, fue el diseñador del Java Collections Framework, lo que da a este libro un valor especialmente sólido.


🧪 Desarrollo guiado por pruebas y programación extrema

Muchas de las prácticas modernas de desarrollo tienen su origen en la Extreme Programming (XP), propuesta por Kent Beck a finales de los años 90. XP plantea llevar al extremo una idea sencilla: si algo es bueno en el desarrollo de software, hacerlo de forma continua.

En este contexto surge el desarrollo guiado por pruebas (Test Driven Development, TDD), una práctica en la que primero se escriben las pruebas y después el código que las satisface. Este enfoque no solo mejora la calidad del software, sino que también influye directamente en su diseño, favoreciendo soluciones más simples, modulares y fáciles de mantener.

Muchas de las ideas presentes en libros como Refactoring, Clean Code o Working Effectively with Legacy Code encajan de forma natural con esta filosofía: el código se construye y se mejora de manera iterativa, con feedback constante y con una fuerte atención a su calidad interna.

7. Test Driven Development: By Example

Autor: Kent Beck

Introduce una de las prácticas más influyentes del desarrollo moderno: escribir primero las pruebas y después el código.

Más allá de la técnica, muestra cómo TDD conduce a diseños más simples, código más modular y mayor confianza en los cambios.

Sobre el autor: Kent Beck es uno de los creadores de la programación extrema (XP, Extreme Programming) y uno de los firmantes del Manifiesto Ágil, con una enorme influencia en las prácticas modernas de desarrollo.

📎 Kent Beck desarrolló XP mientras trabajaba en el proyecto Chrysler Comprehensive Compensation System (C3), donde buscaba mejorar la calidad del software y la capacidad de adaptación al cambio.

El nombre “extreme” no es casual. La idea era llevar al extremo prácticas que ya se consideraban buenas:

  • Si las revisiones de código son buenas → revisar constantemente (pair programming).
  • Si probar es bueno → probar todo el tiempo (TDD).
  • Si integrar es bueno → integrar continuamente.
  • Si el diseño es importante → refactorizar continuamente.

XP se consolidó con el libro: Extreme Programming Explained (1999), también de Kent Beck. Este libro definió prácticas como:

  • Test-Driven Development.
  • Pair Programming.
  • Continuous Integration.
  • Small Releases.

🧠 Mentalidad y oficio del programador

8. The Pragmatic Programmer

Autores: Andrew Hunt y David Thomas

Este libro, más que técnicas, enseña actitud.

Responsabilidad, aprendizaje continuo, automatización… Un libro muy accesible y lleno de ideas prácticas.

Sobre los autores: Andrew Hunt y David Thomas también fueron firmantes del famoso Agile Manifesto en 2001. Fundaron la editorial Pragmatic Bookshelf, muy influyente en la literatura moderna de programación. También se les atribuye el acrónimo del principio de desarrollo DRY: Don’t Repeat Yourself.


9. Working Effectively with Legacy Code

Autor: Michael Feathers

Si bien los principios SOLID fueron formulados por Robert C. Martin a comienzos de los años 2000, el acrónimo SOLID fue popularizado posteriormente por Michael Feathers (2004).

Los principios SOLID son:

  • Single responsability principle.
  • Open-closed principle.
  • Liskov substitution principle.
  • Interface segregation principle.
  • Dependency inversion principle.

Este libro explica cómo enfrentarse a código existente (y a menudo problemático).

Especialmente útil en entornos reales, donde rara vez se empieza desde cero.

Sobre el autor: Feathers es conocido por su trabajo en metodologías ágiles y por acuñar definiciones muy utilizadas, como considerar “legacy code” aquel que no tiene tests.


10. The Mythical Man-Month

Autor: Frederick P. Brooks Jr.

Este libro es un clásico sobre la gestión de proyectos software.

Explica por qué los proyectos se retrasan y desmonta mitos muy extendidos (como que añadir más desarrolladores acelera el trabajo).

Sobre el autor: Frederick Brooks es conocido como el padre del IBM System/360, pues dirigió el desarrollo del sistema operativo OS/360 en IBM, una experiencia que dio origen a muchas de las reflexiones del libro.


🧩 Fundamentos: pensar como programador

11. Structure and Interpretation of Computer Programs

Autores: Harold Abelson y Gerald Jay Sussman

“The Wizard Book”, un libro exigente, pero muy formativo.

Enseña a razonar sobre programas y a entender profundamente qué significa programar. Hay una cita de este libro que suelo decir en clase y que me encanta:

Los programas deben escribirse para que las personas los lean y, solo de forma incidental, para que las máquinas los ejecuten.

Sobre los autores: ambos son profesores del MIT, y este libro ha sido durante décadas el núcleo de uno de los cursos más influyentes de programación del mundo.


12. Introduction to Algorithms

Autores: Thomas H. Cormen et al.

El estándar en algoritmia.

No trata directamente el estilo de código, pero sí proporciona rigor: eficiencia, complejidad y modelado de problemas.

Sobre los autores: el libro reúne a varios investigadores de primer nivel y es utilizado como manual en universidades de todo el mundo.


🚀 Más allá de los clásicos: referencias modernas

Los libros anteriores comparten una característica: muchos tienen décadas… y siguen siendo fundamentales. Esto se debe a que los principios básicos del buen software cambian muy poco.

Sin embargo, el contexto sí ha evolucionado. Hoy desarrollamos sistemas distribuidos, trabajamos en equipos grandes y desplegamos continuamente.

Para comprender ese entorno, pueden complementarse con algunas obras más recientes:

Designing Data-Intensive Applications, Martin Kleppmann

Sobre el autor: Kleppmann es investigador en sistemas distribuidos en la Universidad de Cambridge, lo que aporta una base académica muy sólida al libro.


A Philosophy of Software Design, John Ousterhout

Sobre el autor: Ousterhout es el creador del lenguaje Tcl y ha trabajado durante décadas en sistemas y diseño de software.


Software Engineering at Google, Google

Sobre los autores: el libro recoge prácticas reales de ingeniería dentro de Google, una de las organizaciones con mayor escala en desarrollo software.


Modern Software Engineering, David Farley

Sobre el autor: Farley es uno de los impulsores del concepto de Continuous Delivery, ampliamente adoptado en la industria actual.

🤔 ¿Por qué mezclar tantos enfoques?

Porque programar bien no es una sola cosa. Implica:

  • escribir código claro (Clean Code),
  • diseñar bien (Design Patterns, Clean Architecture),
  • tomar buenas decisiones (Effective Java),
  • y entender los fundamentos (algoritmos).

Cada uno de estos libros ataca una parte del problema. Si estás empezando o consolidando tu formación como programador, una posible ruta sería:

  1. Clean Code y The Pragmatic Programmer.
  2. Refactoring y Effective Java.
  3. Design Patterns y Clean Architecture.
  4. Fundamentos: algoritmos o SICP.
  5. Y, posteriormente, alguna referencia moderna.

Porque, en última instancia, la programación no consiste solo en hacer que un programa funcione, sino en construir software que pueda perdurar. Y eso, más que cualquier lenguaje o tecnología, es lo que define a un buen programador.

Método main() no estático

Desde la versión 21 de Java se pueden crear métodos main() no estáticos para ejecutar nuestros programas. El objetivo principal es mejorar el protocolo de lanzamiento de programas Java para permitir métodos main() de instancia: métodos que no son static, no necesitan ser public, ni la clase ni el método y no necesitan el parámetro String[].

Esta característica fue introducida como preview en Java 21 y finalizada en Java 25.

En su forma mínima, podemos crear un método main() sin static ni public y sin pedir el parámetro String[].

class Saludo {
    void main() {
        System.out.println("Hola desde un método de instancia");
    }
}

Guarda el código anterior en un fichero llamado Saludo.java. Observa que la clase no la hemos declarado public. Tampoco hemos declarado public ni static el método main(), ni hemos puesto argumentos String[].

Para compilar:

javac Saludo.java

Para ejecutar, si utilizas Java 21, tendrás que indicar a la JVM que utilice las características en preview:

java --enable-preview Saludo

En cambio, si utilizas Java 25, no será necesario hacer ninguna indicación:

java Saludo

Esta ha sido la salida en mi ordenador utilizando Java 21:

Utilizando Java 25:

¿Por qué se introdujo esto?

La motivación principal es pedagógica: static no solo es misterioso para los principiantes, sino perjudicial. Para añadir más métodos que main() pueda llamar, el estudiante debe declarar todo como static (propagando un idioma poco habitual) o enfrentarse a la diferencia entre miembros estáticos y de instancia antes de haber aprendido variables y flujo de control.

Clase con miembros de instancia

Observa que, al no ser estático el método main(), puede utilizar atributos y métodos de instancia, sin necesidad de que sean static. Los podremos utilizar desde dentro de main() con total normalidad. Prueba con el siguiente código:

class Calculadora {
    int base = 10;   // campo de instancia, accesible desde main

    void main() {
        System.out.println("Base: " + base);
        System.out.println("Doble: " + doble(base));
    }

    int doble(int n) {
        return n * 2;
    }
}

Puedes compilar con normalidad pero, para ejecutar, tendrás que tomar las mismas precauciones que en el caso anterior si utilizas Java 21. La salida en mi ordenador con Java 25:

Clase con parámetros de entrada

También podrías utilizar parámetros de entrada. Prueba el siguiente código:

class App {
    void main(String[] args) {
        System.out.println("Argumentos: " + args.length);
    }
}

Como en los ejemplos anteriores, si ejecutas en Java 21 tendrías que añadir –enable-preview. En Java 25, no sería necesario:

Método main(), sin clase (clases implícitas)

Podemos ir un paso más allá. Podemos utilizar directamente el método main(), sin necesitad de que esté incluido en ninguna clase. Esta característica se denomina unnamed classes o también clases implícitas. Prueba el siguiente código dentro de un fichero llamado, por ejemplo, MiClaseImplicita.java :

// Sin declaración de clase. Requiere --enable-preview en javac 21
void main() {
    System.out.println("Hola");
}

Una diferencia con los ejemplos anteriores es que ahora, si compilas con Java 21, también tendrás que añadir el parámetro –enable-preview. En Java 25 no es necesario:

Como puedes ver, las nuevas versiones de Java ofrecen opciones interesantes que merece la pena explorar. Eso sí, ¡no utilices estas opciones en los exámenes de la asignatura, pues podrías suspender! 🤣

Aumenta tu productividad en Eclipse

1. Introducción

Eclipse IDE es la herramientas elegida en la ETSIST para el desarrollo de aplicaciones Java. Vas a tener que utilizarla, no solo en Programación II, sino en asignaturas posteriores, como Programación Avanzada de Aplicaciones o Software de Comunicaciones. Si en tu futura actividad profesional tienes que programar en Java, Eclipse es una buena opción para desarrollar los programas. Conocer bien sus funcionalidades puede marcar una gran diferencia en tu productividad como programador.

Este documento recoge los atajos de teclado más útiles, así como opciones de configuración y características del entorno que te permitirán trabajar de forma más ágil y eficiente durante las prácticas de la asignatura y en tu futuro profesional.

💡 Consejo: en general, todos los atajos de teclado están asociados a una opción que se puede seleccionar en algún menú o mediante alguna acción de ratón, pero es mucho más eficiente aprender a utilizar atajos de teclado. Dedica unos días a practicar estos atajos conscientemente. Al principio puede parecer lento, pero en poco tiempo serán automáticos y ahorrarás mucho tiempo en cada sesión de trabajo.

📋Nota: puedes descargar la versión pdf de este artículo desde el siguiente enlace:
https://github.com/shiguera/Apuntes_C_Java/raw/master/ManualEclipse.pdf

2. Gestión de vistas y perspectivas

Eclipse organiza su interfaz (workbench, banco de trabajo) en perspectivas. Cada perspectiva es un conjunto de vistas y cada vista permite visualizar determinados contenidos. Por ejemplo, el explorador de paquetes que se muestra a la izquierda de la ventana de Eclipse es una vista. Además, Eclipse muestra en la parte central el área de ecditores, que es donde visualizaremos el contenido de los archivos.

Eclipse tiene una serie de perspectivas predefinidas. La perspectiva Java, que es la que usaremos normalmente al hacer los programas, muestra la vista del explorador de paquetes a la izquierda, el editor de código en el centro, las vistas outline y Task list a la derecha y las vistas de problemas, javadoc y otras en la parte inferior. La figura siguiente muestra la perspectiva Java original, con indicación de las vistas que utiliza:

Hay otras perspectivas. Cuando depuremos programas, por ejemplo, se puede activar la perspectiva de depuración, que muestra otra serie de vistas ordenadas de cierta forma.

Desde el menú “Window -> Show View“, podemos abrir nuevas vistas. Cada vista la podemos situar en el panel que elijamos, pichando en su pestaña y arrastrándola al lugar deseado. También podemos separarlas de la ventana principal, pichando con el botón derecho en su pestaña y seleccionando detach. Para volver a llevarla a la ventana principal, podemos pinchar en la pestaña y arrastrarla.

Podemos seleccionar la perspectiva que queremos activar en la opción de menú “Window -> Perspective -> Open Perspective“.

Además de las perspectivas que trae el programa definidas por defecto, podemos crear nuestras propias perspectivas para autilizar en nuestros proyectos. El menú “Window -> Perspective” ofrece opciones para ello.

También podemos restaurar la disposición original de una perspectiva seleccionando “Window -> Perspective -> Reset Perspective“.

2.1 Vistas recomendadas

  • Package Explorer: navegación por la estructura del proyecto. Actívalo siempre.
  • Project Explorer: es similar a la anterior, pero permite ver todas las carpetas del proyecto, por ejemplo la carpeta bin con los archivos compilados. En un artículo anterior en este mismo blog expliqué cómo hacer para ver la carpeta bin.
  • Outline: muestra la estructura del archivo actual (métodos, atributos). Muy útil en clases largas.
  • Console: salida del programa y mensajes de error. Imprescindible en todo momento.
  • Problems: lista de errores y advertencias. Haz clic en cada problema para ir directo a la línea.
  • Javadoc: muestra la documentación de la clase o método bajo el cursor automáticamente.
  • Call Hierarchy (Ctrl + Alt + H): muestra quién llama a un método y a quién llama él.
  • Type Hierarchy (F4): muestra la jerarquía de herencia de una clase.

2.2 Gestión de ventanas en el editor

Sabemos que se pueden tener varias pestañas abiertas en el editor, cada una correspondiente a una clase o fichero diferente. Pero también podemos partir la ventana del editor en horizontal o en vertical, para visualizar simultáneamente varios ficheros, como se ha hecho en la imagen siguiente:

Seguramente, la forma más cómoda de hacerlo sea arrastrando con el ratón una de las pestañas hacia el borde de la ventana. En la imagen anterior se ha hecho además otro ajuste: pulsando Ctrl + M se maximiza la ventana del editor, ocultando otros paneles, como el panel del explorador de paquetes u otros que hubiera abiertos. Para restituir la vista de dichos paneles solo hay que volver a pulsar Ctrl + M .

Podemos mostrar en una de las subventanas cualquier otra vista que tengamos abierta, por ejemplo el terminal u otra. El procedimiento es el mismo: pinchar en la pestaña de la vista que queremos mover y arrastrarla hasta el punto donde queremos visualizarla.

También es posiible mostrar en dos paneles el mismo fichero, situado en diferentes posiciones de dición (split). Esto puede facilitar la edición del código en ficheros de gran tamaño. Para mostrar un mismo fichero en dos paneles en horizontal hay que teclear Ctrl + AltGr + {. Una nueva pulsación del atajo de teclado indicado, desactivará la opción. Para colocar los dos paneles uno encima del otro hay que pulsar Ctrl + Shift + -.

2.3 Atajos de vistas y perspectivas

AtajoDescripción
Ctrl + MMaximizar o restaurar el editor o vista actual
Ctrl + F8Cambiar entre perspectivas (Java, Debug, Git…)
F12Volver el foco al editor de código
Ctrl + AltGr + {Split horizontal
Ctrl + Shift + -Split vertical

3. Atajos para gestión de ficheros

AtajoDescripción
Ctrl + NNew: abre el diálogo File ->New
Ctrl + SGuardar

4. Atajos Esenciales de Edición de Código

Estos son los atajos que usarás a diario al escribir código Java. Merece la pena aprenderlos desde el primer día.

4.1 Escritura y formato

AtajoDescripción
Ctrl + EspacioAutocompletado de código: completa nombres de clases, métodos, variables y genera plantillas (sysout, for, if, try…)
Ctrl + Shift + FFormatea automáticamente todo el archivo según las convenciones de Java
Ctrl + DElimina la línea actual completa sin necesidad de seleccionarla
Alt + ↑ / Alt + ↓Mueve la línea actual (o selección) hacia arriba o hacia abajo
Ctrl + Alt + ↑ / ↓Duplica la línea actual hacia arriba o hacia abajo
Ctrl + Shift + 7Comenta o descomenta la línea actual con // (en teclado español, / equivale a Shift + 7)
Ctrl + Shift + 7 (bloque)En teclado español, este mismo atajo sobre una selección comenta las líneas individualmente con //. Para comentario de bloque /* ... */ no hay atajo directo: usad el menú Source → Toggle Block Comment
Tab / Shift + TabAumenta o reduce la indentación del bloque seleccionado
Ctrl + Z / Ctrl + YDeshacer / Rehacer última acción
Ctrl + CCopy
Ctrl + VPaste
Ctrl + XCut

💡 Consejo: Ctrl + Espacio es el atajo más poderoso de Eclipse. Úsalo constantemente: no solo completa palabras, también genera estructuras completas como bucles for-each, bloques try-catch, el método main, etc.

Por ejemplo, para escribir System.out.println(), solo tendrás que teclear sysout seguido de Ctrl + espacio. Más adelante, en el apartado sobre Generación automática de código ampliamos información sobre este tema.

4.2 Navegación dentro del código

AtajoDescripción
Ctrl + Clic / F3Ir a la definición de la clase, método o variable bajo el cursor
Alt + ← / Alt + →Retroceder / avanzar en el historial de navegación
Ctrl + LIr a una línea concreta del archivo por número
Ctrl + QVolver a la última posición editada
Ctrl + EMostrar lista de editores abiertos para cambiar rápidamente entre archivos
Ctrl + F6Alternar entre los editores abiertos (como Alt+Tab del sistema)
Ctrl + Shift + GBuscar todas las referencias a un método o variable en el proyecto

4.3 Selección de texto

AtajoDescripción
Shift + Alt + ↑Ampliar la selección al elemento sintáctico superior (seleccionar bloques completos)
Shift + Alt + ↓Reducir la selección al elemento sintáctico inferior
Ctrl + ASeleccionar todo el contenido del archivo
Alt + Shift + AActivar modo de selección por columnas (selección rectangular)

5. Búsqueda y Reemplazo

AtajoDescripción
Ctrl + FAbrir el panel de búsqueda en el archivo actual
Ctrl + HBúsqueda avanzada en todo el proyecto (por archivos, expresiones regulares…)
Ctrl + KBuscar la siguiente ocurrencia del texto seleccionado
Ctrl + Shift + KBuscar la ocurrencia anterior del texto seleccionado
Ctrl + JBúsqueda incremental: encuentra resultados en tiempo real mientras escribes
Ctrl + Shift + RAbrir un recurso (archivo) del proyecto buscando por nombre
Ctrl + Shift + TBuscar y abrir un tipo (clase, interfaz, enum) de todo el workspace
Ctrl + OMostrar el esquema del archivo actual: métodos, atributos, constructores

💡 Consejo: Ctrl + Shift + T y Ctrl + Shift + R son imprescindibles en proyectos grandes. Permiten localizar cualquier clase o archivo instantáneamente sin navegar por el Package Explorer.


6. Refactorización de Código

Eclipse ofrece potentes herramientas de refactorización que modifican el código de forma segura, actualizando automáticamente todas las referencias en el proyecto.

🗒️ Nota: se denomina refactorizar a modificar el código de un programa sin afectar a su funcionamiento. Por ejemplo, cambiar el nombre a una variable o método es una refactorización frecuente.

AtajoDescripción
Alt + Shift + RRenombrar una clase, método, variable o paquete actualizando todas las referencias
Alt + Shift + MExtraer el código seleccionado a un nuevo método
Alt + Shift + LExtraer la expresión seleccionada a una variable local
Alt + Shift + IHacer inline: sustituir el uso de una variable por su valor directamente
Alt + Shift + CCambiar la firma de un método (parámetros, nombre, visibilidad)
Alt + Shift + TAbrir el menú de refactorización completo sobre el elemento seleccionado
Alt + Shift + SAbrir el menú Source: generar getters/setters, constructores, toString…

💡 Consejo: Usa siempre Alt + Shift + R para renombrar, nunca lo hagas manualmente con buscar y reemplazar. Eclipse actualiza todas las referencias del proyecto de forma segura y no os perderéis ninguna ocurrencia.


7. Generación Automática de Código

Eclipse puede generar por ti mucho código repetitivo. Accede al menú Source (Alt + Shift + S) para ver todas las opciones disponibles:

AtajoDescripción
Alt + Shift + S, RGenerar métodos getter y setter para los atributos de la clase
Alt + Shift + S, OGenerar constructores usando campos de la clase
Alt + Shift + S, HGenerar los métodos hashCode() y equals()
Alt + Shift + S, SGenerar el método toString()
Alt + Shift + S, VImplementar métodos abstractos no implementados de interfaces o clases abstractas
Alt + Shift + S, UGenerar métodos de superclase no implementados
Ctrl + 1Correcciones rápidas: resolver imports, crear métodos, capturar excepciones, etc.

💡 Consejo: Ctrl + 1 (Quick Fix) es uno de los atajos más valiosos. Si Eclipse marca un error en rojo, pulsad Ctrl + 1 y os ofrecerá soluciones automáticas: añadir imports, crear métodos que faltan, encapsular en try-catch…

7.1 Plantillas de código (Templates)

Eclipse tiene plantillas de código que se activan escribiendo la abreviatura y pulsando Ctrl + Espacio. Por ejemplo, si tecleas main, seguido de Ctrl + espacio, se escribirá el método main()completo.

AbreviaturaGenera
sysoutSystem.out.println();
syserrSystem.err.println();
forBucle for clásico con índice
foreachBucle for-each sobre una colección
tryBloque try-catch-finally
mainpublic static void main(String[] args)
ifelseEstructura if-else completa

Puedes crear tus propias plantillas en: Window → Preferences → Java → Editor → Templates.

8. Depuración (Debug)

El depurador de Eclipse es una herramienta fundamental para entender el comportamiento de tus programas y localizar errores lógicos de forma eficiente.

AtajoDescripción
F11Ejecutar en modo Debug (lanza la última configuración usada)
Ctrl + Shift + BAñadir o quitar un Breakpoint en la línea actual
F8Continue: continuar la ejecución hasta el siguiente breakpoint
F6Step Over: ejecutar la línea actual sin entrar en el método llamado
F5Step Into: entrar dentro del método llamado en la línea actual
F7Step Return: salir del método actual y volver al llamador
Ctrl + Shift + IInspect: evaluar una expresión o variable en el contexto actual
Ctrl + F2Terminar la sesión de depuración

💡 Consejo: Para depurar eficientemente: pon breakpoints estratégicos al inicio de métodos sospechosos, usa la vista Variables para inspeccionar el estado, y añade Watch Expressions para evaluar expresiones complejas en tiempo real.


9. Opciones de Configuración Útiles

9.1 Compilación automática

Window → Preferences → General → Workspace: activad Build automatically para que Eclipse compile y detecte errores al instante.

9.2 Acciones al guardar (Save Actions)

Window → Preferences → Java → Editor → Save Actions: configurad que al guardar (Ctrl + S) Eclipse formatee el código y organice los imports automáticamente.

💡 Consejo: Activar el formateo automático al guardar es una de las mejores decisiones que puedes tomar. Tu código siempre estará bien indentado sin ningún esfuerzo extra.

9.3 Otros ajustes recomendados

  • Números de línea: clic derecho en el margen izquierdo del editor → Show Line Numbers.
  • Tema oscuro: Window → Preferences → General → Appearance → Theme 'Dark'. Reduce la fatiga visual en sesiones largas.
  • Organizar imports: Ctrl + Shift + O completa imports pendientes y elimina imports innecesarios del archivo actual.

10. Abrir Archivos en el Explorador del Sistema y el Terminal

En ocasiones necesitarás acceder a los archivos del proyecto desde fuera de Eclipse: para compartir ficheros, abrirlos con otra aplicación o lanzar un terminal en esa ubicación. Eclipse facilita esto directamente desde el Package Explorer.

10.1 Abrir la ubicación en el explorador de archivos del sistema

Haz clic derecho sobre el fichero o carpeta en el Package Explorer y selecciona:

Show In → System Explorer

Esto abrirá el explorador de archivos del sistema operativo (Explorador de Windows, Finder en macOS o el gestor de archivos en Linux) con el fichero o carpeta ya seleccionado en su ubicación real en el disco.

💡 Consejo: Esta opción es especialmente útil para localizar rápidamente dónde está guardado tu proyecto en el disco, sin tener que buscar manualmente la carpeta del workspace de Eclipse.

10.2 Abrir un terminal externo desde esa ubicación

Una vez en el explorador de archivos del sistema operativo, puedes abrir un terminal situado directamente en esa carpeta sin necesidad de navegar manualmente hasta ella:

  • Windows: haz clic derecho sobre la carpeta (o sobre el fondo de la ventana si ya estás dentro) y selecciona Abrir en Terminal.
  • macOS: clic derecho sobre la carpeta → Servicios → Nuevo Terminal en la Carpeta (puede requerir activarlo en Preferencias del Sistema → Teclado → Servicios).
  • Linux: clic derecho sobre la carpeta → Abrir en Terminal (el nombre exacto depende del gestor de archivos).

De esta forma puedes ejecutar comandos directamente en el directorio del proyecto, por ejemplo para compilar con javac o ejecutar con java.


11. Personalización de Atajos de Teclado

Eclipse permite consultar y modificar todos los atajos desde un único panel, lo que resulta especialmente útil para resolver conflictos o asignar atajos a comandos que no los tienen (como ocurre con Toggle Block Comment en teclado español).

11.1 Cómo acceder al panel de atajos

Window → Preferences → General → Keys

O directamente pulsando Ctrl + Shift + L dos veces seguidas.

11.2 Consultar combinaciones en uso

En el panel de Keys encontrarás una tabla con todos los comandos del IDE. Puedes filtrarla de dos formas:

  • Por nombre de comando: escribe parte del nombre en el campo Filter para ver si ya tiene atajo asignado y cuál es.
  • Por combinación de teclas: haz clic en el campo Binding y pulsa la combinación que quieres comprobar. Eclipse mostrará si ya está asignada a algún comando.

Ten en cuenta que un mismo atajo puede estar libre en un contexto (Context) aunque esté siendo utilizado en otro. Por ejemplo, un atajo de teclado puede funcionar cuando estás en el editor de código y no funcionar en otras vistas.

11.3 Asignar o modificar un atajo

  1. Busca el comando deseado en el campo Filter (por ejemplo, Toggle Block Comment).
  2. Selecciona el comando en la tabla.
  3. Haz clic en el campo Binding y pulsa la combinación de teclas que quieras asignar.
  4. Comprueba que el campo Conflicts no muestre ningún conflicto.
  5. Pulsa Apply and Close.

💡 Consejo: Si al pulsar la combinación Eclipse detecta un conflicto, aparecerá resaltado en la sección Conflicts. En ese caso puedes eliminar el atajo del comando conflictivo (si no lo usas) o elegir otra combinación libre.

11.4 Ejemplo: asignar un atajo a Toggle Block Comment en teclado español

El comando Toggle Block Comment (comentario de bloque /* ... */) no tiene atajo funcional en teclado español. Para asignárselo:

  1. Abrid Window → Preferences → General → Keys.
  2. En Filter, escribe Toggle Block Comment.
  3. Selecciona el comando y haz clic en Binding.
  4. Pulsa la combinación que quieras, por ejemplo Ctrl + Shift + 8 (comprueba que no haya conflictos).
  5. Pulsa Apply and Close.
AtajoDescripción
Ctrl + Shift + LMostrar la lista completa de todos los atajos disponibles
Ctrl + Shift + L (x2)Abrir las preferencias de atajos de teclado para personalizarlos

12. Productividad en el Editor

12.1 Múltiples cursores y edición en bloque

Eclipse permite editar varias líneas simultáneamente mediante la selección por columnas, también llamada selección rectangular. Es muy útil para modificar código repetitivo en varias líneas a la vez.

Para activarla, pulsa Alt + Shift + A y luego selecciona con el ratón (o con las teclas de dirección) el bloque de líneas que quieres editar. Todo lo que escribas se aplicará en todas las líneas seleccionadas al mismo tiempo.

💡 Consejo: Es especialmente útil para añadir o eliminar un prefijo o sufijo en varias líneas a la vez, como comentar un bloque de declaraciones o alinear asignaciones.

12.2 Comparar dos archivos entre sí

Eclipse permite comparar el contenido de dos archivos directamente desde el Package Explorer, sin necesidad de herramientas externas. Selecciona los dos archivos manteniendo Ctrl pulsado, haz clic derecho y elige:

Compare With → Each Other

Se abrirá una vista de diferencias que muestra lado a lado el contenido de ambos archivos, resaltando las líneas que difieren. Es muy útil para comparar versiones de una práctica o revisar cambios entre dos implementaciones.


13. Comprensión del Código

13.1 Call Hierarchy y Type Hierarchy

Estas dos vistas ayudan a entender la estructura y el flujo de un programa, especialmente cuando se trabaja con herencia y polimorfismo.

Call Hierarchy (Ctrl + Alt + H sobre un método): muestra un árbol con todos los métodos que llaman al método seleccionado (Caller Hierarchy) y todos los métodos a los que él llama (Callee Hierarchy). Es muy útil para entender el impacto de un cambio o seguir el flujo de ejecución.

Type Hierarchy (F4 sobre una clase o interfaz): muestra la jerarquía completa de herencia, tanto hacia arriba (superclases e interfaces implementadas) como hacia abajo (subclases). Imprescindible cuando trabajéis con herencia para tener una visión global de la jerarquía de clases.

13.2 Javadoc emergente al pasar el ratón

Simplemente dejando el cursor del ratón sobre el nombre de una clase, método o variable durante un momento, Eclipse muestra automáticamente una ventana emergente con su documentación Javadoc: descripción, parámetros, valor de retorno y excepciones.

No es necesario ningún atajo: basta con sobrevolar con el ratón. Si quieres verlo de forma fija en una vista permanente, activa la vista Javadoc desde Window → Show View → Javadoc, que se actualiza automáticamente según el elemento bajo el cursor.

💡 Consejo: Aprovecha esta función constantemente al usar las clases de la API de Java (listas, colecciones, String, etc.). Es mucho más rápido que buscar en la documentación oficial.


14. Gestión del Proyecto

14.1 Exportar el proyecto como archivo .jar

Cuando quieras entregar una práctica como ejecutable, o reutilizar tu código en otro proyecto, puedes empaquetarlo como un archivo .jar:

  1. Clic derecho sobre el proyecto en el Package Explorer → Export...
  2. Selecciona Java → Runnable JAR file (si quieres un JAR ejecutable) o Java → JAR file (si es una librería).
  3. Elige la clase que contiene el método main como punto de entrada (solo para JAR ejecutable).
  4. Indica la ruta de destino y pulsa Finish.

💡 Consejo: Un Runnable JAR puede ejecutarse directamente con java -jar archivo.jar desde el terminal, sin necesidad de tener Eclipse instalado.

14.2 Incorporar un archivo .jar externo al classpath del proyecto

Cuando necesites usar una librería externa (por ejemplo, una proporcionada por el profesor o descargada de Internet), debes añadir su .jar al classpath del proyecto para que Eclipse la reconozca:

  1. Copia el archivo .jar en una carpeta dentro del proyecto (por convenio, se suele llamar lib/).
  2. Clic derecho sobre el archivo .jar en el Package Explorer → Build Path → Add to Build Path.

A partir de ese momento Eclipse reconocerá las clases de esa librería y ofrecerá autocompletado y Javadoc para ellas (si el .jar incluye documentación).

💡 Consejo: Si el .jar no aparece en el Package Explorer, comprobad que está dentro de la carpeta del proyecto. Si lo tenéis en otra ubicación del disco, usad Build Path → Configure Build Path → Libraries → Add External JARs... en su lugar.

14.3 Importar y exportar preferencias

Si trabajas en varios equipos (por ejemplo, en casa y en el laboratorio), puedes exportar toda tu configuración de Eclipse —atajos personalizados, plantillas, formato de código— y recuperarla en otro equipo.

  • Exportar: File → Export → General → Preferences. Guardad el archivo .epf resultante.
  • Importar: File → Import → General → Preferences. Seleccionad el archivo .epf.

💡 Consejo: Guardad tu archivo .epf en la nube o en el mismo repositorio Git del proyecto para tenerlo siempre disponible.

14.4 Working Sets

Cuando tengas varios proyectos abiertos a la vez en el workspace, el Package Explorer puede volverse difícil de manejar. Los Working Sets permiten agrupar proyectos relacionados y mostrar solo los que os interesan en cada momento.

Para crearlos: haz clic en el menú desplegable del Package Explorer (icono de flecha en la esquina superior derecha) → Select Working Set...New....


15. Calidad del Código

15.1 Warnings y nivel de severidad

Eclipse no solo detecta errores de compilación (marcados en rojo), sino también advertencias (warnings, marcadas en amarillo) que señalan código potencialmente problemático: variables declaradas pero no usadas, conversiones implícitas entre tipos, comparaciones innecesarias, etc.

Es recomendable prestarles atención aunque no impidan compilar. Puedes configurar qué situaciones generan error, warning o se ignoran en:

Window → Preferences → Java → Compiler → Errors/Warnings

💡 Consejo: Un código sin warnings es señal de mayor calidad y rigor. Acostúmbrate a mantener la vista Problems limpia, no solo de errores sino también de advertencias.

15.2 Task Tags: TODO, FIXME y XXX

Eclipse reconoce automáticamente ciertos comentarios especiales en el código y los muestra como tareas pendientes en la vista Tasks (Window → Show View → Tasks):

EtiquetaUso habitual
// TODOFuncionalidad pendiente de implementar
// FIXMECódigo que funciona pero necesita ser corregido
// XXXCódigo problemático o cuestionable que requiere atención

Por ejemplo:

// TODO implementar el manejo de excepciones
// FIXME este cálculo falla con valores negativos

Estas etiquetas aparecerán listadas en la vista Tasks, lo que permite llevar un registro de las partes del código que aún necesitan trabajo sin perder el hilo.

💡 Consejo: Usa // TODO mientras desarrollas para marcar partes incompletas, y eliminalos antes de entregar la práctica. Un TODO en el código entregado indica que sabes que algo falta.


16. Tabla Resumen — Cheat Sheet

Los 20 atajos que debes conocer sí o sí:

AtajoDescripción
Ctrl + EspacioAutocompletar código
Ctrl + 1Corrección rápida (Quick Fix)
Ctrl + Shift + FFormatear código
Ctrl + Shift + OOrganizar imports
Ctrl + DEliminar línea actual
Ctrl + SGuardar (Save)
Ctrl + /Comentar / descomentar línea
Ctrl + Z / YDeshacer / Rehacer
F3 / Ctrl + ClicIr a definición
Alt + ← / →Navegar historial de edición
Ctrl + Shift + TBuscar tipo (clase)
Ctrl + Shift + RBuscar recurso (archivo)
Ctrl + OEsquema del archivo actual
Alt + Shift + RRenombrar (refactoring)
Alt + Shift + MExtraer método
Alt + Shift + SMenú Source (generar código)
Ctrl + Shift + BAñadir / quitar breakpoint
F5 / F6 / F7 / F8Debug: Step Into / Over / Return / Continue
Ctrl + MMaximizar editor
Ctrl + Shift + LVer todos los atajos disponibles

¡La práctica hace al maestro! Cuanto más uses estos atajos, más naturales te resultarán.

Compilar y ejecutar Java desde el terminal

El modelo de ejecución de Java se inicia con el código fuente del programa escrito en lenguaje Java y almacenado en ficheros .java. Dicho código fuente se compila a un código intermedio llamado bytecode, que se guarda en ficheros .class. Finalmente, la máquina virtual de Java (JVM, Java Virtual Machine) ejecuta dicho código. La figura siguiente esquematiza este modelo de ejecución.

La utilización de Eclipse facilita la tarea de compilar y ejecutar nuestros programas Java, aunque hace perder un poco la perspectiva del proceso de compilación y ejecución. En otro artículo anterior ya comente cómo configurar Eclipse para sortear, al menos en parte, este inconveniente.

Creo que es bueno practicar la compilación y ejecución de programas escritos en Java utilizando el terminal. Además de redundar en el dominio de las herramientas, cuando hay que ejecutar un programa con parámetros de entrada, resulta más sencillo hacerlo desde el terminal que desde Eclipse.

En este artículo voy a explicar cómo hacerlo en distintas circunstancias, desde un programa sencillo compuesto de una única clase, hasta programas complejos con varias clases distribuidas en paquetes y que utilizan bibliotecas externas .jar.

Programa con una única clase

El caso más sencillo es cuando tenemos una única clase, Main.java, dentro del directorio del proyecto. La estructura sería la de la siguiente figura:

Para compilar el programa, tendremos que abrir un terminal situado en la carpeta del proyecto y teclear la siguiente instrucción:

javac Main.java

Si todo va bien y el programa no tiene errores, el resultado será que el compilador de Java (javac) creará el fichero Main.class, que es el compilado bytecode de Main.java. La estructura de la carpeta del proyecto quedará:

Puedes comprobarlo tú mismo listando los archivos de la carpeta con la orden dir.

Para ejecutar el programa, hay que utilizar la máquina virtual de Java (JVM) de nuestro sistema, que es el programa java. A la JVM hay que decirle el nombre de la clase que contiene el método main() ejecutable, en este caso:

java Main

Observa que se indica el nombre de la clase, sin extensiones.

Programa con varias clases

Podría suceder que el programa tuviera más de una clase, pero todas en el mismo directorio del proyecto. Por ejemplo, supón que nuestro programa, además de la clase Main.java, utiliza una clase auxiliar llamada Punto.java y que las dos clases están en el directorio del proyecto:

Para compilar, tendremos que indicar a javac el nombre de todas las clases que queramos compilar:

javac Main.java Punto.java

Cuando queremos compilar todos los ficheros .java de una carpeta también se puede usar el comodín * (asterisco), que se puede leer como “todos“:

javac *.java

Esta instrucción compilaría todos los ficheros .java que haya en el directorio. En ambos casos, el resultado será que se crearán los ficheros .class correspondientes:

Para ejecutar el programa, la instrucción sería la misma que antes. Aunque haya varios .class, al invocar la JVM solo hay que pasarle el nombre de la clase que contiene el método main():

java Main

Esta estructura, con los ficheros .java y los ficheros .class en el mismo directorio, es la que resulta al crear el proyecto con Eclipse y seleccionar la opción “Use project folder as root for sources and class files”, como muestra la siguiente figura:

Redirigir los .class al directorio bin

En el apartado anterior, el fichero .class que se genera al compilar, lo hemos guardado en el mismo directorio del proyecto donde teníamos los ficheros .java. Es habitual que los ficheros compilados de un proyecto se guarden en un directorio diferente, por ejemplo en el directorio bin.

Podemos indicar al compilador de Java en qué directorio queremos que ponga los ficheros .class compilados, utilizando el parámetro -d. La orden que habría que teclear en el terminal sería:

javac -d bin *.java

Con esta orden, se compilarán todos los ficheros .java que haya en la carpeta del proyecto y los ficheros .class resultantes se pondrán en el directorio bin. La estructura de carpetas y ficheros, despues de compilar, quedaría:

Para ejecutar el programa resultante, hay que indicar a la JVM el nombre de la clase que contiene el método main(). Además, hay que indicar a la JVM la ruta donde tiene que buscar las clases compiladas, utilizando el parámetro -classpath o su forma abreviada -cp. Si ejecutamos la instrucción desde el directorio del proyecto, la orden que hay que teclear es:

java -cp bin Main

La instrucción anterior le dice a la máquina virtual: “Las clases compiladas están en la carpeta bin y tienes que ejecutar la clase Main”.

Caso general: directorios src y bin

El caso general sería el de un proyecto que tiene los ficheros fuente .java en el directorio src, repartidos en distintos paquetes. Además, queremos que los ficheros .class de las clases compiladas se guarden en el directorio bin. La estructura de ficheros antes de la compilación podría ser la siguiente:

Por supuesto, los ficheros .java de las clases tendrán que tener indicadas las instrucciones package que les corresponda en cada caso.

Con el terminal situado en la carpeta del proyecto, la orden para compilar todo y hacer la salida de los .class al directorio bin es la siguiente:

javac -d bin src\geometria\*.java src\principal\*.java 

En este caso, le estamos pidiendo al compilador que compile todos los ficheros .java que haya en las carpetas src\geometria y src\principal y coloque los ficheros compilados en la carpeta bin.

El resultado sería el siguiente:

Observa que los ficheros compilados .class quedan dentro de la carpeta bin, organizados con la misma estructura de paquetes que tienen en la carpeta src del código fuente.

Para ejecutar el programa, habrá utilizar el parámetro -cp, para indicar a la JVM la ruta donde tiene que buscar las clases compiladas y decirle el nombre de la clase que queremos ejecutar:

java -cp bin principal.Main

Fíjate en que el nombre de la clase que queremos ejecutar tiene que llevar como prefijo el paquete en el que se encuentre dicha clase. Lo que le decimos a la máquina virtual de Java es: “busca las clases compiladas en la carpeta bin y ejecuta la clase principal.Main”.

Esta estructura de carpetas es la que crea Eclipse al crear un proyecto cuando elegimos la opción “Create separate folders for sources and class files“.

Ejecutar programas con parámetros de entrada

La primera vez que queramos ejecutar desde Eclipse un programa que requiere parámetros de entrada, habrá que crear una configuración de ejecución (Run configuration). Se accede a través de la opción de menú “Run -> Run configurations” o pichando sobre la clase que tiene el método main() con el botón derecho del ratón y seleccionando “Run as -> Run configurations“.

Dentro del diálogo que aparece, habrá que seleccionar la pestaña “Arguments” y poner el valor de los argumentos que queramos pasar al programa dentro del recuadro “Program arguments“, como muestra la figura siguiente:

Cada vez que queramos ejecutar el programa, cambiando el valor de los argumentos, habrá que abrir la configuración de ejecución y modificar el valor de los mismos. Es un proceso ineficiente y tedioso.

Si ejecutamos el programa desde el terminal, es mucho más sencillo. Refiriéndonos al ejemplo utilizado anteriormente, si quisiéramos ejecutar Main y pasarle dos argumentos, la orden que habría que teclear en el terminal sería:

java -cp bin principal.Main argumento1 argumento2

Queda claro que el proceso es más sencillo. Ademas, no necesitaríamos compilar desde la consola. Podemos estar usando Eclipse, que se encargará de compilar automáticamente nuestros ficheros fuente y utilizar el terminal simplemente para ejecutar el programa.

Utilización de bibliotecas .jar

Algunos programas utilizan bibliotecas externas .jar que contienen clases compiladas. Imagina que la estructura de nuestro proyecto es la de la siguiente figura:

El proyecto, además de las clases propias, utiliza clases de biblioteca.jar. Para compilar este proyecto tendremos que decir al compilador:

  • En qué directorio queremos poner las clases compiladas: parámetro -d.
  • Dónde puede encontrar las clases compiladas que necesite: parámetro -cp.
  • Qué ficheros .java queremos que compile.

La orden que habría que teclear en la consola sería:

javac -d bin -cp lib\biblioteca.jar src\principal\*.java src\geometria\*.java

Como resultado, el compilador creará el directorio bin con las clases compiladas dentro de él, organizadas en los paquetes que corresponda, como muestra la figura siguiente:

Para ejecutar el programa, será necesario indicar a la JVM que hay clases compiladas que las tiene que buscar en el directorio bin y otras que están en biblioteca.jar. La orden sería la siguiente:

java -cp bin;lib\biblioteca.jar principal.Main

Observa que en el parámetro -cp se han puesto dos rutas para clases compiladas, separadas por punto y coma.

En Eclipse, para incorporar una biblioteca externa a nuestro proyecto hay que utilizar la opción de menú “Project -> Properties -> Java Build Path“. En la pestaña Libraries, habrá que añadir el fichero biblioteca.jar al classpath, como muestra la siguiente figura:

JSHELL

JShell es una herramienta que se instala con el Java Development Kit (JDK) y es muy útil para probar código Java, sin necesidad de hacer un programa completo y tener que compilarlo.

Para arrancar JShell tendrás que abrir un terminal y teclear:

jshell

Si todo va bien, deberías ver una pantalla similar a la de la siguiente figura:

Si quieres salir de la aplicación, tienes que teclear:

/exit

Nota: si al teclear jshell, no aparece el prompt de la aplicación, puede significar que no tienes instalado el JDK o que no tienes configurada correctamente la variable PATH del sistema.

Utilización como calculadora

Un posibilidad que ofrece JShell y que puede ser útil en determinadas circunstancias es utilizarlo como calculadora. Puedes realizar operaciones matemáticas y tendrás acceso a los métodos de la clase Math y al uso de variables.

Por ejemplo, trata de hacer en tu consola las operaciones que aparecen en la siguiente figura u otras similares:

Observa que, tras cada operación, aparece el signo del dólar ($) seguido de un número. Son variables que quedan guardadas en la memoria y pueden ser invocadas a posteriori. Observa la siguiente serie de operaciones, en la que se reutiliza uno de los resultados obtenidos:

Variables Java

También podemos definir y utilizar variables Java, pero en ese caso habrá que declarar y asignar las variables con su tipo de dato, como lo haríamos dentro de un programa:

Como detalle, puedes ver que estas instrucciones Java no las terminamos en punto y coma. En jshell no es necesario terminar las instrucciones sueltas en punto y coma (Cuando definamos clases, sí que hay que respetar la sintaxis Java).

En cualquier momento puedes consultar la lista de variables que hay en memoria tecleando la orden /vars:

Si quieres borrar totalmente el contenido de la memoria puedes teclear la orden /reset.

Probar código

Es cómodo probar código sin tener que teclear una clase completa, compilarla y ejecutarla. Además, para ver la salida por pantalla de las instrucciones, no necesitamos usar métodos System.out.println():

Probar funciones

Puedes teclear el código de una función y utilizar la función posteriormente.

Las funciones que hayas definido quedan guardadas en memoria. Puedes obtener el listado de las funciones que hayas definido en la sesión utilizando la orden /methods. Si tecleas /reset se borrará la memoria completa, incluyendo variables y funciones.

Clases y objetos

Puedes crear clases y objetos:

Podrías crear también, no solo clases, sino interfaces o enumeraciones. Para listar los tipos de datos que hayas definido en la sesión tienes que teclear la orden /types.

Una vez definido un tipo de datos, puedes crear funciones que lo utilicen, sin necesidad de incluir la función como método del tipo:

Guardar el trabajo

La orden /list sirve para ver las instrucciones que has tecleado. La orden /save, seguida de un nombre de fichero, sirve para guardar dichas órdenes en un fichero y poderlas reutilizar a posteriori. Podrías cargar el fichero en JShell al abrirlo o utilizando la orden /open.

Cargar ficheros

Podemos cargar ficheros existentes utilizando la orden /open. Imagina que tienes un fichero llamado Punto.java, en el directorio en el que has abierto JShell, con el siguiente contenido:

public class Punto {
   public int x, y;

   public Punto(int x, int y) {
      this.x = x;
      this.y = y;
   }   
   public double dist(Punto p) {
      return Math.sqrt(x*p.x + y*p.y);
   }
   public double distOrigen() {
      return Math.sqrt(x*x + y*y);
   }
}

Puedes abrir el fichero y utilizar sus definiciones:

Puedes cargar un fichero en memoria en el momento de abrir JShell tecleando su nombre a continuación de jshell:

jshell Punto.java

El fichero puede ser una clase .java o un fichero de órdenes guardadas previamente con /save. En ambos casos, las ordenes contenidas en el fichero se ejecutarán como si las tecleases dentro del terminal.

Edición en consola

Puedes usar las teclas de flecha arriba/abajo, para editar instrucciones anteriores y moverte dentro de ellas para cambiar su contenido. Esto permite modificar instrucciones anteriores que hayamos utilizado para definir tipos de datos o funciones.

Las instrucciones multilínea aparecerán de una sola vez y podrás moverte dentro de ellas para editarlas.

Cuando se necesita modificar un método ya definido, JShell ofrece el comando /edit seguido del nombre del método. Este comando abre una pequeña ventana de edición gráfica integrada en la que puede modificarse el código y confirmar los cambios con el botón Accept:

 /edit grad2rad

El editor integrado de JShell es una ventana gráfica propia de la herramienta, independiente del editor del sistema operativo. Funciona sin ninguna configuración adicional en Windows, macOS y en Linux con entorno gráfico de escritorio.

💡El editor de JShell se puede utilizar también para editar clases o interfaces. La condición para editar una función o una clase es que tienen que estar definidas previamente.

Un truco útil para crear una clase es definirla vacía en JShell y luego editarla en el editor para completar atributos y métodos de manera más cómoda. Algo parecido se puede hacer con las funciones y los interfaces.

Resumen de órdenes

La tabla siguiente muestra un resumen de las órdenes que hemos mostrado en el artículo.

InstrucciónExplicación
/exitCierra la consola JSHELL
CTRL + LLimpia la pantalla
/varsLista las variables en memoria
/methodsLista las funciones en memoria
/typesLista los tipos de datos definidos
/listLista las órdenes tecleadas con anterioridad
/resetLimpia la memoria de la sesión actual
/open nombre_ficheroAbre un fichero y carga sus definiciones
/save nombre_ficheroGuarda las ordenes en un fichero
/edit identificadorAbre un editor para editar el contenido de una función, un método o un interface

El separador de decimales en Java

El Locale es la configuración regional que le dice a un programa informático cómo mostrar y formatear datos según un idioma, país y cultura específicos.

El Locale afecta a la forma de mostrar diversos elementos:

  • Números: separador decimal, miles
  • Fechas: DD/MM/YYYY vs MM/DD/YYYY
  • Moneda: €1.234,56 vs $1,234.56
  • Mayúsculas/minúsculas: Ñ → ñ (turco: i → İ)
  • Ordenación: ñ después de n en español

Por ejemplo, según el Locale seleccionado, estas serían distintas formas de mostrar el mismo número 1234.56:

🇺🇸 USA (en_US): 1,234.56
🇪🇸 España (es_ES): 1.234,56
🇩🇪 Alemania (de_DE): 1.234,56
🇫🇷 Francia (fr_FR): 1 234,56
🇯🇵 Japón (ja_JP): 1,234.56

El Locale consta de un idioma, un país y, opcionalmente, una variante. Por ejemplo:

Locale = Idioma + País [+ Variante]
ej: es_ES   → español (España)
    en_US   → inglés (Estados Unidos)
    pt_BR   → portugués (Brasil)

Al arrancar el sistema operativo (Windows, Linux, Mac,…) se carga un Locale por defecto que es el que usarán nuestros programas Java, salvo que indiquemos otra cosa.

Puedes comprobar el Locale que tienes cargado en el ordenador mediante la siguiente instrucción:

System.out.println(Locale.getDefault());

En mi caso, la salida ha sido:

es_ES

que corresponde al idioma español de España.

El Locale afecta a la forma de leer o escribir números double, ya sea en el terminal o en ficheros de texto.

Dentro de nuestros programas, podemos establecer el Locale de la siguiente forma:

Locale.setDefault(Locale.UK); // Locale del Reino Unido
Locale.setDefault(Locale.US); // Locale de Estados Unidos
Locale.setDefault(new Locale("es", "ES"));

Imprimir números double

Si se utilizan los métodos print() o println(), los números double se imprimen utilizando el punto como separador de decimales, independientemente del Locale activo en el ordenador:

System.out.println(1234.56); // Imprime 1234.56

El comportamiento es diferente cuando se usan métodos printf(). Con la configuración en español, cuando imprimimos un número double utilizando la instrucción printf(), el separador de decimales será la coma, no el punto:

System.out.printf("%.2f%n", 1234.56); // Imprime 1234,56

Si queremos que, además del separador de decimales, se imprima el separador de miles, hay que añadir al especificador de formato una coma antes del punto decimal:

System.out.printf("%,.2f%n", 1234.56); // Imprime 1.234,56

Si queremos forzar el uso del punto como separador de decimales podemos hacer:

Locale.setDefault(Locale.UK); // Locale del Reino Unido
System.out.printf("%.2f%n", 1234.56); // Imprime 1234.56
System.out.printf("%,.2f%n", 1234.56); // Imprime 1,234.56

Este comportamiento al escribir es el mismo cuando se escribe en el terminal o cuando se escribe en ficheros de texto.

¿Y por qué los métodos print() y println() utilizan siempre el punto como separador de decimales, independientemente del Locale que haya activo? La razón es que, internamente, estos métodos convierten los números double a cadena de texto utilizando el método toString() de la clase Double y dicho método no utiliza ningún formato regional, siempre utiliza el punto decimal en la conversión.

Hay que tener claras dos cosas:

  • Internamente, los números double no se ven afectados de ninguna manera por el Locale del ordenador. La representación interna del número utiliza otro tipo de evaluación, el Locale solo afecta a la representación textual del número al hacer entradas o salidas desde el terminal o desde ficheros de texto.
  • Al codificar, al escribir los programas, tenemos que utilizar siempre el punto como separador de decimales.

Leer números double

Con el ordenador usando el Locale español, si leemos un número double desde el terminal utilizando el método nextDouble() de la clase Scanner, deberá estar escrito con la coma como separador de decimales. Si el número está escrito con el punto, el programa lanzará una excepción y se interrumpirá abruptamente.

Prueba el siguiente programa:

package principal;

import java.util.Locale;
import java.util.Scanner;

public class Main {
   public static void main(String[] args) {
      System.out.println(Locale.getDefault());
	
      Scanner sc = new Scanner(System.in);
      System.out.print("Teclee un número con decimales: ");
      double x = sc.nextDouble();
      sc.close();
   }
}

He ejecutado el código en mi ordenador, configurado con Locale español y he tecleado el número utilizando el punto como separador de decimales. El resultado ha sido el siguiente:

Observa que el programa lanza una excepción del tipo InputMismatchException y se interrumpe.

He repetido la ejecución pero, en esta ocasión, he tecleado el número utilizando la coma como separador de decimales:

Observa que ahora el programa ha funcionado correctamente.

La clase Scanner ofrece el método setLocale(), que permite elegir un determinado Locale en las lecturas que haga. Por ejemplo, en el código siguiente, el primer double se lee utilizando el Locale del Reino Unido y, el segundo, utilizando el Locale español:

Scanner sc = new Scanner(System.in);

sc.useLocale(Locale.UK);
double x = sc.nextDouble(); // Espera leer 3.14

sc.useLocale(new Locale("es", "ES"));
double y = sc.nextDouble(); // Espera leer 3,14

Leer cadenas de texto y convertirlas en double

Otra técnica que puede ser útil al leer números con Scanner es leer una línea de texto con nextLine() y luego convertir la cadena leída a double utilizando el método estático parseDouble() de la clase Double.

El método Double.parseDouble(), al igual que sucede con el método toString(), siempre trabaja con el punto como separador de decimales. El siguiente ejemplo, leería un número double que debería estar escrito con punto decimal:

double z = Double.parseDouble(sc.nextLine());

Imponer un Locale para todo el programa

En programas profesionales, si no tomamos precauciones, puede resultar que la ejecución sea diferente según el Locale del ordenador en el que se esté ejecutando el programa. Por tanto, es necesario tomar medidas de forma que la ejecución siempre sea la misma, independientemente del Locale que esté configurado en el ordenador en el que se ejecute el programa.

Podemos imponer al principio del programa que se utilice un Locale determinado. Esta solución afectará tanto a las entradas que hagamos a través de objetos Scanner como a las salidas que hagamos con métodos printf(). Por ejemplo, podríamos imponer el Locale del Reino Unido o el de Estados Unidos y forzar a que las entradas y salidas de números decimales haya que hacerlas usando el punto como separador de decimales. La instrucción sería una de las dos siguientes:

Locale.setDefault(Locale.UK);
Locale.setDefault(Locale.US);

Otra opción sería imponer el Locale español de España. Para seleccionar el Locale de España no disponemos de una constante predefinida como en los casos anteriores y la instrucción que habría que poner al principio del programa sería:

Locale.setDefault(new Locale("es", "ES"));

También es posible imponer el Locale pasando un parámetro a la Máquina Virtual de Java al ejecutar el programa. Si ejecutamos desde el terminal y queremos imponer el Locale de Estados Unidos, habría que hacer:

java -Duser.language=en -Duser.country=US principal.Main

En Eclipse, podemos configurar la ejecución del programa y pasarle los argumentos a la JVM, como se muestra en la figura:

Cualquiera que sea la opción que uses, en programas profesionales en los que se realicen cálculos científicos con números decimales, es necesario tomar las precauciones necesarias para que el programa se ejecute de manera correcta en cualquier entorno de ejecución.

Ver la carpeta bin y los archivos .class en Eclipse

Eclipse ofrece lo que llama perspectivas, que son disposiciones concretas de las ventanas y paneles del editor. Las perspectivas se seleccionan en el menú “Window”.

Cuando utilizamos Eclipse Standard Edition para editar nuestros proyectos Java, lo más frecuente es tener activada la perspectiva Java, que es la perspectiva por defecto de la edición estándar de Eclipse.

En esta perspectiva, el panel de la izquierda está ocupado por la vista llamada Explorador de paquetes. El explorador de paquetes no muestra ni la carpeta .bin, en la que solemos guardar los ficheros compilados, ni los ficheros .class, aunque los tuviéramos en otra carpeta.

La siguiente figura muestra la vista del Explorador de paquetes de Eclipse, con un proyecto abierto, donde se puede apreciar que la carpeta bin no se muestra:

Para ejecutar un programa Java, el procedimiento habitual es pichar con el botón derecho del ratón sobre el fichero .java que contiene el método main() y elegir la opción “Run as -> Java application”.

Esto puede llevar a los programadores nóveles a pensar que están ejecutando el fichero .java y eso es un error conceptual importante. Los ficheros .java no son ejecutables, primero hay que compilarlos a bytecode (.class) utilizando el compilador de Java y luego, lo que se ejecuta, es el método main() del fichero .class que lo contenga.

Vamos a explicar cómo activar la visualización de la carpeta .bin y de los ficheros .class. Para ello, hay que mostrar la vista Explorador de proyectos, que la podemos activar desde la opción de menú “Window -> Show View -> Project Explorer“. El resultado será el de la siguiente figura:

Por defecto, esta vista tampoco muestra la carpeta bin ni los ficheros .class. Tendremos que configurar la vista de manera adecuada. Hay que abrir el menú de la vista, que son los tres puntos que se muestran en la barra de herramientas de la vista y elegir la opción denominada “Filters and customization”, como se muestra en la siguiente figura:

Al acceder a esta opción del menú de la vista, se nos ofrece una lista de tipos de elementos para los que podemos activar o desactivar el filtro: si el filtro está activado, ese tipo de elementos no se mostrará en la vista.

Tenemos que asegurarnos de no tener activados los filtros correspondientes a*.class resources y Java output folders, como se muestra en la siguiente figura:

Con esto, ya podremos ver en el explorador de proyectos la carpeta bin y los ficheros .class. Es posible que tengas que refrescar la vista para que se hagan efectivas las opciones de visualización seleccionadas, utilizando la opción de menú “File -> Refresh” (F5).

La siguiente figura muestra el resultado con el proyecto usado en los ejemplos, donde se puede ver la carpeta bin y los ficheros .class dentro de ella.

Ahora puedes hacer la siguiente prueba: borra el fichero .class correspondiente a la clase Java que contiene el método main(). A continuación, vuelve a probar a ejecutar el fichero .java correspondiente. Podrás comprobar que no se puede ejecutar el programa.

Para regenerar el fichero compilado .class, haz cualquier modificación en el fichero fuente .java y vuelve a guardarlo en el disco. Verás que, cuando grabas el fichero .java modificado, Eclipse recompila el proyecto y vuelve a generar los .class. Cuando estamos trabajando con Eclipse, cada vez que modificamos un fichero .java, Eclipse recompila el proyecto.

De modo que, a partir de ahora, cada vez que ejecutes un programa Java, ten bien presente que los ficheros .java no son ejecutables y que es necesario compilar y ejecutar.