Phase 1 · Objects
A square is not a rectangle
By the end of this page you'll write one line of code that runs a different method for every kind of object it meets — and you'll know why a square that inherits from a rectangle gets five times four wrong.
Shout "dinner's ready!" up the stairs. Your little brother comes thundering down. The dog gets there first. Your sister texts back "5 mins". One sentence from you, three completely different answers. And you didn't need to know who was upstairs to say it. You said one thing, and each of them knew what it meant for them. Java has exactly that trick, and it's the one this chapter is about.
So far every method call you've written had one answer. You called doubled(21) and the one method called doubled ran. Today you'll write a single line that runs a different method each time round a loop. Java picks which one while the program runs, by asking the object.
That one trick also pays off three IOUs from 1.2. Where do the starter methods come from? What is a "supertype"? And why did a set ignore an equals that worked fine when you called it yourself?
Fair warning about one thing coming later. Every square is a rectangle — ask any maths teacher. Halfway through this chapter you'll write that sentence in Java. Then a program will tell you, calmly and with no error, that five times four is sixteen.
What you'll be able to do
- Write an
interface, and two classes that keep its promise in their own ways. - Say which decision
javacmakes about a call, and which one Java makes while running. Then use that to explain 1.2's ignoredequals. - Spot an
extendsthat is going to break, and build with "has-a" instead. - Make a class
final, so nobody can extend it, and say when you'd want that.
Section 1One promise, two ways to keep it
Here's a job for a program. You've got some shapes — a circle, a square — and you want the area of each. A circle's area is π × radius × radius. A square's is side × side. Two different sums.
So far, you'd write two methods with two names, circleArea and squareArea. Then every bit of code that handles shapes has to ask "which kind is this?" before it can pick one. Add a triangle and you'd hunt down every one of those questions.
The plug socket on your wall makes a promise. Anything with this shape of plug gets power. That's all it promises. It doesn't know whether you'll plug in a kettle or a phone charger. It doesn't need to. The kettle boils water and the charger charges, each in its own way. The socket only fixed the shape of the deal.
Java lets you write the socket. It's called an interface: a named list of methods that a class can promise to have. No code inside, only the promise. Here's the whole of src/Shape.java.
interface Shape {
double area();
}Line 1 says "here is an interface called Shape". It looks like a class, with interface where class would be. The { opens the list of promises.
Line 2 is the one promise. It reads like the top line of a method from 0.4. It hands back a double and is called area, with empty brackets because it needs nothing handed in. But where the body in { } should be, there's only a ;. That's the whole point. The interface says there will be an area. It doesn't say how to work one out.
Line 3 closes it. Three lines, and not one of them does any arithmetic.
Keeping the promise
Now a class that keeps it. This is src/Circle.java, without its header comment.
class Circle implements Shape {
private final double radius;
Circle(double radius) {
this.radius = radius;
}
double radius() {
return radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}Line 1 has the new word. implements Shape means "this class keeps every promise Shape makes". From now on, javac holds you to that. A Circle must have an area method, or it won't compile.
Line 2 is a field, private final, exactly as 1.2 built Money's. Lines 4 to 6 are a constructor like Dog's in 1.1. Lines 8 to 10 are a getter. That's 1.2's word for a method whose only job is to hand back a field.
Lines 12 to 15 keep the promise. Two words in them need a moment: line 13's public and line 12's @Override.
public is required. Every method an interface promises is automatically public, open to anybody. A class can't keep that promise less openly than it was made. That's the same rule 1.2 met with equals. Leave public off and javac refuses. Its message ends attempting to assign weaker access privileges; was public.
@Override is the word 1.2 put above equals. There it meant "this replaces a starter method, so refuse to compile if it doesn't". It works for an interface's promise too. It tells javac "this is the area that Shape asked for", so a misspelt aera gets caught. The @ sign marks it as a tag for the compiler rather than code. Tags like that are called annotations, and 1.6's Section 3 is about them.
Line 14 is the actual sum. Math.PI is a ready-made constant, 3.14159…, sitting in Java's Math class. Times the radius, times the radius again.
src/Square.java is the same shape of file, with side instead of radius. Its area hands back side * side. It also has a method called toString, which you can ignore until Section 3.
What if a class says implements Shape and then forgets the method? Delete the four area lines from Square.java and compile.
$ javac Square.java
Square.java:5: error: Square is not abstract and does not override abstract method area() in Shape
class Square implements Shape {
^
1 errorRead it slowly. "Does not override abstract method area() in Shape" means "you promised an area and there isn't one". abstract is Java's word for "a promise with no code behind it yet". Shape's area is abstract, because it ends in ; instead of a body. Ignore "Square is not abstract" for now. It's offering a way out you don't want, and the box at the end of Section 3 explains it. Put the lines back before you go on.
One line, two methods
Here's the program that uses all three files, src/Lab1_OneCall.java.
Shape[] shapes = { new Circle(1), new Square(2) };
for (Shape s : shapes) {
System.out.println(s.area());
}Line 1 makes an array from 0.5, with two slots. Each slot is labelled Shape. From here on, "label" means the type written on a box or slot, like the Shape in Shape s. It no longer means the name. Into the first goes a ticket for a new circle of radius 1. Into the second goes a ticket for a new square of side 2. That's allowed, because a Circle is a Shape. It said so with implements. So is a Square.
Line 2 is 0.5's for loop, visiting each slot in turn. Each time round, the box s holds the next ticket. And look at s's type. It's Shape, not Circle or Square.
Line 3 is the interesting one. It calls s.area() and prints the answer. There is exactly one call to area in this whole program. And Shape's area has no code in it at all.
Two lines print. What are they? A compile error is a perfectly fair guess. Shape's area() is a promise with nothing in it, and s is a Shape. If you think it runs, what are the two numbers?
$ java Lab1_OneCall
3.141592653589793
4.0It compiled, it ran, and it got both answers right. π for a circle of radius 1. Four for a square of side 2.
If you guessed a compile error, that was a careful guess. You noticed the promise has no code. But a promise doesn't have to have code. It only has to be kept, and every object in that array keeps it.
So one line of code, s.area(), ran two different methods. First time round, the circle's. Second time round, the square's. The loop never asked "which kind is this?". Each object knew how to answer for itself.
That's the idea this whole chapter hangs on. One call, and each object answers it in its own way. It has a name: polymorphism, which is Greek for "many shapes". It's a grand word for the dinner shout.
Here's what it buys you. Add a Triangle that implements Shape and put one in the array. The loop doesn't change by a single character. Your turn, at the end of the chapter, has you do exactly that.
One more thing to try. Lab 1 has a switched-off line at the bottom: Shape blank = new Shape();. Take its // off and compile.
$ javac Lab1_OneCall.java
Lab1_OneCall.java:14: error: Shape is abstract; cannot be instantiated
Shape blank = new Shape(); // take the // off: can you build a promise?
^
1 errorYou can't build a promise. "Instantiated" means "built as an object", and an interface has no code to build one from. What would blank.area() even do? So Shape can be the label on a box, but never the thing the ticket finds. Put the // back.
- An interface is a list of promised methods with no code:
double area();ends in;, not a body. implements Shapemeans "this class keeps every promise inShape".javacchecks that it does.- A promised method must be
publicin the class that keeps it.@Overrideasksjavacto check you got it right. - Polymorphism: one call,
s.area(), runs a different method depending on the object. The calling code never asks which kind it has.
Section 2The label and the object
How did Java know which area to run? s is labelled Shape, and Shape has no code. Something had to make a choice.
Here's the key fact. Every method call is really two questions, asked at two different times, by two different programs. Mix them up and nothing about this chapter will make sense. Keep them apart and almost everything will.
A supply teacher has a register that says "Year 9 student" next to every name. They may ask anyone on it to read a paragraph aloud, because every student can read. They may not ask "play us your violin". Maybe the kid in the corner plays brilliantly. The register doesn't say so, and the register is all the teacher can see. But when they ask "read aloud", each kid reads in their own voice.
Question one: are you allowed to ask? javac answers that, while compiling. It looks only at the label on the box — the type you declared it with. javac never sees an object, not one, ever. Objects don't exist until the program runs, and by then javac has finished.
Question two: who answers? The java program answers that, while running. It follows the ticket to the actual object on the heap, and asks it.
src/Lab2_TheLabel.java puts both questions side by side.
Circle c = new Circle(2);
Shape s = c;
System.out.println("A: " + c.radius());
System.out.println("B: " + s.area());
// System.out.println("C: " + s.radius()); // same object. Same answer?Line 1 builds a circle of radius 2 and puts its ticket in c, a box labelled Circle. Line 2 copies that ticket into s, a box labelled Shape. That's 1.1's Dog b = a; again. Two boxes, two copies of one ticket, one circle on the heap.
Line 4, c.radius(), asks the circle for its radius, through c. Line 5, s.area(), asks for its area, through s. Both run fine and print A: 2.0 and B: 12.566370614359172, which is π × 2 × 2.
Line 6 is switched off. Its s.radius() asks the same circle for its radius, through s this time. The object really does have a radius() method — line 4 just used it. Take the // off and compile.
$ javac Lab2_TheLabel.java
Lab2_TheLabel.java:15: error: cannot find symbol
System.out.println("C: " + s.radius()); // same object. Same answer?
^
symbol: method radius()
location: variable s of type Shape
1 errorLook at the last line: variable s of type Shape. javac tells you exactly where it looked. It read the label, found that Shape promises only area, and refused. The circle at the end of the ticket was never consulted. It couldn't be. This is the violin. Put the // back.
Now question two, which happens while the program runs. Step through Lab 1's loop.
What javac sees
Heap
Step 1 — compiling. javac reaches s.area(). It checks the label: s is a Shape, and Shape promises area(). Allowed. So it writes into the compiled file: "call the area() that Shape promised, on whatever s holds". It has not picked the circle's or the square's. It couldn't, because it doesn't know what s will hold.
Stack — main's plate
Heap
Step 2 — running, first time round. Now the program runs. Both objects exist, and s holds a ticket for #1. Look at the heap. Every object carries one thing you never asked for. When new Circle(1) built it, Java wrote a hidden note inside: made by Circle. Every object knows which class built it, for its whole life.
Stack — main's plate
Heap
The classes
Step 3 — who answers? Java follows s's ticket to #1 and reads the note: Circle. So it goes to the Circle class and finds its area. That one runs, with radius being #1's own field. Out comes 3.141592653589793.
Stack — main's plate
Heap
The classes
Step 4 — second time round. The loop puts #2's ticket in s. Same line of code, same compiled instruction. Java follows the ticket again, and this time the note says Square. So Square's area runs, and out comes 4.0. The line never changed. The object did.
That run-time choice — follow the ticket, read the note, run that class's version — is called dynamic dispatch. "Dynamic" because it happens while the program runs. "Dispatch" because a call gets sent to the right place, like a parcel. A call that works this way is often called a virtual call.
The label decides what you may ask. The object decides who answers. javac checks the first, from the type of the box. java settles the second, from the note inside the object.
1.2's ignored equals, explained
Now you can settle a mystery from 1.2. There, an equals written as equals(Money that) worked when you called it yourself. Then a HashSet skipped it completely. 1.2 promised this chapter would explain why. Here's the same bug with the set taken out, in src/Lab3_WhichEquals.java.
static class Coin {
private final int pence;
Coin(int pence) {
this.pence = pence;
}
public boolean equals(Coin that) { // the tempting version from 1.2
return this.pence == that.pence;
}
}A coin holds a number of pence, private final, filled by the constructor. Then lines 8 to 10 are the tempting equals from 1.2. It takes a Coin, not an Object, so it needs no instanceof and no cast. It compares the pence with ==, which is safe on a plain int.
Coin a = new Coin(5);
Coin b = new Coin(5);
Object o = b; // same ticket, plainer label
System.out.println("A: " + a.equals(b));
System.out.println("B: " + a.equals(o));
System.out.println("C: " + (o == b));Lines 1 and 2 build two five-pence coins. Line 3, Object o = b;, copies b's ticket into a box labelled Object. That's 1.2's word for "any object at all". Line 5, a.equals(b), asks whether a equals b. Line 6, a.equals(o), asks whether a equals o. Line 7, (o == b), checks that o and b really hold the same ticket.
So b and o find the very same coin. The only difference is the label on the box. Surely that can't change an answer?
Three lines, A, B and C. C is a warm-up. A and B hand the same coin to the same method name. Same answer, or not?
$ java Lab3_WhichEquals
A: true
B: false
C: trueSame coin, opposite answers. C confirms that o and b hold one ticket. Yet A says equal and B says not. Changing nothing but a label changed the result.
If that feels like Java cheating, you're in good company. Run both questions over it and it comes apart.
Here's the thing to notice first. equals(Coin that) and the starter equals(Object other) are two different methods. They share a name, but their brackets differ. That's 0.4's overloading: one name, several parameter lists. Writing equals(Coin) didn't replace the starter. It added a second equals beside it. So a Coin has both.
Now ask the two questions about each line.
- Case A,
a.equals(b). Question one, at compile time:bis labelledCoin, and there's anequalsthat takes aCoin. That's the closest fit, sojavacwrites down "callequals(Coin)". Question two, at run time: run the coin'sequals(Coin). It compares pence.true. - Case B,
a.equals(o). Question one:ois labelledObject.equals(Coin)can't take anObject, because not every object is a coin. Onlyequals(Object)fits, sojavacwrites down "callequals(Object)". Question two: Java followsa's ticket and reads the note, Coin. DoesCoinhave its ownequals(Object)? No. So the starter one runs. It asks "same object?", andaandbare two coins.false.
So the choice happens in two stages. javac picks which method, from the labels in the brackets. Java picks whose version of that method, from the object. Dynamic dispatch only ever does the second job.
And that is 1.2's HashSet, exactly. The set's code was compiled years ago, by people who'd never heard of your Money. Inside, every item is labelled Object. So its equals calls are all case B. They ask for equals(Object), every time. Your equals(Money) was a different method, and nothing in the set could ever ask for it.
The fix is the one 1.2 used. Write equals(Object other), so yours is the method the set asks for. Then dynamic dispatch finds your version on the object, and runs it. And put @Override on top, so javac stops you if you get the brackets wrong.
Going deeper See the two decisions in the compiled file
You don't have to take the stepper's word for it. javap -c prints the instructions javac wrote into a .class file. It comes with the JDK. Here's the one call in Lab 1.
$ javap -c Lab1_OneCall | grep invokeinterface
54: invokeinterface #25, 1 // InterfaceMethod Shape.area:()Djavac wrote "call Shape.area, taking nothing, handing back a double". That's the ()D at the end — D is short for double. No mention of Circle or Square anywhere. The choice of class is left to run time, which is what invokeinterface means.
Now Lab 3's two equals calls.
$ javap -c Lab3_WhichEquals | grep equals
25: invokevirtual #18 // Method Lab3_WhichEquals$Coin.equals:(LLab3_WhichEquals$Coin;)Z
41: invokevirtual #32 // Method java/lang/Object.equals:(Ljava/lang/Object;)ZThere's question one, frozen into the file. Case A's call takes a Coin in its brackets. Case B's takes an Object. Z means boolean, because B was already taken by byte. javac fixed which method each line calls, and nothing at run time can swap one for the other.
invokevirtual is the run-time half. It means "find this method on the object's own class, and if it isn't there, keep looking upwards". Section 3 is about what "upwards" means.
How fast is it? In HotSpot, the usual JVM, each object's hidden note is a pointer to its class's data. Each class keeps a table of its methods, with each method in a fixed slot. So a virtual call is: read the note, look in one slot, jump. The JIT often removes even that, once it sees only one class ever turns up at a call. Phase 4 opens that up.
You write Object o = new Square(2); and then System.out.println(o.area());. What happens?
- Every call is two questions. Allowed? — answered by
javac, from the label. Who answers? — answered by Java while running, from the object. - Every object carries a hidden note naming the class that built it. Dynamic dispatch follows the ticket, reads the note, and runs that class's version.
javacpicks which method, by name and by the labels in the brackets. Java only picks whose version of it.equals(Money)is an overload, a second method. Code that asks forequals(Object)never reaches it. That's why 1.2's set ignored it.
Section 3The family tree
Two words in Lab 3 should have bothered you. "Does Coin have its own equals(Object)? No. So the starter one runs." Runs from where? Coin's file never mentions one.
1.2 kept promising this chapter would say where starter methods come from. Here's the experiment, src/Lab4_Starters.java. Remember that Square.java has a toString method and Circle.java doesn't.
Shape c = new Circle(1);
Shape s = new Square(2);
System.out.println("D: " + c);
System.out.println("E: " + s);
System.out.println("F: " + c.equals(new Circle(1)));Lines 1 and 2 build one of each shape. Lines 4 and 5, "D: " + c and "E: " + s, glue an object straight onto some text with +. When + meets an object and a piece of text, Java asks the object for a text version of itself. It does that by calling the object's toString method. You met toString in 0.6, on a StringBuilder. Neither line names it, but it gets called. Line 6, c.equals(new Circle(1)), asks whether a circle equals another circle of the same size.
Here's the method in Square.java that's been waiting since Section 1.
@Override
public String toString() {
return "a square with side " + side;
}Line 1 says this replaces something. Line 2 says it's public, hands back a String, and is called toString. Line 3 builds a sentence from the square's own side field and hands it back.
Three lines, D, E and F. E should be easy. For D: Circle.java has no toString at all. Compile error, a blank, or something else? And for F, you've seen this one before — remember 1.2.
$ java Lab4_Starters
D: Circle@6b95977
E: a square with side 2.0
F: falseD printed something, even though nobody wrote a toString for circles. It's the class name, an @, and a number in hexadecimal. That's a way of writing numbers with the digits 0–9 and a–f. Your number may be different. It's the circle's starter hash code, the "serial number" from 1.2's post room.
E is Square's own sentence. F is false, because Circle has no equals of its own either. So the starter one ran: "same object?". Two circles, so no.
So Circle has a toString, an equals and a hashCode, and its file contains none of them. Something gave them to it.
Here's where they come from. In Java, every class has exactly one parent class. The parent's methods belong to the child too, without the child writing a line. Getting methods from a parent like that is called inheritance. You name a class's parent with the word extends.
And if you don't name one? Then Java names it for you. class Circle implements Shape really means this:
class Circle extends Object implements Shape {Object is the class at the very top. It's 1.2's word for "any object at all", and now you know why that works. Every class is Object's child, or its grandchild, or further down. So every object anywhere is an Object. That's what lets a HashSet hold anything, and println print anything.
Object is where the starter methods live: toString, equals, hashCode and a few more. That's the family tree 1.2 kept pointing at.
You inherit your grandmother's recipe book. You can cook everything in it without writing a single recipe. But say you write your own lasagne on a card and tuck it in at the front. That's the one you make. When someone asks for lasagne, you check your own cards first. Only if there's no card do you open her book.
That's exactly how dynamic dispatch looks a method up. Java follows the ticket and reads the note, Circle. It looks in Circle for toString. Not there. So it goes up one level to the parent, Object, and finds the starter version. That one prints the class name and the hash code. For the square, the search stops early, because Square has its own card at the front.
Writing your own version of a method you'd otherwise inherit is called overriding it. That's the word in @Override. 1.2 said "replace", and that's what it means. 1.2's error said "does not override or implement a method from a supertype". A supertype is anything above you in the tree. That's your parent class, its parent, all the way up to Object — plus any interface you implement.
A class can extend exactly one parent. But it can implement as many interfaces as it likes, with commas: implements Shape, Comparable. Comparable is an interface Java itself provides. It promises "I can be compared with another of my kind". One recipe book to inherit from, but as many sockets as you like to fit. An interface hands down no code, so there's nothing that could clash.
Going deeper Proof that Circle really has a parent
Circle.java doesn't mention Object. But its constructor does something you never wrote. Its very first act is to run the parent's constructor, so the Object part of the circle gets built first. Ask javap to show the compiled constructor.
$ javap -c Circle | grep Object
1: invokespecial #1 // Method java/lang/Object."<init>":()V<init> is the compiled name for a constructor. So the circle's constructor starts by calling Object's. Section 4 shows you the word for that call, super, written out by hand.
Going deeper Why the error said "Square is not abstract"
Section 1's error had an odd first half: "Square is not abstract and does not override…". That's javac offering a second way out. Write abstract class Square, and the class is allowed to leave a promise unkept.
The price is that nobody may build one, exactly like Shape. An abstract class sits halfway between a class and an interface. It can have fields and real methods to inherit. It can also have some methods with no body, which its children must supply.
Reach for one only when the children genuinely share code. If all they share is a promise, an interface says it more honestly. That's the choice 1.7 makes. There you'll build a JSON library whose six kinds of value share no code at all.
- Every class has exactly one parent, named with
extends. If you don't name one, it'sObject. - Inheritance: a child class gets every method its parent has, without writing them.
Objectis the top of every family tree. It's where the startertoString,equalsandhashCodecome from.- Overriding means writing your own version of an inherited method. Dynamic dispatch checks the object's own class first, then walks up the tree.
- A supertype is anything above you: every parent up to
Object, plus every interface you implement.
Section 4When inheritance is wrong
Inheritance looks like free code. Write a class once, and every child gets all its methods for nothing. So the obvious move is to use it everywhere two things are related. Every square is a rectangle. So surely Square extends Rectangle, and Square gets the area sum for free?
Let's try it. src/Lab5_SquareRectangle.java has its own Rectangle and two kinds of square, tucked inside it. They have nothing to do with Square.java. And one warning first. This Rectangle can change size after it's built. 1.2 would frown at that. Hold that thought, because it turns out to be the cause.
static class Rectangle {
private double width;
private double height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
void setWidth(double width) {
this.width = width;
}
void setHeight(double height) {
this.height = height;
}
double area() {
return width * height;
}
}Lines 2 and 3 are two fields, private but not final, so they can change. Lines 5 to 8 are an ordinary constructor. Lines 10 to 16 are two methods that change one field each. A method whose job is to change a field like that is a setter, the opposite of a getter. Lines 18 to 20 work out the area.
Version 1: inherit everything
static class LazySquare extends Rectangle {
LazySquare(double side) {
super(side, side);
}
}Line 1 says a LazySquare is a Rectangle. It inherits all of it: the fields, both setters and area. Lines 2 to 4 are its only code, a constructor.
Line 3 has a new word. super(side, side) means "run my parent's constructor, handing it these values". The parent here is Rectangle, so this builds a rectangle part that's side wide and side high. A child's constructor must start this way. The parent's fields need filling in first, and only the parent's constructor knows how. That's the call javap showed you in Section 3, only now you're writing it.
Version 2: keep it square
LazySquare has a problem you might already see. It inherited setWidth, which changes the width and leaves the height alone. So the careful version overrides both setters.
static class Square extends Rectangle {
Square(double side) {
super(side, side);
}
@Override
void setWidth(double side) {
super.setWidth(side);
super.setHeight(side);
}
@Override
void setHeight(double side) {
super.setWidth(side);
super.setHeight(side);
}
}Lines 1 to 4 are the same as LazySquare. Lines 6 to 10 override setWidth, so a square stays square. super.setWidth(side) with a dot means "run my parent's version of setWidth". That's needed, because a bare setWidth here would call this method again, forever — 0.4's ForeverRecursive. So line 8 sets the width the rectangle way, and line 9 sets the height to match. Lines 12 to 16 do the same for setHeight.
The method that trusts rectangles
Last, a method written by someone who has never heard of squares.
static double makeFiveByFour(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
return r.area();
}
public static void main(String[] args) {
LazySquare lazy = new LazySquare(3);
lazy.setWidth(5);
System.out.println("G: lazy square after setWidth(5): " + lazy.area());
System.out.println("H: rectangle made five by four: " + makeFiveByFour(new Rectangle(1, 1)));
System.out.println("I: square made five by four: " + makeFiveByFour(new Square(1)));
}Lines 1 to 5 take any Rectangle, make it 5 wide and 4 high, and hand back its area. Whoever wrote this expects 20. So would you.
In main, lines 8 to 10 build a lazy 3 by 3 square. They set its width to 5 and print its area. Line 12 hands makeFiveByFour an ordinary rectangle. Line 13 hands it the careful square. That's allowed, because a Square is a Rectangle — it said so with extends.
This is the result promised at the top of the page. It catches people who've been programming for years, so commit to three numbers before you look.
Three areas, G, H and I. What's the lazy square's area after setWidth(5)? And what does makeFiveByFour hand back for the rectangle, and for the careful square?
$ java Lab5_SquareRectangle
G: lazy square after setWidth(5): 15.0
H: rectangle made five by four: 20.0
I: square made five by four: 16.0I says five by four is sixteen. No error, no warning. A believable wrong number, which 1.2 called the worst kind of bug.
Follow it through with dynamic dispatch. r is labelled Rectangle, but the ticket finds a Square. So r.setWidth(5) runs Square's setWidth, which makes it 5 by 5. Then r.setHeight(4) runs Square's setHeight, which makes it 4 by 4. Area 16. Every line did what it was written to do.
And G is the other way to lose. The lazy square inherited a setWidth that doesn't know about squares. Now it's 5 by 3. A "square" with an area of 15.
Look at the trap you're in. Inherit the setters unchanged, and your squares stop being square. Override them, and code written for rectangles quietly gets wrong answers. There's no third version of Square extends Rectangle that escapes. The bug isn't in any line. It's in the claim.
In maths, "a square is a rectangle" means every square has a rectangle's properties. In code, extends makes a much bigger promise. It says: you can hand this to any code written for the parent, and it will behave like the parent. A Rectangle behaves like this: change the width, and the height stays put. makeFiveByFour relied on that, and was right to. A square can't keep that promise and stay a square.
Before writing B extends A, picture some code written for A, by someone who has never heard of B. Ask: could I hand that code a B, and nothing would surprise them? If you can name one surprise, the answer is no. "B is a kind of A" in everyday English is not enough.
Notice what the trap needed: setters. With 1.2's immutable objects there are none. A rectangle that can never change size makes no promise about changing size, so a square can't break it. Immutability doesn't make every extends safe. But it removes a whole family of these surprises.
What to do instead
Your src/ already has the answer. Square.java doesn't extend any rectangle. It implements Shape, and so would a Rectangle. Two classes side by side, each keeping the same promise its own way, neither one the other's parent. Nobody can hand a square to rectangle code, so nobody gets surprised.
And if Square wants to reuse a rectangle's code? Then it can hold one, instead of being one.
final class Square implements Shape {
private final Rectangle inside;
Square(double side) {
this.inside = new Rectangle(side, side);
}
public double area() {
return inside.area();
}
}Line 2 is a private field holding a rectangle. Lines 4 to 6 build one with equal sides. Lines 8 to 10 keep Shape's promise by asking the rectangle inside, and handing its answer back. Nothing outside can reach inside, so nothing outside can stretch it.
This is called composition: building a class out of other objects it holds, rather than inheriting from a parent. People sum it up as has-a instead of is-a. A square has a rectangle inside it, doing the sum. It doesn't claim to be one. That leaves one word in the sketch: the final on line 1.
final on a class: nobody may extend this
1.2 put final on fields: fill once, then never again. On a class it means something new. A final class can have no children. Nobody may write extends with its name.
Java's own String is one. That's part of why 0.6 could promise nothing changes a String. src/Lab6_FinalString.java has an attempt to extend it, switched off.
// static class LoudString extends String { } // take the // offIt's a one-line class that tries to be a child of String. Take the // off and compile.
$ javac Lab6_FinalString.java
Lab6_FinalString.java:9: error: cannot inherit from final String
static class LoudString extends String { } // take the // off
^
1 errorWhy would String's authors lock the door? Picture a child of String that adds a field that can change, and overrides a few methods to read it. A child can go anywhere its parent goes. So it could be handed to any code that trusts Strings never to change. Think of 1.2's HashSet, holding Strings. Every promise String made would be broken by a class its authors never saw. final closes that door. Put the // back.
So the rule people use is this. Make a class final unless you've designed it to be a parent. This section showed how hard designing a good parent is. That's why the sketch's has-a Square is final too.
Going deeper The rule has a name: Liskov substitution
The test in the callout is usually called the Liskov substitution principle, after the computer scientist Barbara Liskov. She described it in a 1987 conference keynote, and later in a 1994 paper with Jeannette Wing.
Here's their version, in plain words. Say B is a subtype of A. A subtype is the opposite of a supertype: a class lower in the family tree, like Square under Rectangle. Then anything that's true of every A should be true of every B too. "Setting the width leaves the height alone" is such a property. Square breaks it.
Java can check that a child has the right methods. It can't check that they behave as promised. That part is a promise between people, and extends is where you make it.
Going deeper The fragile parent: when the parent's own code calls yours
There's a second way extends bites, and this one catches experts. Your child class can break because of how the parent was written inside, which you never see.
This is a famous example from Joshua Bloch's book Effective Java, run for real in src/FragileParent.java. Say you want a set of words that counts how many were ever added. The obvious move is to extend HashSet and override its two adding methods.
import java.util.HashSet;
// ... the FragileParent class starts here ...
static class CountingSet extends HashSet<String> {
int added = 0;
@Override
public boolean add(String word) {
added = added + 1;
return super.add(word);
}
// addAll(...) is overridden the same way: add words.size(), then super.addAll(...)
}Line 1 is an import, as in 1.2. It fetches HashSet from Java's toolbox so the file can use it. In CountingSet, add counts one, then lets the real HashSet add it. addAll, which adds a whole collection at once, counts the collection's size, then lets the real one add them. Its brackets hold a wildcard type, ? extends String. It means "a collection of String, or of anything below it in the family tree". 1.4 explains the question mark.
The file also has a CountingBag. It holds a HashSet in a private field, and its addAll calls its own add once per word. Both get three words.
$ java FragileParent
CountingSet says 6 added
CountingBag says 3 addedSix. Three words went in. Here's why. The real HashSet's addAll works by calling add once per word. And dynamic dispatch sends those calls to your add, which counts each one again. Nothing on the page tells you addAll does that. It's a private habit of the parent's code, and a future version of Java would be allowed to change it.
That's the fragile parent. A child is welded to the inside of its parent, not just its promises. The bag only uses the set's promises, so it can't be caught out. Has-a beats is-a again.
Judgment call. A game has a Bird class with a fly() method, which moves the bird upwards. A teammate adds Penguin extends Bird and overrides fly() to do nothing, because penguins can't fly. What do you say?
super(...)in a constructor runs the parent's constructor first.super.method()runs the parent's version of a method.- In code,
B extends Apromises that a B can stand in anywhere an A is used, and behave like one. Everyday "is a" isn't enough. Square extends Rectanglebreaks that promise, as soon as rectangles can change size: five by four came out as 16.- Composition — has-a instead of is-a — reuses another class's code without making its promises. When in doubt, hold it rather than extend it.
- A
finalclass can have no children. Make classesfinalunless they're designed to be parents.Stringis one.
Everything is in this chapter's src/ folder. Compile it all once with javac *.java, then run each program. Some steps ask you to take a // off a line. Put it back afterwards, or the next javac *.java will stop on it. Predict on paper before each run. A wrong prediction points straight at one wrong idea, and now you know which one.
Section 5Check yourself
You write Shape s = new Circle(3); and then double r = s.radius();. Circle really does have a radius() method. What happens?
Judgment call. You need a Cube for a 3D game. A teammate suggests Cube extends Square, since every face of a cube is a square. Cube would override area() to hand back all six faces' worth. What's your answer?
In Lab 1, the line System.out.println(s.area()); runs once per shape. Which area method does it call?
Section 6What broke, and what surprised you
Write this down in your own words before moving on. The predictions you got wrong are the useful ones. Each points at one belief you've now replaced.
Worth answering properly, on paper:
- Which predictions did you get wrong? For each one, what were you picturing instead?
- In one sentence each: what does the label decide about a call, and what does the object decide?
- Lab 3 handed one coin to
equalstwice and got two answers. Explain why, without the word "overloading" — then with it. - Circle printed
Circle@…, and nobody wrote it atoString. Where did that method come from, and how did Java find it? - Why does
Square extends Rectanglegive 16? Say which promise broke, and whose.
You can now write one promise and keep it in many ways. You also know when not to reach for extends. In 1.7 you'll use exactly this to build the value tree of a JSON library. Six small final classes keep one interface's promise, and one call prints the whole tree. First, 1.4 looks at the angle brackets in HashSet<String>, and at what the compiler does with them.
Nice. Chapter marked complete.