Clases anidadas
En resumen: Una clase anidada es una clase declarada dentro de otra porque solo tiene sentido ahí; Java tiene clases anidadas estáticas e internas, locales y anónimas.
Problema
Una clase que solo tiene sentido en un lugar
En la lección sobre la lista enlazada, Node era una clase aparte. Cualquiera en el programa puede crear nodos, pero solo tienen sentido en la lista. Lo mismo pasa con una línea de pedido:
class OrderLine { String product; int quantity; double price;}
class Order { List<OrderLine> lines = new ArrayList<>();}OrderLine sin Order no tiene sentido. El nombre es largo para que no choque con la línea de una factura, InvoiceLine, ni con la línea de un almacén. Y nada dice que las dos clases van juntas. ¿Puede la línea vivir dentro del pedido?
La idea
Clase anidada estática
Puede. Una clase declarada dentro de otra clase se llama clase anidada:
class Order { static class Line { String product; int quantity; double price;
Line(String product, int quantity, double price) { this.product = product; this.quantity = quantity; this.price = price; }
double total() { return quantity * price; } }
private List<Line> lines = new ArrayList<>();
void add(Line line) { lines.add(line); }
double total() { double sum = 0; for (Line line : lines) { sum += line.total(); } return sum; }}Order order = new Order();order.add(new Order.Line("Café en grano", 2, 12.5));order.add(new Order.Line("Taza", 4, 3.0));System.out.println("Total: " + order.total() + " €");Total: 37.0 €- Desde fuera, el nombre es
Order.Line. Dentro deOrder, es soloLine. staticsignifica que la línea no está ligada a un pedido concreto.new Order.Line(...)se crea sin un objetoOrder, como una clase normal.- Si es
private static class Line, nadie fuera deOrderpuede usarla.
Ya conoces una clase así: Map.Entry de la lección sobre HashMap está anidada en Map. Y LinkedList en Java guarda sus nodos en una private static class Node.
La idea
Clase interna: ve al objeto externo
El carrito tiene un descuento para todos los productos que hay en él. Cada producto tiene que conocer el descuento de su propio carrito. Entonces la clase no lleva static:
class Cart { private int discountPercent; private List<Item> items = new ArrayList<>();
Cart(int discountPercent) { this.discountPercent = discountPercent; }
class Item { String name; double price;
Item(String name, double price) { this.name = name; this.price = price; }
double finalPrice() { return price * (100 - discountPercent) / 100; } }
void add(String name, double price) { items.add(new Item(name, price)); }
void print() { for (Item item : items) { System.out.println(item.name + ": " + item.finalPrice() + " €"); } }}Cart cart = new Cart(10);cart.add("Auriculares", 80);cart.add("Funda", 20);cart.print();Auriculares: 72.0 €Funda: 18.0 €static se llama clase interna. Cada Item lo crea un carrito concreto y ve sus campos, incluso los private. finalPrice lee el discountPercent del carrito en el que está.Cart cart = new Cart(10);cart.add("Auriculares", 80);cart.add("Funda", 20);cart.print();Paso a paso, en palabras
- El carrito es un objeto Cart con un descuento del 10 %.
- add crea un Item. Además de name y price, cada Item guarda una flecha oculta al carrito que lo creó: Cart.this.
- El segundo Item apunta al mismo carrito.
- finalPrice de «Auriculares» sigue la flecha oculta y lee discountPercent: 80 * (100 - 10) / 100 = 72.0.
- finalPrice de «Funda» sigue la flecha oculta y lee discountPercent: 20 * (100 - 10) / 100 = 18.0.
Más precisamente, para curiosos
- Desde fuera, un objeto interno se crea a través del objeto de la clase externa:
cart.new Item(...). Esta sintaxis es rara, porque normalmente la clase externa crea las internas ella misma, comoaddaquí:
Cart cart = new Cart(10);Cart.Item cable = cart.new Item("Cable", 10);System.out.println(cable.finalPrice());9.0- Cada objeto interno guarda una referencia oculta al externo. Si los campos de las dos clases tienen el mismo nombre, el externo se escribe
Cart.this.discountPercent. - Si la clase anidada no usa los campos de la clase externa, hazla
static. Así no guarda una referencia que no necesita.
Pruébalo
Dos carritos
La clase Cart es la misma. Un carrito no tiene descuento, y el otro tiene un 20 % para los socios del club.
¿Qué imprimirá el programa?
Cart regular = new Cart(0);Cart member = new Cart(20);regular.add("Libro", 15);member.add("Libro", 15);regular.print();member.print();Cada Item ve el descuento del carrito que lo creó. El libro de regular tiene un 0 %, y el de member, un 20 %.
Pruébalo
Encuentra el error
Main.java:9: error: non-static variable currency cannot be referenced from a static context return product + ": " + price + " " + currency; ^¿En qué línea está el problema?
Elige la línea con el error.
Line es static, así que no está ligada a un pedido. No hay ningún pedido cuya moneda pueda leer. La corrección es quitar static. Entonces Line pasa a ser una clase interna y ve el currency de su propio pedido. Otra forma es hacer currency static, si la moneda es la misma para todos los pedidos.La idea
Una clase dentro de un método
Una clase también se puede declarar dentro de un método. Entonces solo se ve ahí y se llama clase local:
static void printBestSeller(String[] names, int[] sold) { class Sale { String name; int count;
Sale(String name, int count) { this.name = name; this.count = count; } }
Sale best = new Sale(names[0], sold[0]); for (int i = 1; i < names.length; i++) { if (sold[i] > best.count) { best = new Sale(names[i], sold[i]); } } System.out.println("Más vendido: " + best.name + " (" + best.count + " vendidos)");}String[] names = { "Café", "Té", "Agua" };int[] sold = { 120, 45, 300 };printBestSeller(names, sold);Más vendido: Agua (300 vendidos)Sale mantiene juntos el nombre y la cantidad. Nadie la usa fuera de printBestSeller, así que vive ahí. Estas clases son raras, pero conviene conocerlas.
La idea
Una clase sin nombre
Los descuentos de la tienda son una interfaz:
interface Discount { double apply(double price);}Hasta ahora creabas una clase para cada descuento: class BlackFriday implements Discount { ... }. Si el descuento se usa en un solo lugar, la clase se puede escribir justo ahí, sin nombre:
Discount blackFriday = new Discount() { @Override public double apply(double price) { return price * 70 / 100; }};Discount none = new Discount() { @Override public double apply(double price) { return price; }};System.out.println(blackFriday.apply(100));System.out.println(none.apply(100));70.0100.0new Discount() { ... } hace dos cosas. Declara una clase sin nombre que implementa Discount, y crea un objeto de ella. Una clase así se llama clase anónima. Por dentro es como una clase normal: @Override y los métodos de la interfaz.
Más precisamente, para curiosos
- Una clase anónima también puede extender una clase, no solo implementar una interfaz:
new Product("Café", 12.5) { ... }llama al constructor deProducty sobrescribe métodos. - Una clase anónima puede leer las variables locales del método en el que está escrita, pero solo si no cambian después.
- Una interfaz con un solo método, como
Discount, se escribirá mucho más corto en el módulo «Java moderno». La clase anónima es el paso hacia eso.
La idea
Comparator: ordenar con tu propia regla
Collections.sort ordenaba texto y números. Pero no puede ordenar alumnos:
Collections.sort(students);El compilador dice no suitable method found for sort(List<Student>). No sabe por qué ordenar a los alumnos: ¿por nombre, por nota, por edad?
La regla se da aparte, como un Comparator. Es una interfaz con un método, compare(a, b). Devuelve un número negativo si a debe ir antes que b, cero si son iguales y un número positivo si a va después de b. Es práctico como clase anónima:
class Student { String name; double grade;
Student(String name, double grade) { this.name = name; this.grade = grade; }
@Override public String toString() { return name + " " + grade; }}Collections.sort(students, new Comparator<Student>() { @Override public int compare(Student a, Student b) { return Double.compare(b.grade, a.grade); }});System.out.println(students);[María 6.0, Juan 5.5, Ana 5.5, Pedro 4.75]Double.compare(b.grade, a.grade)compara al revés, así que la nota más alta va primero.- Juan y Ana tienen la misma nota y se quedan en el orden en que estaban.
Collections.sortno intercambia elementos iguales. - Para texto y números en orden inverso hay un
Comparatorlisto:Collections.reverseOrder().
List<String> cities = new ArrayList<>();cities.add("Sevilla");cities.add("Valencia");cities.add("Bilbao");Collections.sort(cities, Collections.reverseOrder());System.out.println(cities);[Valencia, Sevilla, Bilbao]Más precisamente, para curiosos
Comparatorestá enjava.util, con un parámetro de tipo:Comparator<Student>compara alumnos.- El texto y los números se ordenan sin
Comparator, porqueStringeIntegerimplementan la interfazComparablecon un métodocompareTo. Tu clase también puede implementarla, si tiene un orden natural.Comparatores para los casos en que hay varios órdenes. Double.compareeInteger.comparete protegen de errores. Conint,a.age - b.ageparece más corto, pero con números muy grandes la resta se desborda y el signo cambia.
Pruébalo
¿Cuál va primero?
¿Qué imprimirá el programa?
List<Product> products = new ArrayList<>();products.add(new Product("Monitor", 220));products.add(new Product("Ratón", 25));products.add(new Product("Teclado", 60));
Collections.sort(products, new Comparator<Product>() { @Override public int compare(Product a, Product b) { return Double.compare(a.price, b.price); }});for (Product p : products) { System.out.println(p.name);}Double.compare(a.price, b.price) es negativo cuando a es más barato. Así que el más barato va primero: 25, 60, 220. Para poner primero el más caro, cambia a por b.Ejercicio
Clases que van juntas
Resuelve las tareas en tu editor.
1. Carrito de compras. Una clase ShoppingCart con una clase anidada estática Item (nombre y precio) y los métodos addItem(Item item), getTotal() y count(). En main, agrega new ShoppingCart.Item("Leche", 2.5), pan por 1,20 € y café por 7,50 €.
Productos: 3Total: 11.2 €2. Monitor de un servidor web. Una clase WebServer con una clase anidada estática Request (dirección y código de estado) y los métodos addRequest(Request r) y printStats(). Las estadísticas dicen cuántas solicitudes hay, cuántas hay de cada código de estado (ordenados) y qué porcentaje son correctas, es decir, con un código del 200 al 299. Las solicitudes son /home 200, /cart 200, /login 401, /home 200, /api/orders 500.
Solicitudes: 5200: 3401: 1500: 1Correctas: 60%3. Administrador de un edificio. Una clase Building con una clase anidada estática Apartment (número, número de habitaciones, propietario) y los métodos addApartment(Apartment a) y getApartmentInfo(int number). Guarda los apartamentos en un Map<Integer, Apartment>. Agrega el 101, de 3 habitaciones, de Fernández, y el 102, de 2 habitaciones, de Pérez. Busca el 101 y el 999.
Apartamento 101: 3 habitaciones, propietario: FernándezNo existe el apartamento 999.4. Clasificación. Ordena a los alumnos de la lección por nota, de la más alta a la más baja. Cuando las notas son iguales, ordénalos por nombre, en orden alfabético. Todo va en un solo Comparator: si la comparación por nota no es 0, devuélvela; si no, compara los nombres con compareTo.
María 6.0Ana 5.5Juan 5.5Pedro 4.75Más tareas
- Configurador de computadoras. Una clase
Computercon las clases anidadas estáticasCPU(marca, velocidad en GHz),RAM(GB) yStorage(GB) y los métodossetCPU,setRAM,setStorage.toStringdescribe la configuración:Intel 3.5 GHz, 16 GB RAM, 512 GB. - El planificador de viajes, otra vez. Haz la tarea de la lección sobre
HashMapcon una clase anidadaTripPlanner.Destinationque guarde el nombre y la lista de actividades. - La red de contactos, otra vez. Mueve
Contactde la tarea de la lección sobreHashMapdentro deContactNetwork. ¿Debería serstatic? - Cálculo mental, otra vez. Mueve
Questionde la tarea de la lección sobre listas dentro deMathGame. - Productos de dos formas. Ordena una lista de productos por precio, del más caro, y por nombre cuando el precio es el mismo. Después ordena la misma lista solo por nombre, en orden inverso.
- Tu propio descuento. Escribe tres clases anónimas
Discount: un 10 % a partir de 100 €, 5 € fijos de descuento y «el segundo producto a mitad de precio». Aplícalas a un mismo carrito.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Cadenas · Listas · Pila, cola y lista enlazada · HashMap y HashSet
- Continúa en: Lambdas · Streams · Patrones de diseño · Tests con JUnit
- Ejercicios sobre el tema: Colecciones