record, switch и шаблони
Проблем
Мястото е заето, но HashMap не го намира
Резервации в кино: ключът е мястото (ред и номер), стойността е името на човека.
class Seat { private int row; private int number;
public Seat(int row, int number) { this.row = row; this.number = number; }}Map<Seat, String> reservations = new HashMap<>();reservations.put(new Seat(5, 12), "Мария");
System.out.println(reservations.get(new Seat(5, 12)));System.out.println(reservations.containsKey(new Seat(5, 12)));nullfalseget връща null. Помниш защо от урока за хеширането: new Seat(5, 12) два пъти са два различни обекта, а Seat не казва кога са равни. Поправката е equals и hashCode, а за отпечатване и toString, плюс getter-и. Около 30 реда за клас, който само носи две числа.Идеята
record: клас за данни
record Seat(int row, int number) {}Map<Seat, String> reservations = new HashMap<>();reservations.put(new Seat(5, 12), "Мария");
System.out.println(reservations.get(new Seat(5, 12)));System.out.println(reservations.containsKey(new Seat(5, 12)));
Seat seat = new Seat(5, 12);System.out.println(seat);System.out.println("Ред " + seat.row() + ", място " + seat.number());МарияtrueSeat[row=5, number=12]Ред 5, място 12record е клас, чиято работа е да носи данни. В скобите са компонентите му, а Java пише вместо теб:
- частно
finalполе за всеки компонент; - конструктор с всички компоненти;
- метод за четене на всяко поле, със същото име:
row(), а неgetRow(); equalsиhashCode, които сравняват полетата. Затоваgetвече намира Мария;toStringвъв видаSeat[row=5, number=12].
Setter-и няма. Обект от record не се променя след създаването си.
Map<Seat, String> reservations = new HashMap<>();reservations.put(new Seat(5, 12), "Мария");
System.out.println(reservations.get(new Seat(5, 12)));System.out.println(reservations.containsKey(new Seat(5, 12)));
Seat seat = new Seat(5, 12);System.out.println(seat);System.out.println("Ред " + seat.row() + ", място " + seat.number());Стъпка по стъпка, с думи
- Празен речник: ключът е място в киното, стойността е име.
- put слага ключ Seat(5, 12) със стойност „Мария“.
- get получава нов обект Seat. Това е друг обект в паметта, не ключът от put.
- hashCode на record се смята от полетата, затова е еднакъв: търсенето отива на същото място.
- equals на record сравнява поле по поле: ред 5 и 5, място 12 и 12. Равни са, и get връща „Мария“.
- containsKey прави същото с още един нов Seat: true.
- toString и методите row() и number() record-ът също пише сам.
По-точно, за любопитните
recordеfinal: не може да се наследи. Неявно наследяваjava.lang.Recordи не може да наследи друг клас, но може да имплементира интерфейси.- Може да има свои методи, статични полета и статични методи. Полета на обекта извън компонентите не може да има:
record Cart(List<String> items) { private int count;}Main.java:4: error: field declaration must be static private int count; ^ (consider replacing field with record component)- Неизменността е само на първо ниво. Ако компонентът е
List, полето винаги сочи същия списък, но самият списък може да се променя. - В модул „Колекции“ правеше
equalsиhashCodeна ръка. Сrecordтова вече го има. Ако ти трябва друго правило за равенство, пак можеш да ги презапишеш.
Идеята
Проверка и методи в record
record Price(double amount, String currency) { Price { if (amount < 0) { throw new IllegalArgumentException("Цената не може да е отрицателна: " + amount); } }
Price withDiscount(double percent) { return new Price(amount * (1 - percent / 100), currency); }}Price coffee = new Price(18.50, "EUR");Price sale = coffee.withDiscount(20);System.out.println(coffee);System.out.println(sale);
try { new Price(-5, "EUR");} catch (IllegalArgumentException e) { System.out.println(e.getMessage());}Price[amount=18.5, currency=EUR]Price[amount=14.8, currency=EUR]Цената не може да е отрицателна: -5.0- В компактния конструктор
amountиcurrencyса параметрите. Не пишешthis.amount = amount: това става само след проверката. withDiscountне променя цената. Връща новаPrice. Така се работи с обекти, които не се променят:coffeeси остава 18,50 €.
Пробвай
Равни ли са?
Seat е record, както в картата преди тази.Какво ще отпечата програмата?
Seat a = new Seat(5, 12);Seat b = new Seat(5, 12);System.out.println(a == b);System.out.println(a.equals(b));System.out.println(a.hashCode() == b.hashCode());a и b са два обекта, затова == е false: той сравнява стрелките. equals на record сравнява полетата и те са еднакви. hashCode също е от полетата, затова е еднакъв. Точно това иска HashMap.Пробвай
Доставката излиза скъпа
Доставката до офис е 4,50 €, а програмата казва 6,90 €. На кой ред е грешката?
Избери реда с грешката.
price = 4.50; липсва break. Старият switch не спира в края на случая, а пропада в следващия: изпълнява и price = 6.90; и чак тогава стига до break. Поправката е break след ред 5. А още по-добре е switch, в който пропадане няма. Това е следващата карта.Идеята
switch, който връща стойност
switch може да е израз: дава стойност, която записваш в променлива или връщаш.String method = "офис";double price = switch (method) { case "офис" -> 4.50; case "куриер", "до адрес" -> 6.90; case "магазин" -> 0; default -> throw new IllegalArgumentException("Непознат начин на доставка: " + method);};System.out.println("Доставка: " + price + " €");Доставка: 4.5 €- След
->стои стойността на случая. Пропадане няма,breakне се пише. - Един случай може да има няколко стойности:
case "куриер", "до адрес". - В случая може да хвърлиш изключение, както в
default. - Изразът трябва да даде стойност за всеки възможен вход. При текст входовете са безброй, затова
defaultе задължителен. Без него:
double price = switch (method) { case "офис" -> 4.50; case "куриер" -> 6.90;};Main.java:4: error: the switch expression does not cover all possible input values double price = switch (method) { ^Ако случаят има няколко реда, той е в { }, а стойността се дава с yield:
static double delivery(String method, double weightKg) { return switch (method) { case "офис" -> 4.50; case "куриер" -> { double base = 6.90; yield weightKg > 10 ? base + 3 : base; } default -> 0; };}System.out.println(delivery("куриер", 4));System.out.println(delivery("куриер", 12));6.99.9По-точно, за любопитните
- Стрелки може да пишеш и в
switch, който не връща стойност, а само прави нещо. Тогава също няма пропадане. yieldе катоreturn, но заswitch, а не за метода.- Старият запис
case "офис": ... break;още работи и ще го срещаш в стар код. Двата вида не се смесват в единswitch.
Идеята
sealed: затворено семейство
Магазинът приема три начина на плащане: карта, банков превод и наложен платеж. Друг няма. Това може да се каже на компилатора:
sealed interface Payment permits CardPayment, BankTransfer, CashOnDelivery {}
record CardPayment(String cardNumber) implements Payment {}record BankTransfer(String iban) implements Payment {}record CashOnDelivery() implements Payment {}sealed значи „запечатан“. След permits е списъкът с типовете, които имат право да имплементират Payment. Опит за четвърти не се компилира:
record Voucher(String code) implements Payment {}Main.java:6: error: class is not allowed to extend sealed class: Payment (as it is not listed in its 'permits' clause)record Voucher(String code) implements Payment {}^Наследниците тук са record, защото всеки начин на плащане е само данни: номер на карта, IBAN или нищо.
По-точно, за любопитните
- Всеки наследник на
sealedтип еfinal,sealed(и той сpermits) илиnon-sealed(отворен отново).recordеfinalпо правило. - Ако всички наследници са в същия файл,
permitsможе да се пропусне. sealedможе да е и абстрактен клас:sealed abstract class Shape permits Circle, Square.
Виж го
Шаблони в switch
switch проверява типа на обекта:static double fee(Payment payment, double amount) { return switch (payment) { case CardPayment card -> amount * 0.015; case BankTransfer transfer -> 0.50; case CashOnDelivery cash -> 2.00; };}System.out.println(fee(new CardPayment("4111222233334444"), 200));System.out.println(fee(new BankTransfer("BG80BNBG96611020345678"), 200));System.out.println(fee(new CashOnDelivery(), 200));3.00.52.0case CardPayment card значи „ако обектът е CardPayment, наречи го card“. Това е instanceof с шаблон от модул „Наследяване“, но за много типове наведнъж.
default няма. Компилаторът знае трите наследника и проверява, че всеки е покрит. Забравиш ли един, кодът не се компилира:
static double fee(Payment payment, double amount) { return switch (payment) { case CardPayment card -> amount * 0.015; case BankTransfer transfer -> 0.50; };}Main.java:9: error: the switch expression does not cover all possible input values return switch (payment) { ^Това е ползата от sealed. Ако утре добавиш четвърти начин на плащане, компилаторът ще ти покаже всеки switch, който трябва да научи за него.
Шаблонът за record вади полетата направо, а when добавя условие към случая:
static String describe(Payment payment) { return switch (payment) { case CardPayment(String number) -> "карта ****" + number.substring(number.length() - 4); case BankTransfer(String iban) when iban.startsWith("BG") -> "превод от българска сметка"; case BankTransfer(String iban) -> "превод от чужбина"; case CashOnDelivery() -> "наложен платеж"; };}System.out.println(describe(new CardPayment("4111222233334444")));System.out.println(describe(new BankTransfer("BG80BNBG96611020345678")));System.out.println(describe(new BankTransfer("DE89370400440532013000")));System.out.println(describe(new CashOnDelivery()));карта ****4444превод от българска сметкапревод от чужбинаналожен платежПо-точно, за любопитните
- Шаблонът за
recordработи и сinstanceof:
Payment payment = new CardPayment("4111222233334444");if (payment instanceof CardPayment(String number)) { System.out.println("Картата завършва на " + number.substring(12));}Картата завършва на 4444- Ако обектът е
null,switchхвърляNullPointerException. Акоnullе смислен вход, добавиcase null -> .... - Шаблоните се вгнездяват:
case Order(Customer(String name), double total).
Пробвай
Кой случай печели?
Какво ще отпечата програмата?
static String label(Payment payment, double amount) { return switch (payment) { case CashOnDelivery cash when amount > 500 -> "наложен платеж: не се приема над 500 €"; case CashOnDelivery cash -> "наложен платеж"; case CardPayment card when amount > 1000 -> "карта: нужно е потвърждение"; case CardPayment card -> "карта"; case BankTransfer transfer -> "превод"; };}
System.out.println(label(new CashOnDelivery(), 650));System.out.println(label(new CashOnDelivery(), 80));System.out.println(label(new CardPayment("4111222233334444"), 650));when и стига до втория. Картата е за 650 €, а потвърждение се иска над 1000 €, затова печели случаят без условие: „карта“.По-точно, за любопитните
Случаят с when трябва да е преди общия за същия тип. Обратното не се компилира, защото общият случай хваща всичко и този с when никога не би се стигнал:
case CashOnDelivery cash -> "наложен платеж";case CashOnDelivery cash when amount > 500 -> "наложен платеж: не се приема над 500 €";Main.java:11: error: this case label is dominated by a preceding case label case CashOnDelivery cash when amount > 500 -> "наложен платеж: не се приема над 500 €"; ^Идеята
Текстови блокове и var
Текст на няколко реда, като касова бележка или JSON, с " и \n на всеки ред се чете трудно. От Java 15 има текстов блок между """ и """:
String receipt = """ Магазин „Ъгъла“ Кафе на зърна 18.50 € Мед 12.00 € Общо: 30.50 € """;System.out.print(receipt);
String json = """ {"customer": "%s", "total": %.2f} """.formatted("Мария", 75.5);System.out.print(json);Магазин „Ъгъла“Кафе на зърна 18.50 €Мед 12.00 €Общо: 30.50 €{"customer": "Мария", "total": 75.50}- След отварящите
"""текстът започва на нов ред. - Общият отстъп отляво се маха. Мярката е най-левият ред, включително затварящите
""". - Кавичките вътре не се екранират.
formatted(...)попълва%sи%.2f, кактоString.format.
И едно напомняне от „Основи на езика“: var оставя компилатора да изведе типа. С дългите типове от колекциите той спестява най-много:
var prices = new LinkedHashMap<String, Double>();prices.put("кафе", 18.50);prices.put("мед", 12.00);for (var entry : prices.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue() + " €");}кафе: 18.5 €мед: 12.0 €var е само за локални променливи със стойност. Типът е определен веднъж и не се сменя. Полета, параметри и var x = null; не може.
Упражнение
Модерна Java
Реши задачите в редактора си.
1. Фигури.sealed interface Shape с три record наследника: Circle(double radius), Rectangle(double width, double height) и Triangle(double base, double height). Напиши area(Shape) със switch и шаблони за record, без default. Напиши и name(Shape), който за правоъгълник с равни страни казва „квадрат“ (с when). Отпечатай кръг с радиус 1, правоъгълник 2 на 3, квадрат 4 на 4, триъгълник с основа 3 и височина 5 и общата площ.кръг: 3.14правоъгълник: 6.00квадрат: 16.00триъгълник: 7.50Общо: 32.64sealed interface Delivery с ToOffice(String office), ToAddress(String city, double weightKg) и Pickup(). Цената: до офис 4,50 €; до адрес 5,90 € в София и 6,90 € другаде, плюс 1 € за всеки килограм над 5; от магазина 0 €. Отпечатай всяка доставка (с toString на record) и цената ѝ.ToOffice[office=Варна Център] -> 4.50 €ToAddress[city=София, weightKg=2.0] -> 5.90 €ToAddress[city=Варна, weightKg=8.0] -> 9.90 €Pickup[] -> 0.00 €record Point(int x, int y) и преброй колко пъти е бил във всяка точка с LinkedHashMap<Point, Integer> и merge(p, 1, Integer::sum). Накрая потърси new Point(2, 3) в речника.Point[x=2, y=3]: 3Point[x=5, y=1]: 2Point[x=0, y=0]: 1Посещения в (2, 3): 3Още задачи
- Студент като record.
record Student(String name, double grade)с компактен конструктор, който отказва оценка извън 2–6. Подреди списък от студенти сComparator.comparing(Student::grade). - Ден от седмицата. Пренапиши задачата „ден от седмицата“ от урока „Условия“ със
switchизраз:case 1, 2, 3, 4, 5 -> "делник". - Писмо. С текстов блок и
formattedнаправи писмо за потвърждение на резервация: име, дата, брой гости и цена в евро.
Тест