Java,
Week 4
Programming Concepts using Java, Week 4: abstract classes and interfaces, default and static interface methods, private (nested) classes, controlled interaction with objects, callbacks and iterators. Every Java example in these notes was really compiled and run.
Chapters
Abstract Classes and Interfaces
Week 3 built a class hierarchy. This lecture asks how a parent class can force its subclasses to supply a method, how that makes one function work for many types, and why Java adds interfaces to get around the single-parent rule.
1.1Abstract classes and abstract methods
Suppose every shape must be able to report its perimeter, but a plain shape has no formula for it: the formula belongs to a circle, a square, a rectangle. The parent class can declare the method without a body:
public abstract double perimeter(); The parent forces every subclass to provide a concrete implementation.- You cannot create objects from a class that has an abstract method: what would
perimeter()do on such an object? - So the class that contains an abstract method must itself be declared
abstract.
public abstract class Shape {
public abstract double perimeter();
}public class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
public double perimeter() {
return 2 * Math.PI * radius;
}
}public class Main {
public static void main(String[] args) {
Shape[] shapearr = new Shape[3];
double[] sizearr = new double[3];
shapearr[0] = new Circle(1);
shapearr[1] = new Square(2);
shapearr[2] = new Rectangle(2, 3);
for (int i = 0; i < 3; ++i) {
sizearr[i] = shapearr[i].perimeter(); // each object runs its own perimeter()
System.out.println(sizearr[i]);
}
}
}6.283185307179586
8.0
10.0The classes together make one program. Shape only says what must exist; Circle, Square and Rectangle say how. The three numbers are the three perimeters (the circle's is 2πr with r = 1).
What the compiler enforces
Three rules, each one shown by a real error message.
1. An abstract class cannot be instantiated.
public class Main {
public static void main(String[] args) {
Shape s = new Shape();
}
}Main.java:3: error: Shape is abstract; cannot be instantiated
Shape s = new Shape();
^
1 error2. A class with an abstract method must be declared abstract. Leave the word out and the compiler complains about the class itself:
public class Shape {
public abstract double perimeter();
}Shape.java:1: error: Shape is not abstract and does not override abstract method perimeter() in Shape
public class Shape {
^
1 error3. A concrete subclass must implement every abstract method it inherits.
public class Square extends Shape {
private double side;
public Square(double side) {
this.side = side;
}
// perimeter() is missing
}Square.java:1: error: Square is not abstract and does not override abstract method perimeter() in Shape
public class Square extends Shape {
^
1 errorabstract too, and only a further subclass that fills in the rest can be instantiated (extra):public abstract class Square extends Shape {
protected double side = 2;
public double perimeter() {
return 4 * side;
}
// area() still missing: fine, because Square is abstract too
}public class Unit extends Square {
public double area() {
return side * side;
}
}public class Main {
public static void main(String[] args) {
Shape s = new Unit();
System.out.println(s.perimeter() + " " + s.area());
}
}8.0 4.01.2Variables of an abstract type
We cannot create an object of an abstract class, but we can still declare variables whose type is an abstract class. Such a variable can refer to an object of any concrete subclass.
public class Main {
public static void main(String[] args) {
Shape s; // a variable of an abstract type: fine
s = new Circle(1); // it can refer to any concrete subclass object
System.out.println(s.perimeter());
System.out.println(s instanceof Circle);
}
}6.283185307179586
trueThis is exactly the Week 3 idea of polymorphism: the static type Shape decides what we may call, the real object decides which code runs.
int sizearr[] and leaves out the semicolon after perimeter(). A perimeter is a double, and a double does not squeeze into an int without a cast (Week 2). These notes use double[] sizearr:public class Main {
public static void main(String[] args) {
Shape[] shapearr = new Shape[1];
int[] sizearr = new int[1];
shapearr[0] = new Circle(1);
sizearr[0] = shapearr[0].perimeter();
}
}Main.java:6: error: incompatible types: possible lossy conversion from double to int
sizearr[0] = shapearr[0].perimeter();
^
1 error1.3Abstract classes can hold more than abstract methods
An abstract class is a normal class with one restriction (no direct objects). It may have fields, constructors and fully written methods. A well-known use: write the common steps once, and leave only the part that differs as abstract. Here describe() is already complete and calls perimeter(), which each subclass supplies (extra):
public abstract class Shape {
private String name;
public Shape(String name) { // an abstract class can have a constructor
this.name = name;
}
public abstract double perimeter(); // the part every subclass must supply
public String describe() { // a normal method, already written
return name + " with perimeter " + perimeter();
}
}public class Main {
public static void main(String[] args) {
Shape s = new Square(2);
System.out.println(s.describe());
}
}Square with perimeter 8.0Two more facts, both worth knowing for an interview: an abstract class's constructor does run (when a subclass object is built, the parent part is built first) and a class can be abstract even with no abstract method at all, just to forbid direct objects.
public class Main {
public static void main(String[] args) {
Shape s = new Square(2); // an abstract class is built as part of its subclass object
System.out.println(s.perimeter());
}
}Shape constructor
Square constructor
8.0public abstract class Base {
public void hello() {
System.out.println("hello from Base");
}
}public class Main {
public static void main(String[] args) {
Base b = new Base();
}
}Main.java:3: error: Base is abstract; cannot be instantiated
Base b = new Base();
^
1 error1.4Generic functions through an abstract class
Abstract classes can also describe a capability rather than a thing. Suppose we want one sorting function for any kind of object, as long as two objects can be compared. The only thing sorting needs is a way to ask "is this one smaller?":
cmpreturns -1 ifthis<s, 0 if equal, 1 ifthis>s.- Inside quicksort, to compare
a[i]anda[j]we writea[i].cmp(a[j]), whatever the real class is.
The class that wants to be sortable extends Comparable and defines cmp. The lecture says to compare this.size with ((Myclass) s).size: the cast is needed to reach size, because s is typed Comparable and a Comparable has no size. The instanceof test first makes the cast safe (Week 3). A complete, running version:
public abstract class Comparable {
public abstract int cmp(Comparable s);
// return -1 if this < s
// return 0 if this == s
// return 1 if this > s
}public class SortFunctions {
public static void quicksort(Comparable[] a) {
quicksort(a, 0, a.length - 1);
}
private static void quicksort(Comparable[] a, int lo, int hi) {
if (lo >= hi) return;
Comparable pivot = a[hi];
int p = lo;
for (int i = lo; i < hi; ++i) {
if (a[i].cmp(pivot) < 0) { // compare with cmp(), nothing else
Comparable t = a[i]; a[i] = a[p]; a[p] = t;
++p;
}
}
Comparable t = a[p]; a[p] = a[hi]; a[hi] = t;
quicksort(a, lo, p - 1);
quicksort(a, p + 1, hi);
}
}public class Myclass extends Comparable {
private double size; // quantity used for comparison
public Myclass(double size) {
this.size = size;
}
public double getSize() {
return size;
}
public int cmp(Comparable s) {
if (s instanceof Myclass) {
double other = ((Myclass) s).size; // cast, to reach s.size
if (this.size < other) return -1;
if (this.size > other) return 1;
return 0;
}
return 0;
}
}public class Main {
public static void main(String[] args) {
Myclass[] a = { new Myclass(3.5), new Myclass(1.25), new Myclass(9), new Myclass(2) };
SortFunctions.quicksort(a);
for (Myclass m : a) {
System.out.println(m.getSize());
}
}
}1.25
2.0
3.5
9.0cmp is only a skeleton: it shows the if and then comments. Written as is, it does not compile, because the method promises an int on every path:public class Myclass extends Comparable {
private double size;
public int cmp(Comparable s) {
if (s instanceof Myclass) {
// compare this.size and ((Myclass) s).size
}
}
}Myclass.java:8: error: missing return statement
}
^
1 errorjava.lang.Comparable, with a method named compareTo. The lecture defines its own Comparable with cmp to teach the idea. A class in your own package takes precedence over java.lang, so both exist side by side:public class Main {
public static void main(String[] args) {
System.out.println(Comparable.class.getName());
System.out.println(java.lang.Comparable.class.getName());
System.out.println("abc".compareTo("abd")); // the real one: compareTo, not cmp
}
}Comparable
java.lang.Comparable
-11.5Multiple inheritance: interfaces
Here is the catch. Circle already extends Shape, and Java allows only one parent class (Week 3: the diamond problem). How can a Circle also be a Comparable? The answer is to make Comparable an interface:
public interface Comparable {
public abstract int cmp(Comparable s);
}public class Circle extends Shape implements Comparable {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
public double perimeter() {
return 2 * Math.PI * radius;
}
public int cmp(Comparable s) {
Circle other = (Circle) s;
return Double.compare(this.perimeter(), other.perimeter());
}
}public class Main {
public static void main(String[] args) {
Circle[] c = { new Circle(3), new Circle(1), new Circle(2) };
SortFunctions.quicksort(c); // a Circle[] is a Comparable[]
for (Circle x : c) {
System.out.println(x.perimeter());
}
}
}6.283185307179586
12.566370614359172
18.84955592153876implements, not extends, for an interface (extending an interface is something an interface does to another interface).Why this is safe: two abstract methods with the same name cannot contradict each other, because neither has code. Two classes could (the diamond), which is why Java keeps classes to one parent. Trying to extend two classes is a syntax error:
public class Circle extends Shape, Comparable {
}Circle.java:1: error: '{' expected
public class Circle extends Shape, Comparable {
^
1 errorAnd several interfaces are fine; the one object is then a Shape, a Comparable and a Printable at once, so it can be passed to any code that wants any of those types:
public class Circle extends Shape implements Comparable, Printable {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
public double perimeter() {
return 2 * Math.PI * radius;
}
public int cmp(Comparable s) {
return Double.compare(perimeter(), ((Circle) s).perimeter());
}
public void print() {
System.out.println("Circle, perimeter " + perimeter());
}
}public class Main {
public static void main(String[] args) {
Circle c = new Circle(1);
Shape s = c; // a Circle is a Shape
Comparable k = c; // ... and a Comparable
Printable p = c; // ... and a Printable
p.print();
System.out.println(s instanceof Comparable);
}
}Circle, perimeter 6.283185307179586
true1.6Lecture summary
- Use the class hierarchy to group related classes together.
- An abstract method in a parent forces each subclass to implement it sensibly.
- Any class with an abstract method is itself abstract; no objects can be created from it, but variables of that type are fine.
- Abstract classes can describe capabilities, which allows generic functions such as one quicksort for everything Comparable.
- An interface is an abstract class with no concrete components. A class extends one class but implements any number of interfaces.
Interfaces
The previous lecture ended with the definition of an interface. This one looks at what interfaces are for, at the two kinds of method Java later allowed inside them, and at the old multiple-inheritance conflict that those additions brought back.
2.1An interface is a purely abstract class
- All methods are abstract. They are implicitly
public abstract, so writingint cmp(Comparable s);is enough. - A class implements an interface by providing concrete code for each abstract function.
- Classes can implement multiple interfaces. Since the functions are abstract there is no contradictory inheritance: nothing to pick between.
If a class says it implements an interface but leaves a method out, the compiler refuses, exactly as for an abstract class:
public class Circle implements Comparable {
}Circle.java:1: error: Circle is not abstract and does not override abstract method cmp(Comparable) in Comparable
public class Circle implements Comparable {
^
1 errorAnd because interface methods are public, the implementing method must be public too. Leaving the word out gives the method package access, which is a weaker access than the interface promised:
public class Circle implements Comparable {
int cmp(Comparable s) { // package access: weaker than public
return 0;
}
}Circle.java:2: error: cmp(Comparable) in Circle cannot implement cmp(Comparable) in Comparable
int cmp(Comparable s) { // package access: weaker than public
^
attempting to assign weaker access privileges; was public
1 error2.2Describing the relevant slice of a class
- Interfaces describe relevant aspects of a class. The abstract functions describe a specific "slice" of capabilities.
- Another class only needs to know about these capabilities, nothing else. This is exposing limited capabilities.
Take the generic quicksort for any datatype that supports comparison. We express that capability by making the argument type Comparable[]. That is the only information quicksort needs about the underlying type. Now the very same quicksort sorts employees by salary and plain numbers wrapped in a class, with no change to it:
public class Main {
public static void main(String[] args) {
Employee[] e = { new Employee("Ravi", 2000), new Employee("Asha", 1000), new Employee("Meena", 1500) };
SortFunctions.quicksort(e);
for (Employee x : e) System.out.println(x.getName());
Dnum[] d = { new Dnum(30), new Dnum(10), new Dnum(20) };
SortFunctions.quicksort(d); // same function, a completely different type
for (Dnum x : d) System.out.println(x.value);
}
}Asha
Meena
Ravi
10
20
30cmp (return -1, 0 or 1). That promise lives only in a comment, and every implementer has to honour it by hand.What else an interface can contain (extra)
Interface fields are always constants (public static final), so they need a value and can never change. They are shared by every implementer:
public interface Limits {
int MAX = 100; // implicitly public static final
}public class Main implements Limits {
public static void main(String[] args) {
System.out.println(MAX);
System.out.println(Limits.MAX);
}
}100
100public class Main implements Limits {
public static void main(String[] args) {
MAX = 200;
}
}Main.java:3: error: cannot assign a value to static final variable MAX
MAX = 200;
^
1 errorpublic interface Counter {
int count; // no value: not allowed, every field is a constant
}Counter.java:2: error: = expected
int count; // no value: not allowed, every field is a constant
^
1 errorAnd like an abstract class, an interface has no objects of its own:
public class Main {
public static void main(String[] args) {
Comparable c = new Comparable();
}
}Main.java:3: error: Comparable is abstract; cannot be instantiated
Comparable c = new Comparable();
^
1 error2.3Adding methods to interfaces
Newer versions of Java extended interfaces to allow concrete functions to be added. There are two kinds.
- Cannot access instance variables (they belong to no object).
- Invoke them directly or with the interface name:
Comparable.cmpdoc().
- Give a default implementation for some functions.
- The class may override them.
- Invoke like a normal method, on an object:
a[i].cmp(a[j]).
Static function in an interface
public interface Comparable {
public abstract int cmp(Comparable s);
public static String cmpdoc() {
String s;
s = "Return -1 if this < s, ";
s += "0 if this == s, ";
s += "+1 if this > s.";
return s;
}
}public class Main {
public static void main(String[] args) {
System.out.println(Comparable.cmpdoc());
}
}Return -1 if this < s, 0 if this == s, +1 if this > s.A static interface method belongs to the interface only. It is not inherited by the implementing class, and cannot be called through an object either (extra, checked by running):
public class Main {
public static void main(String[] args) {
Dnum d = new Dnum();
System.out.println(d.cmpdoc());
}
}Main.java:4: error: cannot find symbol
System.out.println(d.cmpdoc());
^
symbol: method cmpdoc()
location: variable d of type Dnum
1 errorpublic class Main {
public static void main(String[] args) {
System.out.println(Dnum.cmpdoc());
}
}Main.java:3: error: cannot find symbol
System.out.println(Dnum.cmpdoc());
^
symbol: method cmpdoc()
location: class Dnum
1 errorDefault function in an interface
A class that does nothing gets the default. A class that wants something better overrides it. Calling it is the same either way:
public interface Comparable {
public default int cmp(Comparable s) {
return 0; // default: everything is "equal"
}
}public class Dnum implements Comparable {
public int value;
public Dnum(int value) {
this.value = value;
}
@Override
public int cmp(Comparable s) { // overrides the default
return Integer.compare(value, ((Dnum) s).value);
}
}public class Main {
public static void main(String[] args) {
Comparable p = new Plain();
System.out.println(p.cmp(new Plain())); // the default runs
Comparable a = new Dnum(1);
System.out.println(a.cmp(new Dnum(5))); // the override runs
}
}0
-12.4Dealing with conflicts
The old problem of multiple inheritance returns. Once interfaces carry code, two of them can offer different versions of the same method.
- Conflict between static/default methods: the subclass must provide a fresh implementation.
- Conflict between a class and an interface: the method inherited from the class wins. This was motivated by reverse compatibility.
Two interfaces. With nothing written, the compiler refuses:
public class C implements I1, I2 {
}C.java:1: error: types I1 and I2 are incompatible;
public class C implements I1, I2 {
^
class C inherits unrelated defaults for hello() from types I1 and I2
1 errorThe class must supply a fresh implementation. Inside it, you may still choose one of the inherited versions by naming the interface before super:
public class C implements I1, I2 {
@Override
public String hello() { // a fresh implementation
return "C picks: " + I1.super.hello(); // or write something brand new
}
}public class Main {
public static void main(String[] args) {
System.out.println(new C().hello());
}
}C picks: from I1A class and an interface. Employee inherits from the class Person and implements Designation. Both have getName(). The one inherited from the class wins, with no error and no choice to make:
public class Person {
public String getName() {
return "No name";
}
}public interface Designation {
public default String getName() {
return "No designation";
}
}public class Employee extends Person implements Designation {
}public class Main {
public static void main(String[] args) {
Employee e = new Employee();
System.out.println(e.getName());
Designation d = e; // through the interface type: still the class's method
System.out.println(d.getName());
}
}No name
No nameOld code written before Java 8 had classes with methods but no defaults. When defaults were added to widely used interfaces, a rule that class beats interface kept all that old code compiling and behaving as before. This is the lecture's point about the pitfalls of extending a language while maintaining compatibility.
A real example of the same idea: forEach was added to the standard library as a default method of Iterable, so every existing list gained it without a single class being rewritten (extra):
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("Asha");
names.add("Ravi");
names.forEach(n -> System.out.println("hello " + n)); // forEach is a default method of Iterable
}
}hello Asha
hello Ravi2.5Lecture summary
- Interfaces express abstract capabilities, in terms of methods that must be present.
- They cannot specify the intended behaviour of those functions.
- Java later allowed concrete functions: static (no instance variables) and default (may be overridden).
- This reintroduces the conflict in multiple inheritance: the subclass must resolve it with a fresh implementation; for a class versus an interface, the class wins.
Private Classes
Until now a class was either public (anyone may use it) or hidden inside its file. This lecture shows a third option: a class inside another class, visible only to the class that holds it. It is encapsulation applied one level deeper.
3.1Nested objects
- An instance variable can be of a user defined type.
EmployeeusesDatefor the joining date. Dateis a public class, also available to other classes: that is sensible, because everyone has dates.- So the question is: when could a private class make sense?
public class Employee {
private String name;
private double salary;
private Date joinDate; // a nested object: an instance variable of a user defined type
public Employee(String name, double salary, Date joinDate) {
this.name = name;
this.salary = salary;
this.joinDate = joinDate;
}
public String toString() {
return name + " joined on " + joinDate;
}
}public class Main {
public static void main(String[] args) {
Employee e = new Employee("Asha", 1000, new Date(14, 1, 2022));
System.out.println(e);
}
}Asha joined on 14-1-20223.2A linked list and its Node
A LinkedList is built out of Node objects: each node holds a value and a pointer to the next node. The lecture first makes Node a public class. Why should Node be public? Nobody using a list wants to deal with nodes at all.
- We may want to enhance
Nodelater, with aprevfield, to get a doubly linked list. - That does not affect the interface of
LinkedList:insertandheadstay the same. But ifNodeis public, other code may already depend on it.
With a public Node any outside code can build nodes, read their fields and even tie them into a loop. The list has no say:
public class Node {
public Object data;
public Node next;
public Node(Object data) {
this.data = data;
}
}public class Main {
public static void main(String[] args) {
LinkedList list = new LinkedList();
list.insert("a");
list.insert("b");
list.insert("c");
System.out.println(list.head()); // the most recently inserted comes out first
System.out.println(list.head());
System.out.println(list.size());
Node loose = new Node("anyone can build a node"); // Node is public: other code can use it
loose.next = loose; // ... and even tie it into a loop
System.out.println(loose.next.data);
}
}c
b
1
anyone can build a nodehead() returns the first value and removes it (first = first.next), so it behaves like a "pop", yet it never reduces size. The listings here add size--. The lecture also elides insert; here it puts the new node at the front, which is why the newest value comes out first.3.3Make Node a private class
private modifier. It is also called an inner class. Objects of the private class can see the private components of the enclosing class (and the enclosing class can see theirs).public class LinkedList {
private class Node {
private Object data;
private Node next;
private Node(Object data) {
this.data = data;
}
}
private int size;
private Node first;
public Object head() {
Object returnval = null;
if (first != null) {
returnval = first.data; // the outer class may read Node's private fields
first = first.next;
size--;
}
return returnval;
}
public void insert(Object newData) {
Node n = new Node(newData);
n.next = first;
first = n;
size++;
}
public int size() {
return size;
}
}public class Main {
public static void main(String[] args) {
LinkedList list = new LinkedList();
list.insert("a");
list.insert("b");
list.insert("c");
System.out.println(list.head());
System.out.println(list.head());
System.out.println(list.size());
}
}c
b
1Notice what the code does. head() reads first.data and first.next, which are private fields of Node, and it compiles: nesting gives the outer class that access. Outside code, on the other hand, cannot even name the type:
public class Main {
public static void main(String[] args) {
LinkedList.Node n = null;
}
}Main.java:3: error: LinkedList.Node has private access in LinkedList
LinkedList.Node n = null;
^
1 errorThe same visibility works the other way round. Here the private inner class changes a private field of the enclosing object, and the notation Counter.this names that enclosing object explicitly:
public class Counter {
private int count = 0; // private to Counter
private class Step {
void bump() {
count++; // the inner class reaches the outer private field
System.out.println("count is now " + Counter.this.count);
}
}
public void run() {
Step s = new Step();
s.bump();
s.bump();
}
}public class Main {
public static void main(String[] args) {
new Counter().run();
}
}count is now 1
count is now 2Rules worth knowing (extra, all checked by running)
- Only a nested class can be private. A top-level class is
publicor package-private;privateon it is an error.
private class Node {
}Node.java:1: error: modifier private not allowed here
private class Node {
^
1 error- Inner versus static nested. An ordinary inner class is tied to one object of the outer class (it can read
xbelow). Astaticnested class is not tied to any outer object, so it is the lighter choice for something likeNodethat never needs the outer object.
public class Outer {
private int x = 7;
private class Inner { // ordinary inner class: tied to one Outer object
int twice() { return 2 * x; }
}
private static class Plain { // static nested class: no Outer object needed
int five() { return 5; }
}
public void show() {
Inner a = new Inner();
Plain b = new Plain();
System.out.println(a.twice());
System.out.println(b.five());
}
}14
5From outside, an accessible inner class needs an outer object to be built, written outer.new Inner(), and inside it Outer.this.name reaches the outer field even when the inner class has a field of the same name:
public class Main {
public static void main(String[] args) {
Outer o = new Outer();
Outer.Inner in = o.new Inner(); // an inner object needs an outer object
in.show();
}
}inner
inner
outer3.4Lecture summary
- An object can have nested objects as instance variables.
- In some situations the structure of these nested objects need not be exposed.
- Private classes give an additional degree of data encapsulation.
- Combine private classes with interfaces to give controlled access to the state of an object: the next lecture.
Controlled Interaction with Objects
Encapsulation is a key principle of object oriented programming. This lecture goes from the simple case, a date that must stay valid, to a harder one: a railway booking system that must control who may ask what, and how often, by combining a private class with an interface.
4.1Manipulating objects: accessors and mutators
- Internal data is private. Access to it is regulated through public methods.
- Accessor methods read (
getDay,getMonth,getYear). Mutator methods change (setDay,setMonth,setYear). - Regulating access lets us ensure data integrity.
Why not simply a setter for every field? Because a date is a whole: whether the day is legal depends on the month and the year. With three independent setters the object can pass through, and even end in, a state that was never valid:
public class Main {
public static void main(String[] args) {
Date d = new Date();
d.setDay(31);
d.setMonth(1);
d.setYear(2024); // 31 January: fine
d.setMonth(4); // now it says 31 April, and nothing stopped us
System.out.println(d);
}
}31-4-2024Update the data as a whole, rather than individual components. One method setDate(d, m, y) receives all three values and validates the day-month-year combination before changing anything:
public class Date {
private int day, month, year;
public int getDay() { return day; }
public int getMonth() { return month; }
public int getYear() { return year; }
private static boolean leap(int y) {
return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
}
private static int daysIn(int m, int y) {
switch (m) {
case 4: case 6: case 9: case 11: return 30;
case 2: return leap(y) ? 29 : 28;
default: return 31;
}
}
// update the date as a whole, not field by field
public void setDate(int d, int m, int y) {
if (m < 1 || m > 12 || d < 1 || d > daysIn(m, y)) {
throw new IllegalArgumentException("invalid date " + d + "-" + m + "-" + y);
}
day = d;
month = m;
year = y;
}
}public class Main {
public static void main(String[] args) {
Date d = new Date();
d.setDate(29, 2, 2024); // 2024 is a leap year
System.out.println(d.getDay() + "-" + d.getMonth() + "-" + d.getYear());
d.setDate(31, 4, 2024); // April has 30 days
}
}29-2-2024
Exception in thread "main" java.lang.IllegalArgumentException: invalid date 31-4-2024
at Date.setDate(Date.java:23)
at Main.main(Main.java:6)The lecture leaves the validation as a comment ("validate the d-m-y combination"). Above it is real: months run 1 to 12 and a day may not exceed the length of that month, with February 29 allowed only in a leap year (divisible by 4, but by 400 if it is a century).
4.2Interaction with state: the railway query
A booking object stores train reservation information. It can answer: how many seats are available on train number trainno on date d? To control spamming by bots, the user must log in before querying, so the query must be connected to the user's logged-in status.
- The first version has
getStatus(trainno, d)directly onRailwayBooking. Anyone can call it, logged in or not. Nothing links a query to a user. - Use objects. On login the user receives an object that can make a query. Only a successful login yields one, and the object is created from a private class that can look up
railwayDB.
Step 1: login returns an object of a private class
The lecture's login only returns inside the if. What happens when the password is wrong? Java insists on an answer for every path:
public class RailwayBooking {
private BookingDB railwayDB = new BookingDB();
private class QueryObject {
public int getStatus(int trainno, Date d) {
return railwayDB.seatsOn(trainno, d);
}
}
private boolean validLogin(String u, String p) {
return u.equals("asha") && p.equals("secret");
}
public QueryObject login(String u, String p) {
QueryObject qobj;
if (validLogin(u, p)) {
qobj = new QueryObject();
return qobj;
}
}
}RailwayBooking.java:20: error: missing return statement
}
^
1 errorWith return null; added for a failed login, the code compiles. But there is a deeper problem. The method is public, yet its return type QueryObject is private. A caller receives an object whose class it is not allowed to use:
public class Main {
public static void main(String[] args) {
RailwayBooking rb = new RailwayBooking();
int seats = rb.login("asha", "secret").getStatus(12627, new Date(14, 1, 2022));
}
}Main.java:4: error: RailwayBooking.QueryObject.getStatus(int,Date) is defined in an inaccessible class or interface
int seats = rb.login("asha", "secret").getStatus(12627, new Date(14, 1, 2022));
^
1 errorThis is the problem the lecture poses: how does the user know the capabilities of the private class QueryObject?
Step 2: describe the capabilities with an interface
- An interface
QIFdescribes the capabilities of the object returned on login. QueryObjectimplementsQIF, andloginis declared to returnQIF.- The caller never needs to know the class name: the type it sees is public, the class behind it is private.
public interface QIF {
public abstract int getStatus(int trainno, Date d);
}public class RailwayBooking {
private BookingDB railwayDB = new BookingDB();
private boolean validLogin(String u, String p) {
return u.equals("asha") && p.equals("secret");
}
public QIF login(String u, String p) { // the caller only ever sees the interface
if (validLogin(u, p)) {
return new QueryObject();
}
return null;
}
private class QueryObject implements QIF {
public int getStatus(int trainno, Date d) {
return railwayDB.seatsOn(trainno, d);
}
}
}public class Main {
public static void main(String[] args) {
RailwayBooking rb = new RailwayBooking();
QIF q = rb.login("asha", "secret");
System.out.println(q.getStatus(12627, new Date(14, 1, 2022)));
System.out.println(rb.login("asha", "wrong")); // a failed login gives no object at all
}
}27
null4.3Limiting the number of queries per login
The query object so far allows an unlimited number of queries. To limit them, maintain a counter as instance variables of the object returned on login. The object remembers the state of the interaction:
private final int QLIM; and never gives it a value. A final field must be assigned exactly once, normally in the constructor, so the compiler refuses:public class RailwayBooking {
private BookingDB railwayDB = new BookingDB();
public QIF login(String u, String p) {
return new QueryObject();
}
private class QueryObject implements QIF {
private int numQueries;
private final int QLIM; // never given a value
public int getStatus(int trainno, Date d) {
return 0;
}
}
}RailwayBooking.java:10: error: variable QLIM not initialized in the default constructor
private final int QLIM; // never given a value
^
1 errorThe fixed version gives QLIM its value in a constructor (the limit is 2 per login here) and answers -1 once the quota is used up. Every login creates a new QueryObject, so each user has an independent counter:
public class RailwayBooking {
private BookingDB railwayDB = new BookingDB();
private boolean validLogin(String u, String p) {
return u.equals("asha") || u.equals("ravi");
}
public QIF login(String u, String p) {
if (validLogin(u, p)) {
return new QueryObject(2); // each login gets its own object, limit 2
}
return null;
}
private class QueryObject implements QIF {
private int numQueries;
private final int QLIM;
private QueryObject(int limit) {
QLIM = limit;
}
public int getStatus(int trainno, Date d) {
if (numQueries < QLIM) {
numQueries++; // remember the state of the interaction
return railwayDB.seatsOn(trainno, d);
}
return -1; // quota used up
}
}
}public class Main {
public static void main(String[] args) {
RailwayBooking rb = new RailwayBooking();
Date d = new Date(14, 1, 2022);
QIF asha = rb.login("asha", "x");
QIF ravi = rb.login("ravi", "x");
System.out.println(asha.getStatus(12627, d));
System.out.println(asha.getStatus(12627, d));
System.out.println(asha.getStatus(12627, d)); // third query: refused
System.out.println(ravi.getStatus(12628, d)); // Ravi has his own counter
}
}27
27
-1
28Private class for the code and the state, public interface for what the outside may do, and one object per interaction. The same shape appears again in the next lectures (the timer owner, the iterator).
4.4Lecture summary
- We can provide controlled access to an object by combining private classes with interfaces.
- External interaction happens through an object of the private class; its capabilities are known through a public interface.
- The object can maintain instance variables to track the state of the interaction.
Callbacks
Sometimes an object starts another piece of work and carries on with its own. When the other work finishes it must tell the starter. That "call me back" arrangement is a callback, and the lecture builds it step by step to show why interfaces make it generic.
5.1Implementing a call-back facility
MyClass mcreates aTimer tand starts t to run in parallel.mcontinues to run. (We will see later how to invoke parallel execution in Java.)- When the time limit expires,
tnotifiesm. AssumeMyClasshas a functiontimerDone()for this.
Timer t should know whom to notify. So MyClass m passes its own identity (this) when it creates the Timer, and the Timer stores it as its owner.
Version 1: a Timer written for MyClass
The interface Runnable indicates that Timer can run in parallel. The Timer is specific to MyClass: its owner field has the type MyClass.
Timer says implements Runnable but defines a method called start(). Runnable asks for a method named run(), so as written the class is rejected:public class Timer implements Runnable {
private MyClass owner;
public Timer(MyClass o) {
owner = o;
}
public void start() {
owner.timerDone();
}
}Timer.java:1: error: Timer is not abstract and does not override abstract method run() in Runnable
public class Timer implements Runnable {
^
1 errorA working version needs run() to do the waiting and the notifying. To really run the timer in parallel we hand it to a Thread and call start() on the thread (the lecture defers this to a later week, so treat it as an extra). join() only makes the example wait for the timer so that the printed order is the same every time:
public class MyClass {
public void f() throws InterruptedException {
Timer t = new Timer(this); // this object created t
Thread th = new Thread(t); // (extra) a thread so that t runs in parallel
th.start(); // start t
System.out.println("f: timer started, f carries on");
th.join(); // (extra) wait, so the output order is fixed
}
public void timerDone() {
System.out.println("MyClass.timerDone() called");
}
}public class Timer implements Runnable {
// Timer can be invoked in parallel
private MyClass owner;
public Timer(MyClass o) {
owner = o; // creator
}
public void run() {
try {
Thread.sleep(100); // the "time limit"
} catch (InterruptedException e) { }
owner.timerDone(); // notify the owner
}
}public class Main {
public static void main(String[] args) throws InterruptedException {
new MyClass().f();
}
}f: timer started, f carries on
MyClass.timerDone() calledVersion 2: a generic Timer with an Object owner
The Timer is specific to MyClass, but we need a generic timer. Using the Java class hierarchy: make the constructor parameter of type Object, compatible with all caller types (every class is an Object). The price: we must cast the owner back to MyClass to call timerDone().
public class Timer implements Runnable {
private Object owner; // any object at all
public Timer(Object o) {
owner = o;
}
public void run() {
try { Thread.sleep(100); } catch (InterruptedException e) { }
((MyClass) owner).timerDone(); // cast the owner back to MyClass
}
}public class Main {
public static void main(String[] args) throws InterruptedException {
new MyClass().f(); // fine: the owner really is a MyClass
Thread th = new Thread(new Timer(new Other())); // compiles, because any Object is accepted
th.start();
th.join();
}
}f: timer started, f carries on
MyClass.timerDone() called
Exception in thread "Thread-1" java.lang.ClassCastException: class Other cannot be cast to class MyClass (Other and MyClass are in unnamed module of loader 'app')
at Timer.run(Timer.java:10)
at java.base/java.lang.Thread.run(Thread.java:1583)That is the weakness. The compiler accepted new Timer(new Other()) because any object is an Object, yet the cast (MyClass) owner fails when the timer fires: a run-time error, found late (Week 3: a bad cast is a ClassCastException). Note too that the error happens inside the timer's own thread, so main does not see an exception at all.
Version 3: use an interface
- Define an interface for the callback:
TimerOwnerwith the one abstract methodtimerDone(). - Modify
MyClassto implementTimerOwner. - Modify
Timerso that its owner is of typeTimerOwner.
public interface TimerOwner {
public abstract void timerDone();
}public class Timer implements Runnable {
private TimerOwner owner;
public Timer(TimerOwner o) {
owner = o;
}
public void run() {
try { Thread.sleep(100); } catch (InterruptedException e) { }
owner.timerDone(); // no cast, no guessing the class
}
}public class MyClass implements TimerOwner {
public void f() throws InterruptedException {
Timer t = new Timer(this);
Thread th = new Thread(t);
th.start();
System.out.println("f: timer started, f carries on");
th.join();
}
public void timerDone() {
System.out.println("MyClass.timerDone() called");
}
}public class Main {
public static void main(String[] args) throws InterruptedException {
new MyClass().f();
// owner need not be the object that created the Timer
Thread th = new Thread(new Timer(new Other()));
th.start();
th.join();
}
}f: timer started, f carries on
MyClass.timerDone() called
Other.timerDone() calledNo cast and no failing at run time: new Timer(x) compiles only if x really has the capability of being notified. And, as the lecture says, the owner need not be the object that created the Timer: in the second half of main the notified object Other is a completely different one.
Even shorter (extra)
Because TimerOwner has a single abstract method, a Java lambda can stand in for an object that implements it, and an anonymous class is the longer way to the same thing. Callbacks like this are how event handlers (a button click) are written in Java.
public class Main {
public static void main(String[] args) throws InterruptedException {
// TimerOwner has one abstract method, so a lambda can stand in for the object
Thread th = new Thread(new Timer(() -> System.out.println("done, said a lambda")));
th.start();
th.join();
}
}done, said a lambdapublic class Main {
public static void main(String[] args) {
TimerOwner o = new TimerOwner() { // an object of an unnamed class
public void timerDone() {
System.out.println("done, said an anonymous class");
}
};
o.timerDone();
}
}done, said an anonymous class5.2Lecture summary
- Callbacks are useful when we spawn a class in parallel: the spawned object notifies the owner when it is done.
- It can notify some other object too: the
ownerinTimerneed not be the object that created it. - Interfaces make the callback generic: the owner has to have the capability to be notified, and nothing more.
Iterators
The last lecture of the week is another case of interaction with state: walking through every element of a list without knowing how the list is stored.
6.1A linear list, two implementations
A generic linear list of objects: the internal implementation may vary. One implementation keeps the objects in an array:
public class LinearList {
// Array implementation
private int limit = 100;
private Object[] data = new Object[limit];
private int size;
public LinearList() { size = 0; }
public void append(Object o) {
data[size++] = o;
}
}Another keeps them as a linked list of nodes. Its append walks to the last node and attaches a new one. The lecture's loop, for (m = head; m.next != null; m = m.next) {}, ends on the last node. But in a list that is still empty, head is null, so m.next fails on the very first append:
public class Main {
public static void main(String[] args) {
LinearList l = new LinearList();
l.append("first");
}
}Exception in thread "main" java.lang.NullPointerException: Cannot read field "next" because "<local2>" is null
at LinearList.append(LinearList.java:9)
at Main.main(Main.java:4)<local2> because plain javac does not keep local variable names in the class file. Compile with javac -g (IDEs do) and it says "m". The cause is the same: m is null. The listings below handle the empty list with its own case.6.2The wish: loop over every element
We want a loop that runs through all the values in a linear list. If the list is an array with public access we could write:
int i;
for (i = 0; i < data.length; ++i) {
... // do something with data[i]
}For a linked list with public access:
Node m;
for (m = head; m != null; m = m.next) {
... // do something with m.data
}m !+ null, a typo for m != null.But we do not have public access, and we do not know which implementation is in use. We need an abstraction:
Start at the beginning of the list
while (there is a next element) {
get the next element;
do something with it
}6.3The Iterator interface
Encapsulate this functionality in an interface called Iterator:
public interface Iterator {
public abstract boolean has_next();
public abstract Object get_next();
}public class LinearList {
private Node head;
private int size;
public LinearList() { size = 0; }
public void append(Object o) {
Node n = new Node(o);
if (head == null) { // the empty list needs its own case
head = n;
} else {
Node m = head;
while (m.next != null) m = m.next;
m.next = n;
}
size++;
}
private class Node {
private Object data;
private Node next;
private Node(Object data) {
this.data = data;
}
}
private class Iter implements Iterator {
private Node position; // remembers where this iterator is
public Iter() {
position = head;
}
public boolean has_next() {
return position != null;
}
public Object get_next() {
Object o = position.data;
position = position.next;
return o;
}
}
// export a fresh iterator
public Iterator get_iterator() {
Iter it = new Iter();
return it;
}
}public class Main {
public static void main(String[] args) {
LinearList l = new LinearList();
l.append("A");
l.append("B");
l.append("C");
Object o;
Iterator i = l.get_iterator();
while (i.has_next()) {
o = i.get_next();
System.out.println(o);
}
}
}A
B
CHow do we implement Iterator in LinearList? We need a pointer to remember the position of the iterator. The lecture also asks how nested loops are handled: two loops over the same list need two independent positions.
Why the position cannot live in the list itself
If the list keeps one position pointer, the inner loop uses up the pointer the outer loop was relying on. Expected nine pairs, we get two:
public class Main {
public static void main(String[] args) {
Bad l = new Bad();
while (l.hasNext()) {
String oi = l.next();
// the inner loop uses the same pointer, so it eats the rest of the list
while (l.hasNext()) {
System.out.println(oi + "," + l.next());
}
}
}
}A,B
A,CCreate an Iterator object and export it. Every call to get_iterator() returns a fresh object that carries its own position. The class of that object is a private class that implements the public interface: this is the Lecture 3 + Lecture 4 pattern again.
Iter's definition depends on the linear list: for the linked list it holds aNode, for the array it holds an index. The client code is identical:
public class LinearList {
private int limit = 100;
private Object[] data = new Object[limit];
private int size;
public LinearList() { size = 0; }
public void append(Object o) {
data[size++] = o;
}
private class Iter implements Iterator {
private int position = 0; // an index this time, not a node
public boolean has_next() {
return position < size;
}
public Object get_next() {
return data[position++];
}
}
public Iterator get_iterator() {
return new Iter();
}
}public class Main {
public static void main(String[] args) {
LinearList l = new LinearList();
l.append("A");
l.append("B");
l.append("C");
Iterator i = l.get_iterator(); // exactly the same client code as before
while (i.has_next()) {
System.out.println(i.get_next());
}
}
}A
B
CFor nested loops, acquire multiple iterators. A new one for every pass of the outer loop:
public class Main {
public static void main(String[] args) {
LinearList l = new LinearList();
l.append("A");
l.append("B");
l.append("C");
Object oi, oj;
Iterator i, j;
i = l.get_iterator();
while (i.has_next()) {
oi = i.get_next();
j = l.get_iterator(); // a fresh iterator for every pass of the outer loop
while (j.has_next()) {
oj = j.get_next();
System.out.println(oi + "," + oj);
}
}
}
}A,A
A,B
A,C
B,A
B,B
B,C
C,A
C,B
C,Cfor (int j = 0; i < data.length; ++j): the inner condition tests i where it means j. Since i never changes inside, j runs off the end of the array:public class Main {
public static void main(String[] args) {
String[] data = { "A", "B", "C" };
for (int i = 0; i < data.length; ++i) {
for (int j = 0; i < data.length; ++j) { // the lecture's typo: i, not j
System.out.println(data[i] + "," + data[j]);
}
}
}
}A,A
A,B
A,C
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
at Main.main(Main.java:6)6.4The for-each loop uses an iterator
The newer Java for over lists implicitly constructs and uses an iterator:
for (type x : a) {
do something with x;
}- For your own class to work in this loop, it must implement the standard interface
java.lang.Iterable, whose one methoditerator()returns ajava.util.Iterator(with the standard nameshasNextandnext, not the lecture'shas_nextandget_next). This is an extra, checked by running:
import java.util.Iterator;
public class LinearList implements Iterable<Object> {
private Object[] data = new Object[100];
private int size;
public void append(Object o) {
data[size++] = o;
}
public Iterator<Object> iterator() { // the one method Iterable asks for
return new Iterator<Object>() {
private int position = 0;
public boolean hasNext() {
return position < size;
}
public Object next() {
return data[position++];
}
};
}
}public class Main {
public static void main(String[] args) {
LinearList l = new LinearList();
l.append("A");
l.append("B");
l.append("C");
for (Object x : l) { // the compiler builds the iterator loop for us
System.out.println(x);
}
}
}A
B
CFor a standard list, the loop and the explicit iterator are the same thing; the compiler writes the second from the first:
import java.util.ArrayList;
import java.util.Iterator;
public class Main {
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("Asha");
names.add("Ravi");
for (String s : names) { // what you write
System.out.println(s);
}
Iterator<String> it = names.iterator(); // what it means
while (it.hasNext()) {
String s = it.next();
System.out.println(s);
}
}
}Asha
Ravi
Asha
RaviA trap: changing the list during the loop (extra)
An iterator remembers a position in a list. If the list is changed behind its back, the position may no longer mean anything, so the standard iterators check and fail fast:
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("Asha");
names.add("Ravi");
names.add("Meena");
for (String s : names) {
if (s.equals("Asha")) {
names.remove(s); // changing the list while an iterator is running
}
}
}
}Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1095)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1049)
at Main.main(Main.java:10)The safe way to remove while iterating is to ask the iterator itself:
import java.util.ArrayList;
import java.util.Iterator;
public class Main {
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("Asha");
names.add("Ravi");
names.add("Meena");
Iterator<String> it = names.iterator();
while (it.hasNext()) {
if (it.next().equals("Asha")) {
it.remove(); // the iterator itself may remove safely
}
}
System.out.println(names);
}
}[Ravi, Meena]6.5Lecture summary
- Iterators are another example of interaction with state.
- Each iterator needs to remember its position in the list.
- Export an object with a pre-specified interface to handle the interaction.
- The new Java
forover lists implicitly constructs and uses an iterator.
Interview Question Bank
What is likely to be asked from Week 4, with 30-second answers, deeper explanations, a lecture-or-extra tag on each question, and code you can run.
Week 4 is where interviewers test design thinking: abstract class versus interface, hiding implementation behind a private class and an interface, callbacks and iterators. Every answer starts with a 30-second version. Each question is tagged From the lecture or Extra.
One clear sentence: what it is.
Three or four lines of code.
What goes wrong without it.
7.1Interview heat-map: what is most likely
7.2Abstract classes and interfaces
Think of Shape and perimeter().
Shape s = new Shape(); // Shape is abstractMain.java:3: error: Shape is abstract; cannot be instantiatedAn abstract class has at least one method with no body (an abstract method) that forces every subclass to supply the code. You cannot create objects of it (the compiler refuses), but you can declare variables of its type and let them refer to objects of concrete subclasses.
Go deeper
- A class containing an abstract method must itself be declared
abstract; a subclass that does not implement all of them is abstract too. - Why bother? It lets one array or one function handle many kinds of object (polymorphism) while still guaranteeing each kind has the method.
Compare them on state, constructors, and how many a class can have.
| Abstract class | Interface | |
|---|---|---|
| Methods | abstract and concrete mixed | abstract; plus default and static (Java 8+) |
| Fields | any, including changing state | only constants (public static final) |
| Constructor | yes (runs for subclass objects) | no |
| How many per class | one (extends) | any number (implements) |
| Says | "is a kind of", with shared code and state | "can do", a capability |
Use an abstract class for a family of related classes that share code or state (Shape). Use an interface for a capability that unrelated classes can have (Comparable). A class can extend one class but implement many interfaces.
Go deeper
- Interfaces avoid the diamond problem because (originally) nothing in them has code to contradict.
- Since Java 8 interfaces can hold code (default, static), so the line has blurred; the state and constructor rows still separate them.
This is the diamond problem from Week 3.
public class Circle extends Shape, Comparable { }Circle.java:1: error: '{' expectedIf two parent classes both define f() with code, a subclass would not know which to use. Interface methods were abstract, so there was nothing to contradict; the class just writes one implementation. Hence one parent class, any number of interfaces.
Go deeper
- Default methods brought the conflict back in a smaller form, so Java adds rules: write your own version, or the class wins over an interface.
Java 8 changed what an interface may hold.
public interface Comparable {
public default int cmp(Comparable s) { return 0; }
public static String cmpdoc() { return "doc"; }
}A default method gives an interface method a body that implementing classes inherit and may override. A static method belongs to the interface itself and is called Comparable.cmpdoc(); it cannot touch instance variables. They were added so that existing interfaces could grow (for example forEach on Iterable) without breaking every class that implements them.
Go deeper
- Static interface methods are not inherited:
Dnum.cmpdoc()andd.cmpdoc()are both "cannot find symbol". - A default method may call the interface's own abstract methods, but it has no instance variables to read: an interface holds no object state.
Think of I1.hello(), I2.hello() and Person.getName().
public class C implements I1, I2 { }C.java:1: error: types I1 and I2 are incompatible;
class C inherits unrelated defaults for hello() from types I1 and I2Two competing defaults: the compiler refuses, and the class must override the method (it may call one of them with I1.super.hello()). A method from a superclass against an interface default: the class wins, silently.
Go deeper
- The class-wins rule exists for reverse compatibility: old code that relied on a class method must not change meaning when an interface later gains a default.
Designation d = e; d.getName() print for the Employee example?Yes to all, with one restriction.
Shape constructor
Square constructor
8.0Yes. An abstract class is a normal class that cannot be instantiated directly. Its constructor runs as the first step of building a subclass object (the parent part is built first), and it can hold state and finished methods. It may also be declared abstract with no abstract method, just to forbid direct objects.
Go deeper
- Pattern: finished method that calls an abstract one (
describe()callsperimeter()): the template method.
new Square(2)?The lecture writes its own Comparable with cmp; Java has a real one.
Arrays.sort(words); // natural order
Arrays.sort(words, Comparator.comparingInt(String::length)); // another order[banana, fig, kiwi, pear]
[fig, kiwi, pear, banana]The class implements java.lang.Comparable<T> and its compareTo method (the real version of the lecture's cmp); sorting then follows that natural order. To sort a different way without changing the class, pass a Comparator.
Go deeper
- Both are the same idea as the lecture: a capability expressed as an interface, so one sort function serves every type.
compareTo return, and why is "-1/0/1" only a convention?7.3Private classes and controlled access
Node private inside LinkedList?Think about who should know that a list is made of nodes.
public class LinkedList {
private class Node {
private Object data;
private Node next;
}
private Node first;
...
}Main.java:3: error: LinkedList.Node has private access in LinkedListA class declared inside another class. Marked private, only the enclosing class can use it, while the two can still read each other's private members. Making Node private hides the implementation: the list can change (for example to a doubly linked list) without any outside code noticing.
Go deeper
- Only nested classes can be private; a top-level
private classis a compile error.
One needs an outer object, the other does not.
14
5An ordinary inner class is tied to one object of the outer class and can use its fields (Outer.this.x). A static nested class is not tied to any outer object, so it holds no hidden reference to one. For a helper like Node that never needs the outer object, private static class is the leaner choice.
Go deeper
- Creating an inner object from outside needs an outer one:
outer.new Inner().
login return the interface QIF instead of the private class QueryObject?A public method that returns a private type.
Main.java:4: error: RailwayBooking.QueryObject.getStatus(int,Date) is defined in an inaccessible class or interfaceThe class is private, so the caller may not use it, even though the object is in its hands (the compiler reports an inaccessible class). The public interface QIF names what the object can do. So the caller gets the capabilities without the class, and you stay free to change the class.
Go deeper
- Same pattern as the iterator (
IterbehindIterator) and the timer owner.
Think about a date: day, month, year.
31-4-202429-2-2024
Exception in thread "main" java.lang.IllegalArgumentException: invalid date 31-4-2024
at Date.setDate(Date.java:23)
at Main.main(Main.java:6)Because fields can be related: whether a day is legal depends on the month and the year. Independent setters allow impossible states (31 April). Provide one method that takes all the parts (setDate(d, m, y)), validates the combination, and only then changes the object.
Go deeper
- Rejecting bad input with an exception (here
IllegalArgumentException) stops the caller from carrying on with a wrong date.
null or an exception?login hands back nothing on a failed login. Is returning null a good design?What does the caller do with the result?
27
nullIt works, but it pushes a hidden duty on every caller: forget the null check and the next call throws a NullPointerException. Better options are to throw a clear exception on failure, or to return an Optional so that the type itself says "might be empty".
Go deeper
- This is an extra: the lecture only sketches the login and focuses on the capabilities.
Optional add over null?7.4Callbacks and iterators
A timer that tells its creator when time is up.
public interface TimerOwner {
public abstract void timerDone();
}f: timer started, f carries on
MyClass.timerDone() called
Other.timerDone() calledA callback is a call from a spawned object back to its owner when the work is done. The worker is given a reference to an interface (TimerOwner) that declares the notification method, so it needs no knowledge of the owner's class: anything with timerDone() will do, and it need not even be the object that created the Timer.
Go deeper
- The same shape drives GUI event listeners ("on click, call me").
- One-method interfaces can be written as a lambda:
new Timer(() -> ...).
Object and cast?Timer(Object o) accepts everything. Why is that worse than Timer(TimerOwner o)?Compare when each mistake is found.
Exception in thread "Thread-1" java.lang.ClassCastException: class Other cannot be cast to class MyClass (Other and MyClass are in unnamed module of loader 'app')With Object the compiler accepts any argument, so passing a wrong kind of object is found only at run time, when the cast (MyClass) owner throws a ClassCastException. With an interface type the same mistake is a compile error, found before the program runs.
Go deeper
- Rule of thumb: make wrong code impossible to compile rather than hoping to catch it in testing.
The same loop has to work for an array list and a linked list.
Iterator i = l.get_iterator();
while (i.has_next()) {
Object o = i.get_next();
}A
B
CAn iterator is an object that walks a collection one element at a time, through two operations: is there a next one? and give me the next one. The list hands out the iterator and keeps its internals private, so the client loop does not depend on how the list is stored, and the storage can change freely.
Go deeper
- The iterator remembers a position: a node for a linked list, an index for an array.
What goes wrong if the list itself keeps the position?
A,B
A,CA,A A,B A,C B,A B,B B,C C,A C,B C,CAsk the list for a fresh iterator for every loop: j = l.get_iterator() inside the outer loop. Each iterator object holds its own position. If the position were a field of the list, the inner loop would use up the outer loop's place.
Go deeper
- Two iterators on one list is exactly what "export a fresh object" buys you.
"The new Java for over lists implicitly constructs and uses an iterator."
Asha Ravi Asha RaviThe compiler rewrites for (T x : c) into a loop over c.iterator() using hasNext() and next(). Your class only has to implement Iterable<T>, whose single method iterator() returns a java.util.Iterator<T>. Arrays are handled separately.
Go deeper
- The lecture's own
Iterator(has_next,get_next) is a teaching version; the real interface useshasNextandnext(and an optionalremove).
Iterable and Iterator?An iterator remembers a position in a list.
Exception in thread "main" java.util.ConcurrentModificationException[Ravi, Meena]The loop is an iterator in disguise. If the list is changed behind its back (names.remove(s)), its remembered position may be wrong, so the standard iterators detect it and fail fast with ConcurrentModificationException. Remove through the iterator (it.remove()) or collect and remove after the loop.
Go deeper
- Extra: it is a safety net, not a guarantee for threads.
append loops for (m = head; m.next != null; m = m.next). Where does it break?Run it on a list with nothing in it.
for (m = head; m.next != null; m = m.next) {} // head is null on an empty listException in thread "main" java.lang.NullPointerException: Cannot read field "next" because "<local2>" is nullOn an empty list head is null, so m.next dereferences null and throws a NullPointerException. The empty list is a special case: if head == null make the new node the head, otherwise walk to the last node.
Go deeper
- Habit: for every list or tree code, test empty, one element, many.
append O(1)?Week 4 Summary Sheet
Week 4 on one page: a lecture-by-lecture glance, key terms, the pairs to tell apart, the errors you can now read, a checklist and corrections to the source notes.
8.1Week 4 at a glance
abstract methods force subclasses; no objects, but variables of that type. Generic quicksort(Comparable[]). An interface is purely abstract: extend one class, implement many.
A slice of capabilities. Static methods (not inherited) and default methods (may override). Conflicts: write your own; class wins over an interface.
A class nested inside another, marked private: hides the implementation (Node in LinkedList). Inner and outer see each other's private members.
Private class + public interface: login returns a QIF. The object keeps state (numQueries). Update a date as a whole.
A spawned object calls its owner back. Timer(MyClass) is too specific, Timer(Object) needs a cast, Timer(TimerOwner) is generic and safe.
An exported object with has_next / get_next remembers a position. One iterator per loop. For-each builds an iterator for you.
8.2Key terms, lecture by lecture
- abstract method
- Signature without a body; subclasses must implement it.
- abstract class
- Class with an abstract method (or declared so); cannot be instantiated.
- Comparable
- Capability: can be compared with another of its kind.
- interface
- Purely abstract class; implemented, not extended.
- implements
- A class provides the code for an interface's methods.
- static method
- Belongs to the interface; no instance variables; not inherited.
- default method
- Interface method with a body; may be overridden.
- class wins
- A superclass method beats an interface default.
- nested class
- A class declared inside another class.
- inner class
- A non-static nested class, tied to an outer object.
- static nested
- Not tied to an outer object.
- Outer.this
- The enclosing object, from inside an inner class.
- accessor / mutator
- Method that reads / changes private data.
- data integrity
- The object stays in a valid state.
- QIF
- The public interface behind which the private query object hides.
- final field
- Assigned exactly once, e.g. in the constructor.
- callback
- A call from a spawned object back to its owner.
- Runnable
- Interface of things that can run in parallel; asks for
run(). - owner
- The object notified; typed as the interface
TimerOwner.
- Iterator
- Interface with
has_next()andget_next(). - position
- What an iterator remembers: node or index.
- Iterable
- Standard interface that lets for-each work (extra).
- fail-fast
- Standard iterators throw if the list changes under them (extra).
8.3The pairs you must tell apart
8.4Messages and errors you can now read
| What went wrong | What you will see | When |
|---|---|---|
new on an abstract class | error: Shape is abstract; cannot be instantiated | compile time |
| abstract method in a non-abstract class, or a missing implementation | error: Square is not abstract and does not override abstract method perimeter() in Shape | compile time |
| interface method implemented with weaker access | attempting to assign weaker access privileges; was public | compile time |
| extending two classes | error: '{' expected | compile time |
| two interfaces with the same default | error: types I1 and I2 are incompatible; | compile time |
| static interface method through the class or an object | error: cannot find symbol | compile time |
| assigning an interface constant | error: cannot assign a value to static final variable MAX | compile time |
| interface field without a value | error: = expected | compile time |
| method with no return on some path | error: missing return statement | compile time |
| double stored in an int | error: incompatible types: possible lossy conversion from double to int | compile time |
| outside code naming a private nested class | error: LinkedList.Node has private access in LinkedList | compile time |
| calling through a private class type | error: ... is defined in an inaccessible class or interface | compile time |
| top-level class marked private | error: modifier private not allowed here | compile time |
| final field never given a value | error: variable QLIM not initialized in the default constructor | compile time |
class says implements Runnable but has no run() | error: Timer is not abstract and does not override abstract method run() in Runnable | compile time |
| cast of an owner to the wrong class | java.lang.ClassCastException: class Other cannot be cast to class MyClass | run time |
| invalid date | java.lang.IllegalArgumentException: invalid date 31-4-2024 | run time |
| appending to an empty linked list with the lecture's loop | java.lang.NullPointerException: Cannot read field "next" because "<local2>" is null | run time |
| inner loop tests the wrong variable | java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3 | run time |
| removing from a list inside for-each | java.util.ConcurrentModificationException | run time |
8.5Last-minute checklist
- abstract method: no body, forces subclasses
- abstract class: no objects, variables fine
- generic code against
Comparable - one parent class, many interfaces
- methods are public abstract
- static: not inherited; default: may override
- two defaults: override yourself
- class beats interface
- nested class hides the implementation
- inner and outer see each other's privates
- top-level class cannot be private
- static nested if no outer object is needed
- private data, public methods
- update a date as a whole, validate
- login returns the interface, not the class
- object keeps its own counter
- owner passes
thisto the worker - worker calls
owner.timerDone() - type the owner as an interface
- owner need not be the creator
- iterator object remembers a position
- one fresh iterator per loop
- private Iter, public Iterator
- for-each = iterator behind the scenes
8.6Corrections made to the source notes
| Where | The source says | Correct |
|---|---|---|
| Lecture 1, the loop | sizearr[i] = shapearr[i].perimeter() into an int array, no semicolon | double[] sizearr and a semicolon (checked by running) |
Lecture 1, cmp | only the if and comments | must return an int on every path |
| Lecture 1, interfaces | "a class that extends an interface" | a class implements an interface |
Lecture 3, head() | removes the first node but keeps size | size-- |
Lecture 4, login | returns only if the login is valid | needs return null; (checked by running) |
Lecture 4, QLIM | private final int QLIM; with no value | assign it in the constructor (checked by running) |
Lecture 5, Timer | implements Runnable with a method start() | needs run() (checked by running) |
| Lecture 6, loops | m !+ null, i < data.length in the inner loop | m != null, j < data.length |
Lecture 6, linked append | walks from head with no empty case | handle head == null (NullPointerException, checked by running) |
no objects, variables ok
generic Comparable
static, default
class wins
see each other's private
inner vs static
QIF, counter state
validate as a whole
TimerOwner interface
no cast needed
one per loop
for-each uses it