Класове в класове
Проблем
Клас, който има смисъл само на едно място
В урока за свързания списък Node беше отделен клас. Всеки в програмата може да прави възли, а те имат смисъл само в списъка. Същото е с реда от поръчката:
class OrderLine { String product; int quantity; double price;}
class Order { List<OrderLine> lines = new ArrayList<>();}OrderLine без Order е безсмислен. Името е дълго, за да не се сблъска с реда от фактурата, InvoiceLine, или с реда от склада. А и нищо не казва, че двата класа вървят заедно. Може ли редът да живее вътре в поръчката?
Идеята
Статичен вложен клас
Може. Клас, обявен вътре в друг клас, се казва вложен клас (nested class):
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("Кафе на зърна", 2, 12.5));order.add(new Order.Line("Чаша", 4, 3.0));System.out.println("Общо: " + order.total() + " €");Общо: 37.0 €- Отвън името е
Order.Line. Вътре вOrderе самоLine. staticзначи, че редът не е вързан за конкретна поръчка.new Order.Line(...)се прави без обектOrder, като обикновен клас.- Ако е
private static class Line, никой извънOrderне може да го ползва.
Вече срещна такъв клас: Map.Entry от урока за HashMap е вложен в Map. А LinkedList в Java пази възлите си в private static class Node.
Идеята
Вътрешен клас: вижда обекта отвън
Количката има отстъпка за всички продукти в нея. Всеки продукт трябва да знае отстъпката на своята количка. Тогава класът е без 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("Слушалки", 80);cart.add("Калъф", 20);cart.print();Слушалки: 72.0 €Калъф: 18.0 €static се казва вътрешен клас (inner class). Всеки Item е създаден от конкретна количка и вижда нейните полета, дори private. finalPrice чете discountPercent на количката, в която е.Cart cart = new Cart(10);cart.add("Слушалки", 80);cart.add("Калъф", 20);cart.print();Стъпка по стъпка, с думи
- Количката е обект Cart с отстъпка 10%.
- add създава Item. Освен name и price, всеки Item пази скрита стрелка към количката, която го е създала: Cart.this.
- Вторият Item сочи към същата количка.
- finalPrice на „Слушалки“ минава по скритата стрелка и чете discountPercent: 80 * (100 - 10) / 100 = 72.0.
- finalPrice на „Калъф“ минава по скритата стрелка и чете discountPercent: 20 * (100 - 10) / 100 = 18.0.
По-точно, за любопитните
- Отвън вътрешен обект се създава през обекта на външния клас:
cart.new Item(...). Синтаксисът е рядък, защото обикновено външният клас сам създава вътрешните, катоaddтук:
Cart cart = new Cart(10);Cart.Item cable = cart.new Item("Кабел", 10);System.out.println(cable.finalPrice());9.0- Всеки вътрешен обект пази скрита референция към външния. Ако полетата на двата класа се казват еднакво, външното се пише
Cart.this.discountPercent. - Ако вложеният клас не ползва полетата на външния, направи го
static. Така не пази излишна референция.
Пробвай
Две колички
Класът Cart е същият. Едната количка е без отстъпка, а другата е с 20% за членове на клуба.
Какво ще отпечата програмата?
Cart regular = new Cart(0);Cart member = new Cart(20);regular.add("Книга", 15);member.add("Книга", 15);regular.print();member.print();Всеки Item вижда отстъпката на количката, която го е създала. Книгата в regular е с 0%, а тази в member е с 20%.
Пробвай
Намери грешката
Main.java:9: error: non-static variable currency cannot be referenced from a static context return product + ": " + price + " " + currency; ^На кой ред е проблемът?
Избери реда с грешката.
Line е static, затова не е вързан за поръчка. Няма поръчка, чиято валута да прочете. Поправката е да махнеш static. Тогава Line става вътрешен клас и вижда currency на своята поръчка. Друг начин е currency да стане static, ако валутата е една за всички поръчки.Идеята
Клас в метод
Клас може да се обяви и в метод. Тогава се вижда само там и се казва локален клас:
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("Най-продаван: " + best.name + " (" + best.count + " бр.)");}String[] names = { "Кафе", "Чай", "Вода" };int[] sold = { 120, 45, 300 };printBestSeller(names, sold);Най-продаван: Вода (300 бр.)Sale държи заедно името и броя. Извън printBestSeller никой не го ползва, затова е там. Такива класове се срещат рядко, но е добре да ги познаваш.
Идеята
Клас без име
Отстъпките в магазина са интерфейс:
interface Discount { double apply(double price);}Досега за всяка отстъпка правеше клас: class BlackFriday implements Discount { ... }. Ако отстъпката се ползва само на едно място, класът може да се напише точно там, без име:
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() { ... } прави две неща. Обявява клас без име, който изпълнява Discount, и създава един обект от него. Такъв клас се казва анонимен клас. Вътре е като в обикновен клас: @Override и методите на интерфейса.
По-точно, за любопитните
- Анонимният клас може и да наследи клас, не само да изпълни интерфейс:
new Product("Кафе", 12.5) { ... }вика конструктора наProductи презаписва методи. - Анонимният клас може да чете локалните променливи на метода, в който е написан, но само ако те не се променят след това.
- Интерфейс с един-единствен метод, като
Discount, в модул „Модерна Java“ ще се записва много по-кратко. Анонимният клас е стъпалото към това.
Идеята
Comparator: подреждане по свое правило
Collections.sort подреди текст и числа. Ученици обаче не може да подреди:
Collections.sort(students);Компилаторът казва no suitable method found for sort(List<Student>). Той не знае по какво да подреди ученици: по име, по успех, по възраст?
Правилото се дава отделно, като Comparator. Това е интерфейс с един метод, compare(a, b). Той връща отрицателно число, ако a трябва да е преди b, нула, ако са равни, и положително, ако a е след b. Удобно е да е анонимен клас:
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);[Мария 6.0, Иван 5.5, Ана 5.5, Петър 4.75]Double.compare(b.grade, a.grade)сравнява наобратно, затова най-високият успех е пръв.- Иван и Ана са с еднакъв успех и остават в реда, в който бяха.
Collections.sortне разменя равни елементи. - За текст и числа в обратен ред има готов
Comparator:Collections.reverseOrder().
List<String> cities = new ArrayList<>();cities.add("Варна");cities.add("София");cities.add("Пловдив");Collections.sort(cities, Collections.reverseOrder());System.out.println(cities);[София, Пловдив, Варна]По-точно, за любопитните
Comparatorе вjava.util, с типов параметър:Comparator<Student>сравнява ученици.- Текстът и числата се подреждат без
Comparator, защотоStringиIntegerизпълняват интерфейсаComparableс методcompareTo. И твоят клас може да го изпълни, ако има един естествен ред.Comparatorе за случаите, в които редовете са няколко. Double.compareиInteger.compareпазят от грешки. Сa.age - b.ageприintизглежда по-кратко, но при много големи числа изваждането препълва и знакът се обръща.
Пробвай
Кое е първо?
Какво ще отпечата програмата?
List<Product> products = new ArrayList<>();products.add(new Product("Монитор", 220));products.add(new Product("Мишка", 25));products.add(new Product("Клавиатура", 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) е отрицателно, когато a е по-евтин. Затова по-евтиният е преди: 25, 60, 220. За най-скъпото отпред размени a и b.Упражнение
Класове, които вървят заедно
Реши задачите в редактора си.
1. Количка за пазаруване. Клас ShoppingCart със статичен вложен клас Item (име и цена) и методи addItem(Item item), getTotal() и count(). В main добави new ShoppingCart.Item("Мляко", 2.5), хляб за 1,20 € и кафе за 7,50 €.
Продукти: 3Общо: 11.2 €2. Монитор на уеб сървър. Клас WebServer със статичен вложен клас Request (адрес и статус код) и методи addRequest(Request r) и printStats(). Статистиката казва колко са заявките, колко са от всеки статус код (подредени) и какъв процент са успешни, тоест с код от 200 до 299. Заявките са /home 200, /cart 200, /login 401, /home 200, /api/orders 500.
Заявки: 5200: 3401: 1500: 1Успешни: 60%3. Сграден мениджър. Клас Building със статичен вложен клас Apartment (номер, брой стаи, собственик) и методи addApartment(Apartment a) и getApartmentInfo(int number). Пази апартаментите в Map<Integer, Apartment>. Добави 101 с 3 стаи на Иванова и 102 с 2 стаи на Петров. Потърси 101 и 999.
Апартамент 101: 3 стаи, собственик ИвановаНяма апартамент 999.4. Класиране. Подреди учениците от урока по успех, от най-високия надолу. При равен успех ги подреди по име, по азбучен ред. Всичко е в един Comparator: ако сравнението по успех не е 0, върни него, иначе сравни имената с compareTo.
Мария 6.0Ана 5.5Иван 5.5Петър 4.75Още задачи
- Конфигуратор на компютър. Клас
Computerсъс статични вложени класовеCPU(марка, честота в GHz),RAM(GB) иStorage(GB) и методиsetCPU,setRAM,setStorage.toStringописва конфигурацията:Intel 3.5 GHz, 16 GB RAM, 512 GB. - Организатор на пътуване, отново. Направи задачата от урока за
HashMapс вложен класTripPlanner.Destination, който пази името и списъка от дейности. - Мрежа от контакти, отново. Премести
Contactот задачата в урока заHashMapвътре вContactNetwork. Трябва ли да еstatic? - Смятане наум, отново. Премести
Questionот задачата в урока за списъците вътре вMathGame. - Продукти по два начина. Подреди списък с продукти по цена от най-скъпия, а при еднаква цена по име. После подреди същия списък само по име в обратен ред.
- Своя отстъпка. Напиши три анонимни класа
Discount: 10% над 100 €, фиксирани 5 € отстъпка и „втори продукт на половин цена“. Приложи ги върху една и съща количка.
Тест