Към съдържанието
wannacode

Класове в класове

Проблем

Клас, който има смисъл само на едно място

В урока за свързания списък 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();
Стъпка по стъпка, с думи
  1. Количката е обект Cart с отстъпка 10%.
  2. add създава Item. Освен name и price, всеки Item пази скрита стрелка към количката, която го е създала: Cart.this.
  3. Вторият Item сочи към същата количка.
  4. finalPrice на „Слушалки“ минава по скритата стрелка и чете discountPercent: 80 * (100 - 10) / 100 = 72.0.
  5. 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();
Отговор

Пробвай

Намери грешката

Редът трябва да покаже валутата на поръчката. Програмата не се компилира:
Грешка при компилиране
Main.java:9: error: non-static variable currency cannot be referenced from a static context
return product + ": " + price + " " + currency;
^

На кой ред е проблемът?

Избери реда с грешката.

Идеята

Клас в метод

Клас може да се обяви и в метод. Тогава се вижда само там и се казва локален клас:

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.0
100.0

new 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);
}
Отговор

Упражнение

Класове, които вървят заедно

Реши задачите в редактора си.

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.

Изход
Заявки: 5
200: 3
401: 1
500: 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 € отстъпка и „втори продукт на половин цена“. Приложи ги върху една и съща количка.

Тест

Провери се

1. Каква е разликата между static class Line и class Item вътре в друг клас?
2. Защо static вложен клас не вижда обикновените полета на външния?
3. Какво създава new Discount() { … }?
4. compare(a, b) връща отрицателно число. Какво значи това за Collections.sort?
5. Кое подрежда учениците по успех от най-високия надолу?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·