Resumen teórico
Mira a tu alrededor en tu cuarto. Una lámpara tiene un interruptor y una luz. Una mascota tiene un nombre y sabe comer y jugar. Cada cosa guarda algo de información sobre sí misma y sabe hacer algunas acciones. La programación orientada a objetos, o POO, construye programas del mismo modo: el programa es un equipo de pequeños objetosun paquete con datos y las acciones que trabajan sobre esos datos que guardan su propia información y se piden unos a otros que hagan cosas.
Casi todas las aplicaciones que usas están escritas así: juegos, bancos, tiendas, apps del celular. Los programas grandes son difíciles de cambiar sin romper algo. La POO da a los programadores herramientas para decidir dónde vive cada pedazo de conocimiento, de modo que un cambio en un lugar no rompa otros diez. Ese es el objetivo real de cada idea de este resumen.
Vas a aprender qué son las clases y los objetos, cómo los objetos esconden lo que llevan dentro (encapsulamiento), cómo una clase puede apoyarse en otra (herencia), cómo la misma llamada puede hacer cosas distintas (polimorfismo), para qué sirven las interfaces y las clases abstractas, por qué muchas veces preferimos armar con piezas en vez de heredar, cómo reconocer código desordenado, qué pasa cuando algo sale mal (excepciones), cómo escribir código que sirve para muchos tipos (genéricos), cómo se limpia la memoria, cómo leer y escribir datos y cómo otros lenguajes hacen POO sin clases. La mayoría de los ejemplos usa Java, el lenguaje del quiz. Al final estarás listo para responderlo.
El panorama: objetos hablando con objetos
Piensa en una escuela. La maestra no abre la mochila del alumno para mirar adentro. Ella pide: 'por favor, muéstrame la tarea'. El alumno decide cómo encontrarla. En la POO los objetos funcionan así. Un objeto envía una petición, llamada llamada a un método, y el otro objeto decide cómo responder.
| Idea | En una frase | Imagen cotidiana |
|---|---|---|
| Encapsulamiento | Un objeto mantiene sus datos privados y ofrece solo acciones seguras. | Una máquina expendedora: aprietas botones, no alcanzas el interior. |
| Abstracción | Mostramos lo que un objeto sabe hacer y escondemos cómo lo hace. | Manejas con volante y pedales, sin saber cómo es el motor. |
| Herencia | Una clase nueva parte de una antigua y agrega o cambia cosas. | Un cachorro es un perro, y un perro es un animal. |
| Polimorfismo | Una petición, muchas respuestas, según el objeto. | 'Haz un sonido' hace ladrar al perro y maullar al gato. |
| Composición | Un objeto se construye con otros objetos. | Una bicicleta tiene ruedas, cadena y timbre. |
Clases, objetos y encapsulamiento
Un cortador de galletas no es una galleta. Es el molde que sirve para hacer muchas galletas. Una clase es el cortador: dice qué datos (llamados campos) tiene toda cosa de este tipo y qué acciones (llamadas métodos) sabe hacer. Un objeto es una galleta hecha con ella, también llamado instancia. Dos cuentas creadas de la misma clase Account tienen los mismos métodos, pero cada una tiene su propio saldo.
class Account {
private int balance; // campo: dato de cada objeto
Account(int start) { // constructor: corre cuando nace el objeto
this.balance = start;
}
int balance() { return balance; }
}
Account a = new Account(10);
Account b = new Account(99); // independiente de aConstructores, this y static
- Un constructor prepara un objeto nuevo. Tiene el mismo nombre de la clase y ningún tipo de retorno.
- Si no escribes ningún constructor, el compilador agrega uno sin argumentos. En cuanto escribes cualquier constructor, ese gratuito desaparece.
this.xsignifica 'el campo x de este objeto'. Ayuda cuando un parámetro se llama igual que el campo.this(...)llama a otro constructor de la misma clase. Debe ser la primera línea del constructor.- Un campo
staticpertenece a la clase, no a un objeto. Hay una sola copia para todos, como el número del timbre de la escuela. Un campo normal (de instancia) existe una vez por objeto.
Orden de construcción
Cuando corre new, Java ejecuta primero el constructor de la superclase (la clase de arriba, mira herencia). Después ejecuta los inicializadores de campos y los bloques de inicialización { }, en el orden en que están escritos en el archivo. Solo al final corre el resto del cuerpo del constructor. Un campo int que copia otro campo copia el valor en ese momento y después queda desligado de él.
Niveles de acceso
| Modificador | Quién puede usar el miembro |
|---|---|
private | Solo la propia clase |
| sin modificador (package-private) | Las clases del mismo paquete (carpeta de clases) |
protected | El paquete y las subclases |
public | Todos |
Pasar objetos y compararlos
Java pasa todo por valor. Para un objeto, el valor que se copia es la referencia, como un papel con la dirección de una casa. Si un método usa la dirección para pintar la puerta, el dueño ve el color nuevo. Pero si el método escribe una dirección nueva en su propia copia del papel, el papel del dueño no cambia. En resumen: cambiar el objeto mediante un parámetro es visible para quien llamó, reasignar el parámetro no lo es.
== entre dos variables de objeto pregunta '¿es exactamente el mismo objeto?' (identidad). equals sirve para preguntar '¿tienen el mismo contenido?', pero la versión heredada de Object también compara identidad. La clase debe sobrescribir equals (y hashCode) para comparar campos. Un record de Java escribe ambos por ti.
Encapsulamiento
Encapsulamiento significa que los datos de un objeto solo se alcanzan mediante los métodos de su propia clase. Así la clase protege sus invariantes, reglas que siempre deben cumplirse, como 'el saldo nunca es negativo'. También puede cambiar cómo guarda las cosas sin romper a quienes la usan.
↑ Volver al índiceLa vida de un objeto: memoria y recolección de basura
Imagina tu escritorio y una bodega grande. Las notas pequeñas se quedan en el escritorio y se tiran cuando te vas. Las cosas más grandes van a la bodega. En Java, el escritorio es la pila (stack): las variables locales y los parámetros viven en la caja de cada llamada a un método y desaparecen cuando esta retorna. La bodega es el heap: todo objeto creado con new vive allí. Una variable como Point p guarda solo una referencia (una dirección) al objeto en el heap.
PILA (escritorio) HEAP (bodega)
┌─────────────┐
│ p ──────────┼───────► [ Point x=1 y=2 ]
└─────────────┘¿Quién tira los objetos que nadie usa? En C++ con gestión manual, escribes delete tú mismo. Si lo olvidas, hay una fuga (leak). Si borras demasiado pronto, hay un puntero colgante (dangling pointer). Java entrega ese trabajo al recolector de basura (garbage collector, GC).
¿Qué es basura?
Un objeto es basura cuando el programa no tiene forma de alcanzarlo. El GC parte de las raíces (variables locales en las pilas y campos estáticos), sigue cada referencia y marca lo que alcanza. Lo que no se marcó puede liberarse. Esto es rastreo (tracing), o marcar y barrer (mark and sweep). La alcanzabilidad es transitiva: un objeto al que solo apuntan objetos inalcanzables también es inalcanzable.
raíz a ──► [1] ──► [2] ──► [3]
después de a.next = null :
raíz a ──► [1] [2] ──► [3] (2 y 3 son basura)- Asignar
nullquita solo esa referencia. Si otro camino llega al objeto, sigue vivo. - El conteo de referencias libera un objeto cuando su contador llega a cero, pero no ve los ciclos (A apunta a B y B apunta a A). El rastreo, que usa la JVM, maneja los ciclos: si nadie alcanzable apunta al par, ambos se liberan.
- Ser elegible no significa ser recolectado ya. La máquina virtual decide cuándo.
Generaciones y fugas
La mayoría de los objetos muere joven (un resultado temporal, una variable de ciclo) y los que sobreviven tienden a vivir mucho. Esta es la hipótesis generacional. Por eso el heap se divide: los objetos nuevos nacen en una zona joven pequeña (eden), que se limpia a menudo; los sobrevivientes se promueven a la generación vieja, que se limpia rara vez.
↑ Volver al índiceHerencia: una clase que se apoya en otra
Un cachorro es un perro, y un perro es un animal. Todo lo que vale para los animales vale para los perros, y los perros agregan sus trucos. La herencia hace esto en código: class Manager extends Employee hace de Manager una subclase de Employee (la superclase). Recibe los miembros de la superclase y puede agregar o cambiar comportamiento. Como cumple cada promesa de la superclase, un Manager puede usarse donde se espera un Employee. Lo contrario es falso.
Object
│
Animal
╱ ╲
Dog Cat
│
Puppy- Java permite una sola superclase (herencia simple). Esto evita recibir dos versiones de un método por dos caminos. Para cumplir muchos contratos, una clase usa
implementscon varias interfaces. Objectes la raíz y le da a toda clasetoString,equalsyhashCode.- Un objeto de la subclase contiene los campos de toda la cadena. Un campo
privatede la superclase existe en él, pero solo los métodos de la superclase pueden tocarlo. Eso mantiene a salvo los invariantes de la superclase. finalen una variable prohíbe reasignarla, en un método prohíbe sobrescribirlo, en una clase prohíbe heredarla (comoString).
Constructores en cadena
Todo constructor empieza llamando a otro: this(...) o super(...). Si no escribes ninguno, el compilador inserta super(). Si la superclase solo tiene constructores con parámetros, esa llamada oculta no funciona y el código no compila: la subclase debe llamar super(args) explícitamente. Así la construcción sube hasta Object, y los cuerpos corren de la clase más general a la más específica.
new C() donde C extends B extends A
el cuerpo de A corre primero -> cuerpo de B -> cuerpo de CReglas de la sobrescritura
Para sobrescribir (override) un método, la subclase repite su firma con un cuerpo nuevo. Puede ofrecer más que la superclase, nunca menos: el acceso puede quedar igual o ampliarse (protected a public), el tipo de retorno puede ser un subtipo, y las excepciones verificadas no pueden ser más amplias. Así, cualquier código escrito para la superclase sigue funcionando.
Conversión hacia arriba y hacia abajo
Un upcast (Animal a = new Cat()) es implícito y siempre seguro. Un downcast ((Dog) a) hay que escribirlo y se verifica al ejecutar contra la clase real del objeto: si a guarda un Cat, lanza ClassCastException. Prueba antes con instanceof, que en Java moderno también declara la variable nueva: if (a instanceof Dog d).
Polimorfismo y despacho dinámico
Di 'haz un sonido' a un cuarto con un perro, un gato y una vaca. Cada uno responde a su manera, y no necesitaste saber qué animal era. Eso es polimorfismo, 'muchas formas'. En código escribimos contra un tipo general (una superclase o una interfaz), por ejemplo rule.apply(total), y cada objeto ejecuta su propia versión. Tipos nuevos entran sin cambiar a quien llama.
Dos tipos para una variable
En Shape s = new Circle(); la variable tiene un tipo declarado (estático), Shape, y el objeto tiene un tipo real (dinámico), Circle. El compilador usa el tipo declarado para comprobar que la llamada está permitida. La máquina virtual usa el tipo real para elegir el código. Elegir al ejecutar es el despacho dinámico (dynamic dispatch, también enlace tardío). Normalmente se hace con una tabla de métodos por clase, llamada vtable.
s.area()
tipo declarado Shape -> compilador: ¿existe area()? sí
tipo real Circle -> ejecución: usa el area() de Circle
la búsqueda empieza en la clase real y sube:
Puppy -> Dog -> Animal -> ObjectSi la subclase no sobrescribe el método, la búsqueda sube y encuentra la versión de la superclase más cercana. Los objetos de la propia superclase conservan el original. Dentro de una sobrescritura, super.draw() llama a la versión de la superclase directa de la clase donde está escrito, sin despacho dinámico. Esto permite extender en lugar de reemplazar.
Sobrecarga no es sobrescritura
| Punto | Sobrecarga (overloading) | Sobrescritura (overriding) |
|---|---|---|
| Qué es | Mismo nombre, parámetros distintos, misma clase | Misma firma, cuerpo nuevo en una subclase |
| Se elige | Al compilar, por los tipos declarados de los argumentos | Al ejecutar, por la clase real del receptor |
| Ejemplo | print(Object) y print(String) | Dog.sound() reemplaza a Animal.sound() |
Java hace despacho simple (single dispatch): al ejecutar solo cuenta la clase del objeto que recibe la llamada, nunca las clases de los argumentos. Si pasas un String guardado en una variable Object, el compilador elige print(Object) y así se queda. Cuando el comportamiento debe depender de ambos objetos, se usa el despacho doble (el patrón Visitor).
Por qué importa: acabar con las cadenas de if
Si la misma decisión por tipo (if role == DEVELOPER ... else if role == DBA ...) se repite en muchos métodos, cada cargo nuevo obliga a editarlos todos, y olvidar uno es fácil. Con polimorfismo, cada cargo es una clase que implementa una interfaz común y lleva sus propias reglas. El código viejo no cambia: abierto para la extensión, cerrado para la modificación (principio abierto-cerrado).
Ejemplo de reglas en una lista, aplicadas en orden, cada una partiendo del resultado de la anterior: un precio de 10000 centavos, luego 10% de descuento, luego 500 menos, luego 10% de descuento otra vez.
- inicio
- 10,000 centavos
- 10% off
- 9,000 centavos
- menos 500
- 8,500 centavos
- 10% off
- 7,650 centavos
Abstracción: interfaces y clases abstractas
Un enchufe de la pared no se preocupa de si conectas una lámpara o una tostadora. Solo define la forma de la clavija. Una interfaz es la clavija: lista las firmas de métodos que se pueden pedir a un objeto, y las clases que la implementan (implements) ponen el código. El código escrito contra la interfaz funciona con cualquier implementación, presente o futura.
Una clase abstracta es una receta con un paso que falta. Puede tener campos, constructores y métodos terminados, pero deja uno o más métodos sin cuerpo, marcados abstract. Como falta una parte, no se puede crear un objeto directamente de ella. La primera subclase concreta debe dar cuerpo a todo método abstracto heredado.
| Característica | Interfaz | Clase abstracta |
|---|---|---|
| Campos de instancia y constructores | No | Sí |
| Cuántas puede usar una clase | Varias (implements A, B) | Una (usa el único extends) |
| Mejor para | Un rol que pueden cumplir tipos sin parentesco | Compartir estado y código entre parientes cercanos |
Métodos default y el diamante
Antes, agregar un método a una interfaz rompía toda clase que la implementaba. Un método default trae un cuerpo que las clases heredan, y por eso forEach pudo entrar en las colecciones de Java 8 sin romper el código viejo. Pero entonces una clase puede recibir dos defaults con la misma firma. Java lo resuelve con tres reglas:
- Un método que viene de una clase siempre gana al de una interfaz (regla 'la clase gana'). Por eso un
defaultnunca cambia una clase que ya tenía ese método, y no puede redefinirtoStringniequals. - Una interfaz más específica gana a la que extiende.
- Si todavía hay empate, la clase no compila hasta que sobrescriba el método ella misma.
Template Method
La clase abstracta puede fijar el orden de los pasos en un método final que las subclases no pueden cambiar, y dejar algunos pasos abstractos o sobrescribibles. La subclase llena los huecos, y el orden general sigue garantizado por la superclase. Este es el patrón Template Method.
abstract class Game {
final void play() { // el orden es fijo
start(); turn(); end();
}
void start() { } // paso opcional
abstract void turn(); // cada juego llena este
void end() { }
}Dos principios de diseño
- Inversión de dependencias: las reglas de negocio y los detalles deben depender de abstracciones. Si
Checkouthacenew SaoPauloTax()adentro, queda pegado a esa clase. Recibe una interfazTaxpor el constructor (inyección por constructor) y podrá usar cualquier impuesto. - Segregación de interfaces: prefiere interfaces pequeñas, una por rol. Una interfaz ancha
Document(leer, escribir, borrar) obliga a una clase de solo lectura a lanzarUnsupportedOperationException, lo que rompe a quien confía en el contrato. Divídela, y deja que los métodos pidan el tipo más estrecho que necesitan (Readablepara unpreview).
Composición frente a herencia
Hay dos formas de reutilizar una clase. La herencia dice 'es un': un perro es un animal. La composición dice 'tiene un': un auto tiene un motor. Un auto no puede usarse donde se espera un motor, así que Car extends Engine está mal. La pregunta decisiva es: ¿uno puede usarse en lugar del otro? Si sí, se puede considerar la herencia. Si no, guarda el otro objeto en un campo y delega el trabajo en él.
class Car {
private final Engine engine; // tiene-un
Car(Engine engine) { this.engine = engine; }
void start() { engine.ignite(); } // delega
}Agregación y composición en UML
Ambas son relaciones todo-parte. En la agregación (rombo vacío) la parte tiene vida propia y puede pertenecer a varios todos: un equipo y sus atletas. En la composición (rombo lleno) la parte pertenece a un solo todo, que la crea y la destruye: un edificio y sus departamentos, un pedido y sus líneas. Cuál elegir depende de los requisitos del sistema.
Por qué 'prefiere composición a herencia'
- Acoplamiento más fuerte. Una subclase depende de cómo se escribió la superclase, no solo de lo que promete. Con composición solo usas la interfaz pública y hasta puedes cambiar el objeto mientras el programa corre.
- Clase base frágil. Un
CountingBagque sobrescribeaddyaddAllpuede contar los elementos dos veces si eladdAllde la superclase llama al propioadd. Ese detalle interno no está en el contrato y puede cambiar. Si el contador, en cambio, guarda una bolsa y delega, las llamadas internas nunca vuelven a él. - Heredó demasiado.
Stack extends ArrayListhereda tambiénadd(0, x)yremove, y entonces un cliente puede romper la regla último en entrar, primero en salir. Con composición la clase expone solopushypop. Eljava.util.Stackreal (que extiendeVector) tiene este defecto. - Comportamiento fijado al nacer. La herencia decide el comportamiento cuando se crea el objeto. Con composición, la parte que varía vive en otro objeto detrás de una interfaz, y poner un objeto distinto en el campo cambia el comportamiento mientras el programa corre. Esto también evita una explosión de subclases cuando hay varios ejes (envío e impuesto).
El principio de sustitución de Liskov
Un programa escrito para el tipo base debe seguir correcto con cualquier subtipo. En matemáticas todo cuadrado es un rectángulo. Pero si Rectangle tiene setWidth y setHeight, un Square mutable que cambia ambos lados rompe a quien fija 5 por 4 y espera área 20. Así que el 'es un' de la herencia se refiere al comportamiento, no a cómo se clasifican las cosas en el mundo real.
| Parte del contrato | Un subtipo puede | Ejemplo que lo rompe |
|---|---|---|
| Precondición (lo que pide) | Ser igual o más débil: aceptar al menos tanto | La base acepta cualquier valor positivo, la subclase rechaza valores mayores que 1000 |
| Poscondición (lo que garantiza) | Ser igual o más fuerte: prometer al menos tanto | La base garantiza un resultado no negativo, la subclase puede devolver uno negativo |
Acoplamiento, cohesión y code smells
Una buena caja de herramientas guarda martillos con martillos y tornillos con tornillos. Eso es cohesión: las partes de una clase trabajan hacia un solo objetivo. Una clase cohesiva tiene una única razón para cambiar (el principio de responsabilidad única), es pequeña y fácil de nombrar y probar. Una señal de baja cohesión es una clase cuyos métodos usan conjuntos distintos de campos.
Acoplamiento es cuánto depende una clase de otras, como cables entre cajas. No se puede eliminar, ya que los objetos deben trabajar juntos, pero sí se puede elegir: depende de pocas cosas, y de cosas estables como las interfaces. El objetivo clásico es alta cohesión y bajo acoplamiento.
- Acoplamiento aferente: cuántas clases dependen de mí. Si es alto, cargo mucha responsabilidad y cambiar mi interfaz pública es arriesgado.
- Acoplamiento eferente: de cuántas clases dependo yo. Si es alto, tengo muchas razones externas para cambiar.
- Una clase usada por 20 otras y que no usa ninguna es la más arriesgada para cambiar su interfaz. Apunta las dependencias hacia módulos estables.
Code smells (malos olores)
Un code smell es un patrón que sugiere código difícil de entender o de cambiar. El programa funciona y las pruebas pasan, pero el próximo cambio será lento. Refactorizar es cambiar la estructura sin cambiar el comportamiento, y la prueba es que las mismas pruebas pasan antes y después.
| Olor | Cómo se ve | Cura habitual |
|---|---|---|
| Long Method (método largo) | 40 líneas en bloques separados por comentarios | Extract Method: cada bloque pasa a ser un método con nombre |
| God Class (clase dios) | Una clase valida, calcula, envía correos y genera informes; difícil de probar | Extract Class: una por responsabilidad, la antigua coordina |
| Feature Envy (envidia de funcionalidad) | Un método usa solo getters de otra clase | Move Method a la clase dueña de los datos (tell, don't ask) |
| Shotgun Surgery (cirugía de escopeta) | Un cambio exige ediciones en muchos lugares (switches de formas de pago) | Reunir: una interfaz con una clase por forma |
| Primitive Obsession (obsesión por primitivos) | Strings e ints para correo, documento, dinero; argumentos intercambiados compilan | Tipos pequeños (objetos de valor) que validan en el constructor |
| Cadena condicional por código de tipo | La misma cadena de if por tipo en muchos métodos | Reemplazar por polimorfismo |
En la medición de este repositorio, al agregar un cuarto tipo de entrega a un checkout:
- cadenas de if
- 4 archivos
- polimorfismo
- 1 archivos
El intercambio no es gratis: con polimorfismo, un tipo nuevo es una clase nueva, pero una operación nueva sobre todos los tipos edita la interfaz y todas las clases. Elige según lo que cambie con más frecuencia.
Complejidad ciclomática
Cuenta los caminos independientes de un método, así que indica cuántas pruebas necesitas. En el quiz, empieza en 1 y suma 1 por cada `for`, `if`, ternario `?:`, `&&` y `||`. Un método con un if y un for tiene complejidad 3. Valores altos sugieren extraer métodos o usar polimorfismo.
Excepciones: cuando algo sale mal
En una carrera de relevos, si una corredora deja caer el testigo, grita y el equipo detiene su plan normal hasta que alguien sepa qué hacer. Una excepción es ese grito. Lanzarla interrumpe el flujo normal, y la máquina virtual deshace la pila de llamadas método por método, buscando un catch que coincida. Si llega a la cima sin encontrarlo, el hilo termina y se imprime el error. Esto permite que un método de bajo nivel detecte el problema y un nivel más alto, que sabe qué hacer, lo maneje, sin códigos de retorno pasados de mano en mano.
main() ── tiene catch (IOException)
└ load()
└ read() <- lanza IOException aquí
búsqueda: read -> load -> main (¡encontrado!)Verificadas y no verificadas
Todo lo que se lanza desciende de Throwable. Las excepciones verificadas (checked, por ejemplo IOException) son fallos que pueden ocurrir incluso en un programa correcto, como un archivo que no existe. El compilador te obliga a manejarlas o declararlas con throws. Las no verificadas (unchecked: RuntimeException y sus hijas, como NullPointerException) normalmente muestran un bug, y Error es un fallo grave de la máquina. El compilador no te obliga a capturarlas.
try, catch, finally
- Cuando una instrucción del
trylanza, el resto deltryse salta. El control va al primercatchque coincide, luego alfinally, y después el programa continúa tras toda la estructura. No hay vuelta a la línea que falló. - Los bloques
catchse prueban en orden y gana el primero cuyo tipo es igual a la excepción o es una superclase de ella. Por eso el más específico va primero. Poner una superclase antes de su subclase no compila, porque el segundo bloque sería código muerto. - El
finallycorre sin importar cómo salga el control deltry: fin normal,return, excepción capturada o no. Solo no corre si la máquina se detiene (por ejemploSystem.exit).
static int value() {
int x = 1;
try { x = 2; return x; } // el resultado 2 se guarda antes
finally { x = 3; } // demasiado tarde para cambiarlo
} // devuelve 2Try-with-resources
Para archivos y conexiones, escribe try (Res a = ...; Res b = ...). Los objetos que implementan AutoCloseable se cierran por ti cuando el bloque termina, con o sin excepción. Se cierran en el orden inverso al de apertura, como una pila, porque un recurso posterior puede usar uno anterior.
Traduce, pero conserva la causa
Cada capa debe hablar su propio idioma: quien llama a place(order) no necesita saber SQL, así que capturas SQLException y lanzas OrderNotSavedException. La excepción nueva debe llevar la original como causa: new OrderNotSavedException(msg, e). Esto es el encadenamiento de excepciones; el rastro impreso muestra las dos, unidas por Caused by. Sin él, el diagnóstico se pierde.
Genéricos y colecciones
Las cajas con etiqueta son mejores que las cajas misteriosas. Una List<String> es una lista con la etiqueta 'solo strings'. El compilador rechaza add(42) y sabe que get(0) devuelve un String. Sin la etiqueta, todo es Object, y cada lectura necesita una conversión (cast) que puede fallar al ejecutar. Los genéricos te dejan escribir una clase o método una vez y usarlo con muchos tipos, siempre comprobado.
Las colecciones principales
| Interfaz | Contrato | Implementaciones comunes |
|---|---|---|
List | Ordenada por posición, admite duplicados | ArrayList, LinkedList |
Set | Sin duplicados | HashSet (sin orden, rápido), LinkedHashSet (orden de inserción), TreeSet (ordenado, O(log n)) |
Queue / Deque | Fila de espera, uno o ambos extremos | ArrayDeque |
Map | Claves únicas, cada una con un valor | HashMap, TreeMap |
En un Map, put con una clave nueva agrega una entrada. Con una clave que ya existe, reemplaza el valor y devuelve el anterior, así que el tamaño cuenta claves distintas. El TreeSet ordena por el orden natural (Comparable) o por un Comparator.
Reglas de los genéricos de Java
- Solo tipos por referencia.
List<int>no compila. Usa la clase envoltorioInteger(yDouble,Boolean). El compilador convierte en ambos sentidos (autoboxing y unboxing), al costo de un objeto por elemento. - Invarianza.
Integeres subtipo deNumber, peroList<Integer>no es unaList<Number>. Si lo fuera, podrías meter un3.14en una lista de enteros. - Límites. Un
Tsin límite solo tiene los métodos deObject.T extends Comparable<T>liberacompareTo, así quemax(a, b)funciona para cualquier tipo que se compare con los de su propio tipo. - Comodines (wildcards).
? extends Tpara lo que solo lees (un productor).? super Tpara lo que solo escribes (un consumidor). Regla para recordar, PECS: producer extends, consumer super. Copiar deList<Integer>aList<Number>usacopy(List<? super T> dst, List<? extends T> src). - Borrado de tipos (type erasure). Tras la comprobación, el compilador cambia
Tpor su límite (oObject) e inserta conversiones. El código compilado no sabe qué eraT. Por esoList<String>yList<Integer>tienen la misma clase al ejecutar, y no puedes escribirnew T(),new T[10]nix instanceof List<String>.
Iteradores fail-fast
Las colecciones de java.util cuentan sus modificaciones. Si quitas elementos de una lista dentro de un ciclo for-each, el iterador lo nota y lanza ConcurrentModificationException. El nombre engaña: no hace falta un segundo hilo. Para quitar mientras recorres, usa el remove() del propio iterador o removeIf.
Flujos, entrada y salida, serialización
Un flujo (stream) es como una manguera de jardín: los datos corren en una dirección y los tomas poco a poco. Un flujo de bytes (InputStream, OutputStream) mueve números crudos de 8 bits, sin significado. Un flujo de caracteres (Reader, Writer) agrega la codificación, la regla que convierte bytes en letras. Usar el tipo equivocado con texto corrompe las letras con tilde.
Byte y carácter son lo mismo solo en ASCII simple. UTF-8 tiene longitud variable: los primeros 128 caracteres usan 1 byte, una letra acentuada como ç usa 2, el símbolo del euro 3, un emoji 4. Para contar los bytes de un texto, suma el tamaño de cada carácter (la palabra ação tiene 4 caracteres pero 6 bytes). Lee texto con un flujo de caracteres y indica la codificación, o cortarás las letras acentuadas por la mitad.
- a
- 1 bytes
- ç
- 2 bytes
- €
- 3 bytes
- emoji
- 4 bytes
Búferes
Ir al sistema operativo por cada carácter es como ir a la biblioteca por cada palabra. Un flujo con búfer (BufferedReader) trae una página entera de una vez y entrega las letras desde la memoria. Escribir es parecido, con una advertencia: los datos llegan al destino solo cuando el búfer se llena, o con flush() o close().
Serialización
Un objeto en memoria es una telaraña de referencias que solo tiene sentido dentro del programa en ejecución. Para guardarlo en un archivo o enviarlo por la red, lo serializas: conviertes su estado en una fila de bytes (binario de Java, con ObjectOutputStream) o en texto (JSON, XML). Una clase Java lo acepta implementando la interfaz marcadora Serializable.
transientdeja un campo fuera: úsalo para contraseñas, cachés, conexiones. Cuando el objeto se lee de vuelta, se reconstruye sin llamar al constructor de la clase serializable, y los campos transient vuelven comonull,0ofalse.- El flujo también guarda el nombre de la clase y un número de versión. Al leer, lo compara con la clase cargada. Si cambiaste la clase y no declaraste
serialVersionUID, el número calculado cambia y leer archivos viejos lanzaInvalidClassException. Declararprivate static final long serialVersionUIDpermite que la clase evolucione de forma compatible.
POO entre lenguajes: traits, interfaces, protocolos
No todos los lenguajes tienen clases y herencia como Java. Pero todos necesitan formas de agrupar datos y comportamiento y de dejar que cosas distintas respondan a la misma petición. Mira cómo lo hacen algunos.
| Lenguaje | Cómo hace polimorfismo | Reutilización |
|---|---|---|
| Java, C# | Interfaces nominales: la clase debe declarar implements | Herencia y composición |
| TypeScript | Interfaces estructurales: basta con tener los miembros | Clases y composición |
| Go | Interfaces estructurales, sin clases ni herencia | Composición |
| Rust | Traits sobre structs y enums, estáticos (T: Display) o dinámicos (dyn Trait) | Sin herencia de implementación |
| Elixir | Protocolos (defprotocol) implementados por tipo de dato | Funciones y módulos, sin clases |
Nominal frente a estructural
En un sistema nominal (Java), un valor pertenece a un tipo porque su clase lo dice por nombre. En un sistema estructural (TypeScript, Go), basta con tener los miembros necesarios. Un Book con price sirve como Priced en TypeScript sin decir implements, y un objeto simple { price: 5 } también sirve, lo cual es útil para dobles de prueba. La misma clase sin implements se rechaza en Java.
interface Priced { price: number }
class Book { constructor(public price: number) {} }
const items: Priced[] = [new Book(10), { price: 5 }]; // ambos se aceptanEl tipado nominal registra la intención y evita coincidencias (dos métodos run con significados distintos). El estructural da flexibilidad y no necesita adaptadores para tipos que no son tuyos. En Go, una interfaz pequeña puede definirse del lado de quien consume, y un tipo que ya existe, incluso de otra biblioteca, la cumple sin cambiar.
El problema de la expresión
Los objetos agrupan el código por variante (una clase por tipo). Agregar un tipo es fácil, agregar una operación exige editar todas las clases. Las funciones con coincidencia de patrones agrupan el código por operación. Agregar una operación es fácil, agregar un tipo exige editar todas las funciones. Este repositorio lo midió: agregar 'lleva y paga' a un carrito fue un archivo nuevo con objetos, y cuatro ediciones dentro de un archivo con funciones. El cambio opuesto tiene el costo opuesto.
↑ Volver al índiceJuntando todo
Todas las herramientas de la POO responden a una pregunta: ¿dónde vive cada pedazo de conocimiento y cómo se propaga un cambio? Usa esta tabla cuando encuentres una pregunta del quiz.
| Si el problema es... | Piensa en... | Porque... |
|---|---|---|
| Gente de afuera rompe mis reglas | Encapsulamiento, copia defensiva | Las reglas viven junto a los datos |
El mismo if por tipo por todas partes | Polimorfismo | Cada tipo lleva sus propias reglas |
| Es difícil cambiar una dependencia | Interfaz e inyección por constructor | Depende de abstracciones |
| La subclase se rompe cuando cambia el padre | Composición | La herencia es el acoplamiento más fuerte |
| Leer archivos binarios viejos falla | serialVersionUID | El formato está acoplado a la clase |
| La memoria crece sin parar con un GC | Alcanzabilidad, colecciones estáticas | El GC libera solo lo inalcanzable |
| ¿Qué método corre? | Tipo declarado en las sobrecargas, tipo real en las sobrescrituras | Son dos momentos distintos |
Errores comunes
- Creer que
privatebasta mientras un getter devuelve la lista interna. - Confundir sobrecarga (compilación) con sobrescritura (ejecución), y olvidar que los métodos y campos estáticos no se sobrescriben.
- Usar herencia solo para reutilizar código, o porque 'un cuadrado es un rectángulo'.
- Llamar a un método sobrescribible desde un constructor.
- Capturar la excepción de la superclase antes que la de la subclase, o lanzar una excepción nueva sin la causa.
- Creer que
List<Integer>es unaList<Number>, o que el tipoTtodavía existe al ejecutar. - Confiar en el recolector de basura para cerrar archivos.