Classes inside classes
Problem
A class that only makes sense in one place
In the lesson about the linked list, Node was a separate class. Anyone in the program can make nodes, but they only make sense in the list. The same goes for an order line:
class OrderLine { String product; int quantity; double price;}
class Order { List<OrderLine> lines = new ArrayList<>();}OrderLine without Order makes no sense. The name is long so it does not clash with the line of an invoice, InvoiceLine, or with the line of a warehouse. And nothing says that the two classes go together. Can the line live inside the order?
The idea
Static nested class
It can. A class declared inside another class is called a 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("Coffee beans", 2, 12.5));order.add(new Order.Line("Mug", 4, 3.0));System.out.println("Total: €" + order.total());Total: €37.0- From the outside, the name is
Order.Line. InsideOrder, it is justLine. staticmeans that the line is not tied to a specific order.new Order.Line(...)is made without anOrderobject, like a regular class.- If it is
private static class Line, nobody outsideOrdercan use it.
You have already met such a class: Map.Entry from the HashMap lesson is nested in Map. And LinkedList in Java keeps its nodes in a private static class Node.
The idea
Inner class: it sees the outer object
The cart has a discount for all products in it. Each product must know the discount of its own cart. Then the class has no 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("Headphones", 80);cart.add("Case", 20);cart.print();Headphones: €72.0Case: €18.0static is called an inner class. Each Item is created by a specific cart and sees its fields, even the private ones. finalPrice reads the discountPercent of the cart it is in.Cart cart = new Cart(10);cart.add("Headphones", 80);cart.add("Case", 20);cart.print();Step by step, in words
- The cart is a Cart object with a 10% discount.
- add creates an Item. Besides name and price, each Item keeps a hidden arrow to the cart that created it: Cart.this.
- The second Item points to the same cart.
- finalPrice of "Headphones" follows the hidden arrow and reads discountPercent: 80 * (100 - 10) / 100 = 72.0.
- finalPrice of "Case" follows the hidden arrow and reads discountPercent: 20 * (100 - 10) / 100 = 18.0.
More precisely, if you are curious
- From the outside, an inner object is created through the object of the outer class:
cart.new Item(...). The syntax is rare, because usually the outer class creates the inner ones itself, likeaddhere:
Cart cart = new Cart(10);Cart.Item cable = cart.new Item("Cable", 10);System.out.println(cable.finalPrice());9.0- Each inner object keeps a hidden reference to the outer one. If the fields of the two classes have the same name, you write the outer one as
Cart.this.discountPercent. - If the nested class does not use the fields of the outer class, make it
static. That way it does not keep a reference it does not need.
Try it
Two carts
The Cart class is the same. One cart has no discount, and the other has 20% for club members.
What will the program print?
Cart regular = new Cart(0);Cart member = new Cart(20);regular.add("Book", 15);member.add("Book", 15);regular.print();member.print();Each Item sees the discount of the cart that created it. The book in regular has 0%, and the one in member has 20%.
Try it
Find the bug
Main.java:9: error: non-static variable currency cannot be referenced from a static context return product + ": " + currency + price; ^Which line has the problem?
Pick the line with the bug.
Line is static, so it is not tied to an order. There is no order whose currency it could read. The fix is to remove static. Then Line becomes an inner class and sees the currency of its own order. Another way is to make currency static, if the currency is the same for all orders.The idea
A class in a method
A class can also be declared in a method. Then it is only visible there and is called a local class:
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 seller: " + best.name + " (" + best.count + " sold)");}String[] names = { "Coffee", "Tea", "Water" };int[] sold = { 120, 45, 300 };printBestSeller(names, sold);Best seller: Water (300 sold)Sale keeps the name and the count together. Nobody uses it outside printBestSeller, so it lives there. Such classes are rare, but it is good to know them.
The idea
A class with no name
The discounts in the shop are an interface:
interface Discount { double apply(double price);}So far you made a class for each discount: class BlackFriday implements Discount { ... }. If the discount is used in only one place, the class can be written right there, with no name:
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() { ... } does two things. It declares a class with no name that implements Discount, and it creates one object of it. Such a class is called an anonymous class. Inside, it is like a regular class: @Override and the methods of the interface.
More precisely, if you are curious
- An anonymous class can also extend a class, not only implement an interface:
new Product("Coffee", 12.5) { ... }calls the constructor ofProductand overrides methods. - An anonymous class can read the local variables of the method it is written in, but only if they do not change after that.
- An interface with a single method, like
Discount, will be written much shorter in the "Modern Java" module. The anonymous class is the step toward that.
The idea
Comparator: sorting by your own rule
Collections.sort sorted text and numbers. But it cannot sort students:
Collections.sort(students);The compiler says no suitable method found for sort(List<Student>). It does not know what to sort students by: by name, by grade, by age?
The rule is given separately, as a Comparator. It is an interface with one method, compare(a, b). It returns a negative number if a should be before b, zero if they are equal, and a positive number if a is after b. It is handy as an anonymous class:
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);[Maria 6.0, Ivan 5.5, Ana 5.5, Peter 4.75]Double.compare(b.grade, a.grade)compares the other way around, so the highest grade comes first.- Ivan and Ana have the same grade and stay in the order they were in.
Collections.sortdoes not swap equal elements. - For text and numbers in reverse order there is a ready-made
Comparator:Collections.reverseOrder().
List<String> cities = new ArrayList<>();cities.add("Sofia");cities.add("Varna");cities.add("Plovdiv");Collections.sort(cities, Collections.reverseOrder());System.out.println(cities);[Varna, Sofia, Plovdiv]More precisely, if you are curious
Comparatoris injava.util, with a type parameter:Comparator<Student>compares students.- Text and numbers are sorted without a
Comparator, becauseStringandIntegerimplement theComparableinterface with acompareTomethod. Your class can implement it too, if it has one natural order.Comparatoris for cases where there are several orders. Double.compareandInteger.compareprotect you from mistakes. Withint,a.age - b.agelooks shorter, but with very large numbers the subtraction overflows and the sign flips.
Try it
Which one is first?
What will the program print?
List<Product> products = new ArrayList<>();products.add(new Product("Monitor", 220));products.add(new Product("Mouse", 25));products.add(new Product("Keyboard", 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) is negative when a is cheaper. So the cheaper one comes first: 25, 60, 220. To put the most expensive first, swap a and b.Exercise
Classes that go together
Solve the tasks in your editor.
1. Shopping cart. A ShoppingCart class with a static nested class Item (name and price) and the methods addItem(Item item), getTotal() and count(). In main, add new ShoppingCart.Item("Milk", 2.5), bread for €1.20 and coffee for €7.50.
Products: 3Total: €11.22. Web server monitor. A WebServer class with a static nested class Request (address and status code) and the methods addRequest(Request r) and printStats(). The stats say how many requests there are, how many there are of each status code (sorted) and what percent are successful, that is, with a code from 200 to 299. The requests are /home 200, /cart 200, /login 401, /home 200, /api/orders 500.
Requests: 5200: 3401: 1500: 1Successful: 60%3. Building manager. A Building class with a static nested class Apartment (number, number of rooms, owner) and the methods addApartment(Apartment a) and getApartmentInfo(int number). Keep the apartments in a Map<Integer, Apartment>. Add 101 with 3 rooms owned by Ivanova and 102 with 2 rooms owned by Petrov. Look up 101 and 999.
Apartment 101: 3 rooms, owner IvanovaThere is no apartment 999.4. Ranking. Sort the students from the lesson by grade, from the highest down. When the grades are equal, sort them by name, in alphabetical order. It all goes in one Comparator: if the comparison by grade is not 0, return it, otherwise compare the names with compareTo.
Maria 6.0Ana 5.5Ivan 5.5Peter 4.75More tasks
- Computer configurator. A
Computerclass with the static nested classesCPU(brand, speed in GHz),RAM(GB) andStorage(GB) and the methodssetCPU,setRAM,setStorage.toStringdescribes the configuration:Intel 3.5 GHz, 16 GB RAM, 512 GB. - Trip planner, again. Do the task from the
HashMaplesson with a nested classTripPlanner.Destinationthat keeps the name and the list of activities. - Contact network, again. Move
Contactfrom the task in theHashMaplesson insideContactNetwork. Should it bestatic? - Mental math, again. Move
Questionfrom the task in the lesson about lists insideMathGame. - Products in two ways. Sort a list of products by price from the most expensive, and by name when the price is the same. Then sort the same list by name only, in reverse order.
- Your own discount. Write three anonymous
Discountclasses: 10% over €100, a fixed €5 off and "second product at half price". Apply them to one and the same cart.
Test