EDITAR: Respondí esta pregunta porque hay un montón de personas que aprenden programación preguntando esto, y la mayoría de las respuestas son muy competentes técnicamente, pero no son tan fáciles de entender si eres un novato. Todos éramos novatos, así que pensé en probar una respuesta más amigable para los novatos.
Los dos principales son el polimorfismo y la validación. Incluso si es solo una estúpida estructura de datos.
Digamos que tenemos esta clase simple:
public class Bottle {
public int amountOfWaterMl;
public int capacityMl;
}
Una clase muy simple que contiene la cantidad de líquido que contiene y su capacidad (en mililitros).
¿Qué pasa cuando lo hago?
Bottle bot = new Bottle();
bot.amountOfWaterMl = 1500;
bot.capacityMl = 1000;
Bueno, no esperarías que eso funcione, ¿verdad? Desea que haya algún tipo de control de cordura. Y peor, ¿qué pasa si nunca especifiqué la capacidad máxima? Oh querido, tenemos un problema.
Pero también hay otro problema. ¿Qué pasaría si las botellas fueran solo un tipo de contenedor? ¿Qué pasaría si tuviéramos varios contenedores, todos con capacidades y cantidades de líquido lleno? Si pudiéramos hacer una interfaz, podríamos dejar que el resto de nuestro programa acepte esa interfaz, y las botellas, bidones y todo tipo de cosas simplemente funcionarían de manera intercambiable. ¿No sería eso mejor? Como las interfaces exigen métodos, esto también es algo bueno.
Terminaríamos con algo como:
public interface LiquidContainer {
public int getAmountMl();
public void setAmountMl(int amountMl);
public int getCapacityMl();
}
¡Excelente! Y ahora solo cambiamos Botella a esto:
public class Bottle extends LiquidContainer {
private int capacityMl;
private int amountFilledMl;
public Bottle(int capacityMl, int amountFilledMl) {
this.capacityMl = capacityMl;
this.amountFilledMl = amountFilledMl;
checkNotOverFlow();
}
public int getAmountMl() {
return amountFilledMl;
}
public void setAmountMl(int amountMl) {
this.amountFilled = amountMl;
checkNotOverFlow();
}
public int getCapacityMl() {
return capacityMl;
}
private void checkNotOverFlow() {
if(amountOfWaterMl > capacityMl) {
throw new BottleOverflowException();
}
}
Dejaré la definición de BottleOverflowException como un ejercicio para el lector.
Ahora note cuán más robusto es esto. Podemos lidiar con cualquier tipo de contenedor en nuestro código ahora aceptando LiquidContainer en lugar de Bottle. Y cómo estas botellas manejan este tipo de cosas puede diferir. Puede tener botellas que escriben su estado en el disco cuando cambia, o botellas que guardan en bases de datos SQL o GNU sabe qué más.
Y todo esto puede tener diferentes formas de manejar varios whoopsies. La botella solo comprueba y si se desborda, arroja una RuntimeException. Pero eso podría ser lo incorrecto. (Hay una discusión útil sobre el manejo de errores, pero lo mantengo muy simple a propósito. Las personas en los comentarios probablemente señalarán las fallas de este enfoque simplista.;))
Y sí, parece que pasamos de una idea muy simple a obtener respuestas mucho mejores rápidamente.
Tenga en cuenta también que no puede cambiar la capacidad de una botella. Ahora está en piedra. Podrías hacer esto con un int declarándolo final. Pero si se trata de una lista, puede vaciarla, agregarle cosas nuevas, etc. No puede limitar el acceso a tocar las entrañas.
También está la tercera cosa que no todos han abordado: los captadores y los establecedores usan llamadas a métodos. Eso significa que se ven como métodos normales en cualquier otro lugar. En lugar de tener una sintaxis específica extraña para DTO y otras cosas, tienes lo mismo en todas partes.