Generalization and specialization named at last, then two rounds of specialising a library class: a DrawablePolygon that adds colour to java.awt.Polygon, and a RegularPolygon that computes its own vertices with trigonometry. Plus constructor chaining with this(...) and throwing an exception to reject bad arguments. Follows Think Java 2e, Chapter 17 (Advanced Topics), Sections 17.1-17.4, pp. 277-283, cross-referenced against Java SE 21 API — java.awt.Polygon.
Subject: Java · 65 slides · code lesson
Open the interactive version of this deck
Title
Think Java 2e · Chapter 17 · Advanced Topics
Sections 17.1-17.4 · pp. 277-283
Objectives
This lesson follows Think Java 2e, Chapter 17 (Advanced Topics), Sections 17.1-17.4, pp. 277-283. Everything on these slides can be checked against those pages.
1. Distinguish generalization from specialization and give an example of each from earlier chapters.
2. Extend a library class you did not write, adding an attribute and a method.
3. Compute the vertices of a regular polygon from a number of sides and a radius.
4. Chain constructors with this(...) to give parameters default values.
5. Validate constructor arguments and throw an exception when they are invalid.
6. Explain why validation belongs in the most general constructor.
Warm-up
Four things, one from each of the last four chapters.
Discussion prompt
From Lesson 14a: what does super(...) do, and are constructors inherited? From Lesson 11a: what is overloading? From Lesson 16: what does protected mean? And from Lesson 4a: what units do Math.cos and Math.sin take?
Hint: Radians, not degrees.
Answer:
super(...) invokes the superclass constructor, and constructors are not inherited. Overloading is two methods with the same name and different parameter lists. protected means subclasses but not other classes. And the trig functions take radians.
All four are used in the next twenty lines. In this chapter, we'll explore the concept of inheritance more fully and present event-driven programming. We'll continue to develop graphical simulations as a running example, but this time in varying shapes and colors!
Concept
You have used inheritance twice for opposite reasons, and Chapter 17 opens by giving each one a name.
Figure (svg): Two panels contrasting generalization, which pulls shared code up, with specialization, which extends downward
Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 17 (Advanced Topics), Sections 17.1-17.4, pp. 277-283 — Chapter 17 opens on printed page 277.
Section
Chapter opener
Concept
When we first looked at inheritance in Chapter 14, our purpose was to avoid duplicating code. We noticed that decks of cards and hands of cards had common functionality, and we designed a CardCollection class to provide it. This technique is an example of generalization.
generalization — The process of extracting common code from two or more classes and moving it into a superclass.
specialization — Extending a class to add new attributes or methods, or to modify existing behavior.
| generalization | specialization | |
|---|---|---|
| starts from | two or more existing classes | one existing class |
| produces | a new superclass | a new subclass |
| you write | the parent | the child |
| the example | CardCollection, in Chapter 14 | GridCanvas, in Chapter 15 |
| also seen in | Automaton, in Chapter 16 | Conway and Langton, in Chapter 16 |
By generalizing the code, we were able to reuse it in the Deck and Hand classes. Chapter 16 did both in one chapter: generalising Conway and Langton into Automaton, and specialising Canvas into GridCanvas.
Notation
The Chapter 15 case is worth re-reading now, because it makes a claim about library design.
Annotate
paint exists to be overridden — it is a hook, not a finished method.Generalization is something you do to your own code; specialization is often something you do to someone else's. That asymmetry is why the chapter names them separately.
Worked example
Every use of extends in this course is one or the other. Sorting them is a good test of whether the distinction has landed.
class Hand extends CardCollection // Ch.14
class GridCanvas extends Canvas // Ch.15
class Conway extends Automaton // Ch.16
class DrawablePolygon extends Polygon // Ch.17| class | the superclass existed first? | which one |
|---|---|---|
| Hand | no — CardCollection was extracted for it | generalization |
| GridCanvas | yes — Canvas is a library class | specialization |
| Conway | no — Automaton was extracted from it | generalization |
| DrawablePolygon | yes — Polygon is a library class | specialization |
Ask which class came first.
Why: If the superclass was written to hold shared code, it is generalization.
Ask who wrote the superclass.
Why: If it is someone else's, it is almost certainly specialization.
Notice both use extends.
Why: The language has one mechanism; the two names describe intent, not syntax.
Notice Conway is both.
Why: It was specialised from nothing and then generalised into Automaton — over two chapters.
Verify: Take Langton extends Automaton and decide which it is.
Why: Generalization — Automaton was created by extracting Conway and Langton's shared code, even though Langton was written first. The direction of the work is what matters, not the direction of the arrow, which points the same way in both cases.
Definition probe
Ask which class existed first, and who wrote it.
Sort into buckets
Sort each example.
Concept
Two names for one keyword only earn their keep if they lead to different decisions — and they do.
| question | generalization | specialization |
|---|---|---|
| what goes in the superclass? | exactly what the subclasses shared | not your decision |
| can you change the superclass? | yes, it is yours | no |
| what if it does not fit? | restructure it | work around it, or do not extend |
| the risk | over-generalising into a grab bag | depending on behaviour that changes |
The third row is the practical one. When you generalise, a bad fit is a design problem you can fix; when you specialise a library class, a bad fit is a constraint — which is exactly the situation Section 17.7 runs into with DrawablePolygon extends Polygon.
Trap
Inheriting from a class just to get at its methods.
// suppose you want the ArrayList methods:
public class Inventory extends ArrayList<Item> {
// now Inventory has add, remove, get, sort,
// clear, subList, and 30 other methods
}| consequence | detail |
|---|---|
| Inventory IS-A ArrayList | so it can be passed anywhere a list can |
| every list method is public on it | including ones that break your invariants |
| you cannot remove them | inheritance is all-or-nothing |
| is the sentence true? | an inventory is a list? not really |
This is Lesson 14a's trap in a library setting. extends is a claim about what your class is, and a claim you make to get at some methods is still a claim everyone else can rely on.
Specialise when the IS-A is true and the class invites it.
// Polygon IS the thing being drawn - the sentence holds,
// and Polygon's attributes are public by design
public class DrawablePolygon extends Polygon { ... }
// an inventory HAS items - composition, as in Lesson 13b
public class Inventory {
private ArrayList<Item> items;
}| test | DrawablePolygon | Inventory extends ArrayList |
|---|---|---|
| is the IS-A sentence true? | yes | no |
| does every inherited method make sense? | yes | no |
| was the parent designed for it? | yes | not especially |
Both tests have to pass. Chapter 17's DrawablePolygon passes them — a drawable polygon really is a polygon, and every Polygon method still makes sense on it.
Prediction
The direction of the work, not the arrow.
public class Conway extends Automaton { ... }| class | written in |
|---|---|
| Conway | Chapter 15 |
| Automaton | Chapter 16 |
Predict first
Is this generalization or specialization?
Correct: Generalization — Automaton was extracted from Conway and Langton's shared code
Why: Conway existed first; Automaton was created afterwards to hold the duplicated main and mainloop. The extends arrow points the same way in both kinds of inheritance — what distinguishes them is which class was written to accommodate the other.
Matching
Chapter 17's vocabulary, part one.
Match the pairs
Why: The first two are new; the last two are Chapter 16's. Note that generalization is usually a refactoring — it restructures code without changing behaviour — while specialization adds behaviour that was not there before.
Explain it to yourself
You have never read Canvas's source.
Discussion prompt
GridCanvas extends Canvas and overrides paint, and you have no idea what is inside Canvas. Why is that enough?
Hint: What do you actually need to know?
Answer:
Because you only need its public interface — the method names, parameters and return types. Overriding paint requires knowing its signature, not its body.
And the library promises to call it. Canvas was designed so that a subclass's paint is invoked when the component needs drawing; that promise is the contract.
Encapsulation is what makes this possible. Lesson 11a said hiding the implementation buys the freedom to change it — and here you are on the other side of that deal, depending only on what was published.
Section
Section 17.1
Concept
The word polygon means many angles; the most basic polygons are triangles, rectangles, pentagons and so forth. Polygons are an important part of computer graphics because they are used to compose more complex images.
Polygon p = new Polygon();
p.addPoint(57, 110);
p.addPoint(100, 35);
p.addPoint(143, 110);| after | npoints | shape |
|---|---|---|
| new Polygon() | 0 | empty |
| addPoint(57, 110) | 1 | a point |
| addPoint(100, 35) | 2 | a line |
| addPoint(143, 110) | 3 | a triangle |
Java provides a Polygon class (in java.awt) that we can use to represent and draw polygons. A polygon is just an ordered list of vertices — the edges are implied, including the one from the last point back to the first.
Picture it
Internally, Polygon objects have three attributes — and unusually, all three are public.
Figure (svg): Two parallel arrays of x and y coordinates with a count of how many points are in use
When a Polygon is created, npoints is 0 and the two arrays are initialized with length 4. So the array length and the number of points are two different things — which is exactly the distinction an ArrayList hides and this class does not.
Worked example
As points are added, npoints is incremented. If npoints exceeds the length of the arrays, larger arrays are created, and the previous values are copied over (similar to how ArrayList works).
public int npoints; // total number of points
public int[] xpoints; // array of X coordinates
public int[] ypoints; // array of Y coordinates| points added | npoints | array length | what happened |
|---|---|---|---|
| 0 | 0 | 4 | initial |
| 3 | 3 | 4 | room to spare |
| 4 | 4 | 4 | full |
| 5 | 5 | 8 or more | new arrays allocated, values copied |
npoints counts the points in use.
Why: Not the array length — the two diverge immediately.
The arrays start at length 4.
Why: Enough for a quadrilateral without any growth.
Growing means allocating and copying.
Why: Larger arrays are created, and the previous values are copied over.
Which is what ArrayList does.
Why: Lesson 13b's growing collection, now visible from the inside.
Verify: Add five points and reason about how many array objects have existed: at least two pairs.
Why: This is the mechanism behind every growable collection. An array cannot change length, so growing means making a bigger one and copying — which is why ArrayList is not free, and why Lesson 10b's quadratic string concatenation had the same shape.
Prediction
It starts at zero.
Polygon p = new Polygon();
p.addPoint(57, 110);
p.addPoint(100, 35);
p.addPoint(143, 110);| after | npoints |
|---|---|
| new Polygon() | 0 |
| three addPoint calls | ? |
Predict first
What is p.npoints?
Correct: 3
Why: As points are added, npoints is incremented, so three calls give three points — a triangle. Note that p.xpoints.length is still 4, because the arrays start at that length and have not yet needed to grow.
Concept
Lesson 11a argued for private instance variables. Polygon's are public, and it is worth asking why the library made that choice.
| typical class | Polygon | |
|---|---|---|
| attributes | private | public |
| access | through getters | directly |
| can a client corrupt it? | no | yes — set npoints to 99 |
| why | encapsulation | speed, and age |
Polygon is old and performance-sensitive — graphics code touches these arrays in tight loops, and going through getters was once a real cost. It also means RegularPolygon can fill xpoints and ypoints directly, which is what the next section does. A design decision you would not repeat, being relied upon.
Trap
xpoints.length is not the number of vertices.
Polygon p = new Polygon();
p.addPoint(57, 110);
p.addPoint(100, 35);
p.addPoint(143, 110);
for (int i = 0; i < p.xpoints.length; i++) { // 4, not 3
System.out.println(p.xpoints[i]); // prints a stray 0
}| expression | value |
|---|---|
| p.npoints | 3 — the vertices |
| p.xpoints.length | 4 — the capacity |
| p.xpoints[3] | 0 — never set |
The extra 0 is not a vertex; it is unused capacity. A triangle would be drawn as a quadrilateral with a corner at the origin — a bug that looks like a graphics glitch.
Loop to npoints.
for (int i = 0; i < p.npoints; i++) {
System.out.println(p.xpoints[i] + ", " + p.ypoints[i]);
}| bound | iterations | correct? |
|---|---|---|
| p.npoints | 3 | yes |
| p.xpoints.length | 4 | no |
npoints is the count; the array length is the capacity. An ArrayList hides this distinction behind size(); Polygon exposes both, so you have to know which one you want.
Definition probe
Two numbers that are easy to confuse.
Sort into buckets
Sort each expression.
Fill the middle
Three vertices, added in order.
Fill in the blanks
Polygon p = new Polygon();
p.addPoint(57, 110);
p.addPoint(100, 35);
p.addPoint(143, 110);
Why: The constructor takes no arguments and produces an empty polygon; each addPoint appends one vertex and increments npoints. The edges are implied by the order, including the closing edge from the last point back to the first.
Socratic
Point[] points would be more obvious.
Discussion prompt
Polygon stores int[] xpoints and int[] ypoints separately rather than one array of Point objects. What does that buy, and what does it cost?
Hint: How many objects is each design?
Answer:
Two arrays of primitives are two objects; an array of 360 Points is 361. For graphics code touching vertices in tight loops, that difference in allocation and indirection is real.
The cost is readability: xpoints[i] and ypoints[i] are one vertex split across two places, and nothing enforces that the arrays stay the same length.
Parallel arrays are a classic trade: faster and more compact, easier to get out of step. You saw the same shape in Lesson 7b's histogram — and the modern answer is usually the array of objects, unless measurement says otherwise.
Section
Section 17.2
Concept
Specialization is useful for adding new features to an existing class, especially when you can't (or don't want to) change its design. Polygon knows its shape and nothing about colour or drawing itself.
public class DrawablePolygon extends Polygon {
protected Color color;
public DrawablePolygon() {
super();
color = Color.GRAY;
}
public void draw(Graphics g) {
g.setColor(color);
g.fillPolygon(this);
}
}| added | what it is |
|---|---|
| protected Color color | a new attribute |
| a constructor | not inherited — must be written |
| draw(Graphics g) | a new method |
| everything else | inherited from Polygon |
We can extend the Polygon class by adding a draw method and a Color attribute. Ten lines, and the result has every Polygon method plus two things Polygon does not have — which is what minimal additional code looks like.
Notation
Three lines, and two of them restate rules from Chapter 14.
Annotate
color as null.super() initialises npoints, xpoints and ypoints, which is what makes addPoint work on a DrawablePolygon.color is protected, so subclasses can set it directly, which RegularPolygon will do.A default value is a small kindness. Leaving color null would mean g.setColor(null) on any polygon whose colour was never set — an error a long way from its cause.
Worked example
DrawablePolygon has the same attributes and methods that Polygon has, so nothing you learned about Polygon stops working.
DrawablePolygon p = new DrawablePolygon();
p.addPoint(57, 110);
p.addPoint(100, 35);
p.addPoint(143, 110);
p.color = Color.GREEN;| line | comes from |
|---|---|
| new DrawablePolygon() | the subclass's constructor |
| addPoint(57, 110) | inherited from Polygon |
| p.color = Color.GREEN | the subclass's new attribute |
| p.draw(g) | the subclass's new method |
Everything Polygon offered still works.
Why: You can use addPoint as before, or you can directly access npoints, xpoints, and ypoints (since they are public).
Including the methods not yet used.
Why: You can also use methods like contains, intersects, and translate.
Plus the new attribute.
Why: p.color = Color.GREEN; — legal here because the code is setting it from outside...
…which needs a caveat.
Why: color is protected, so this assignment works only from a subclass or the same package. The book's example is in the same package.
Verify: Check that draw uses fillPolygon(this) — passing the object itself to a method that takes a Polygon.
Why: g.fillPolygon(this) is substitution at work. fillPolygon expects a Polygon and receives a DrawablePolygon, which is legal because every DrawablePolygon is a Polygon — Lesson 14a's rule, in the library's own API.
Prediction
DrawablePolygon's constructor calls it first.
public DrawablePolygon() {
super();
color = Color.GRAY;
}| call | initialises |
|---|---|
| super() | ? |
| color = GRAY | the new attribute |
Predict first
What does super() initialise?
Correct: npoints, xpoints and ypoints — Polygon's attributes
Why: The constructor for DrawablePolygon uses super to invoke the constructor for Polygon, which initializes the attributes npoints, xpoints, and ypoints. Without those arrays, addPoint would have nowhere to put a vertex.
Concept
Two lines, and both of them are the graphics idiom from Chapter 15.
public void draw(Graphics g) {
g.setColor(color);
g.fillPolygon(this);
}| call | does |
|---|---|
| g.setColor(color) | sets the pen colour for what follows |
| g.fillPolygon(this) | fills the shape described by this object |
| the parameter | a Graphics — supplied by whoever is drawing |
The same shape as Cell.draw in Lesson 15a: set a colour, draw something, take the Graphics as a parameter. Graphics is stateful — setColor affects every later call — which is why the colour is set immediately before use rather than once at the start.
Trap
No constructor means color is never set.
public class DrawablePolygon extends Polygon {
protected Color color;
// no constructor
public void draw(Graphics g) {
g.setColor(color); // color is null
g.fillPolygon(this);
}
}| after new DrawablePolygon() | value |
|---|---|
| npoints | 0 — Polygon's constructor still ran |
| xpoints, ypoints | arrays of length 4 |
| color | null |
If you don't define a constructor, the compiler will generate one that does nothing — nothing beyond calling super(). So the inherited fields are fine and the new one is null, which fails later inside draw.
Write a constructor and give the new field a default.
public DrawablePolygon() {
super();
color = Color.GRAY;
}| line | initialises |
|---|---|
| super() | npoints, xpoints, ypoints |
| color = Color.GRAY | the new attribute |
Every field a subclass adds is a field the superclass's constructor knows nothing about. Writing the constructor is how you take responsibility for them — and super() first, so the inherited state exists before you touch anything.
Definition probe
DrawablePolygon adds two things.
Sort into buckets
Sort each member.
Fill the middle
Set the colour, then fill.
Fill in the blanks
public void draw(Graphics g) setColor}(color);
g.fillPolygon(this);
}
Why: Graphics is stateful, so setColor applies to everything drawn after it — which is why it comes first. Passing this to fillPolygon works because the method takes a Polygon and every DrawablePolygon is one.
Counterexample
A class holding a Polygon and a Color would work too.
Discussion prompt
class ColoredShape { Polygon p; Color c; } gives the same data. What does the inheritance version buy, and what does the composition version buy?
Hint: What can you pass to fillPolygon?
Answer:
Inheritance buys substitution. A DrawablePolygon can be passed straight to fillPolygon, contains, or anything else expecting a Polygon; the composition version must unwrap first.
Composition buys control. You expose only the methods you want, rather than inheriting all of Polygon's — including its public mutable arrays.
Here inheritance wins because the IS-A is genuine and the library API demands a Polygon. When the sentence is true and the parent's methods all make sense, extending is the lighter answer — that is the same test as Lesson 14a's.
Section
Section 17.3
Concept
In mathematics, a regular polygon has all sides the same length and all angles equal in measure. Regular polygons are a special case of polygons, so we will use specialization to define a class for them.
RegularPolygon rp = new RegularPolygon(6, 50, Color.BLUE);
// class RegularPolygon extends DrawablePolygon
// which extends Polygon
// which extends Object| class | adds |
|---|---|
| Polygon | npoints, xpoints, ypoints, addPoint, translate |
| DrawablePolygon | color, draw |
| RegularPolygon | a constructor that computes the vertices |
We could extend the Polygon class, as we did in the previous section. But then we would not have the Color functionality we just added. So we will make RegularPolygon extend DrawablePolygon. Inheritance chains: each level keeps everything below it.
Notation
The constructor uses trigonometry to find the coordinates of each vertex. Four steps, and the book gives all of them.
Annotate
(int) Math.round(x).One formula, applied n times. The whole difference between a triangle and a 360-sided near-circle is the value of n — which is why the constructor takes it as a parameter rather than having three classes.
Worked example
Two blocks: initialise the inherited attributes, then compute the vertices.
public RegularPolygon(int nsides, int radius, Color color) {
// initialize DrawablePolygon attributes
this.npoints = nsides;
this.xpoints = new int[nsides];
this.ypoints = new int[nsides];
this.color = color;
// the amount to rotate for each vertex (in radians)
double theta = 2.0 * Math.PI / nsides;
// compute x and y coordinates, centered at the origin
for (int i = 0; i < nsides; i++) {
double x = radius * Math.cos(i * theta);
double y = radius * Math.sin(i * theta);
xpoints[i] = (int) Math.round(x);
ypoints[i] = (int) Math.round(y);
}
}| nsides | theta | vertex 0 | vertex 1 |
|---|---|---|---|
| 4 | π/2 ≈ 1.571 | (50, 0) | (0, 50) |
| 6 | π/3 ≈ 1.047 | (50, 0) | (25, 43) |
| 360 | ≈ 0.017 | (50, 0) | (50, 1) |
Fill in all four attributes directly.
Why: This constructor initializes all four DrawablePolygon attributes, so it doesn't have to invoke super().
Compute the step angle.
Why: double theta = 2.0 * Math.PI / nsides;
Loop over the vertices.
Why: Inside the for loop, it uses Math.sin and Math.cos to compute the coordinates of the vertices as floating-point numbers.
Round to integers.
Why: Then it rounds them off to integers and stores them in the arrays.
Verify: Check vertex 0 for any n: cos(0) is 1 and sin(0) is 0, so it is always at (radius, 0).
Why: Note 2.0 * Math.PI rather than 2 * Math.PI. Both work here since Math.PI is a double, but writing 2.0 makes the floating-point intent explicit — and Lesson 2b's integer-division trap is close enough that the habit is worth keeping.
Prediction
A circle divided into six.
double theta = 2.0 * Math.PI / nsides; // nsides = 6| value | |
|---|---|
| 2 × π | about 6.283 |
| divided by 6 | ? |
Predict first
What is theta, roughly?
Correct: About 1.047 radians — 60 degrees
Why: A full circle is 2π radians, or about 6.283, and dividing by six gives about 1.047 — which is 60 degrees. Every vertex sits one theta further around, so vertex i is at angle i * theta.
Concept
When we construct a RegularPolygon, the vertices are centered at the point (0, 0).
RegularPolygon rp = new RegularPolygon(6, 50, Color.BLUE);
rp.translate(100, 100);| step | centre | vertex 0 |
|---|---|---|
| after the constructor | (0, 0) | (50, 0) |
| after translate(100, 100) | (100, 100) | (150, 100) |
If we want the center of the polygon to be somewhere else, we can use translate, which we inherit from Polygon. Building at the origin and moving afterwards keeps the trigonometry simple — and translate was free, which is a small return on choosing to extend Polygon.
Trap
Math.cos takes radians, not degrees.
double theta = 360.0 / nsides; // degrees
double x = radius * Math.cos(i * theta); // wrong| nsides | theta in degrees | what cos sees | result |
|---|---|---|---|
| 6 | 60 | 60 radians | nonsense |
| 4 | 90 | 90 radians | nonsense |
| does it crash? | — | — | no |
60 radians is about nine and a half full turns, so the cosine is some arbitrary value between −1 and 1. The shape is drawn, and it is not a hexagon — a bug with no error message at all.
A full circle is 2π radians.
double theta = 2.0 * Math.PI / nsides;
// or, if you must start from degrees:
double theta = Math.toRadians(360.0 / nsides);| nsides | theta in radians | meaning |
|---|---|---|
| 3 | 2.094 | 120 degrees |
| 4 | 1.571 | 90 degrees |
| 6 | 1.047 | 60 degrees |
Lesson 4a introduced Math.toRadians for exactly this, and here the calculation starts in radians so it is not needed. Every trig function in Java's Math class takes radians — there are no degree versions, which is a deliberate simplification you have to remember.
Prediction
i is zero on the first iteration.
double x = radius * Math.cos(i * theta);
double y = radius * Math.sin(i * theta);
// i = 0, radius = 50| expression | value |
|---|---|
| Math.cos(0) | 1.0 |
| Math.sin(0) | 0.0 |
Predict first
What are vertex 0's coordinates?
Correct: (50, 0)
Why: cos(0) is 1 and sin(0) is 0, so vertex 0 lands at (radius, 0) — directly right of the centre. That is true for every regular polygon regardless of the number of sides, which makes it a good value to check the constructor against.
Fill the middle
Cosine gives x; sine gives y.
Fill in the blanks
double theta = 2.0 * Math.PI / nsides;
double x = radius * Math.cos(i * theta);
double y = radius * Math.sin(i * theta);
Why: Math.PI is a class variable — Lesson 12a's static final — and Math.sin takes radians, which is why theta is computed from 2π rather than 360. By definition cos(θ) = x/r and sin(θ) = y/r, so multiplying each by the radius gives the coordinates.
Edge cases
The book creates one.
Discussion prompt
new RegularPolygon(360, 50, Color.BLUE) computes 360 vertices on a circle of radius 50. What does it look like, and what does that say about curves in computer graphics?
Hint: How far apart are adjacent vertices?
Answer:
A polygon with 360 sides is a pretty good approximation of a circle. At radius 50 the vertices are less than a pixel apart, so no straight edge is visible.
Which is how curves are drawn generally: the screen has no curves, only pixels, so every circle you have ever seen on a screen is an approximation.
The interesting part is choosing n. Too few and the shape looks faceted; too many and you compute vertices finer than a pixel for nothing. Real graphics code picks n from the radius — which is a decision this constructor leaves to its caller.
Section
Section 17.4
Concept
Classes in the Java library often have more than one constructor for convenience. We can do the same with RegularPolygon.
public RegularPolygon(int nsides, int radius) {
this(nsides, radius, Color.GRAY);
}
public RegularPolygon(int nsides) {
this(nsides, 50);
}| call | chains to | final arguments |
|---|---|---|
| new RegularPolygon(6) | this(6, 50) | — |
| → this(6, 50) | this(6, 50, GRAY) | — |
| → this(6, 50, GRAY) | the real constructor | 6, 50, GRAY |
The keyword this, when used in a constructor, invokes another constructor in the same class. It has a similar syntax as the keyword super, which invokes a constructor in the superclass. Two keywords, one idea, aimed at different classes.
Picture it
Because we provide only one integer argument, Java calls the third constructor, which calls the second one, which calls the first one.
Figure (svg): A pipeline showing a one-argument constructor call chaining through two and three argument versions
The result is a RegularPolygon with the specified value of nsides, 6, the default value of radius, 50, and the default color, GRAY. One implementation, three ways in — and no duplicated logic.
Worked example
When writing constructors, it's a good idea to validate the values you get as arguments. Doing so prevents run-time errors later in the program, which makes the code easier to debug.
public RegularPolygon(int nsides, int radius, Color color) {
// validate the arguments
if (nsides < 3) {
throw new IllegalArgumentException("invalid nsides");
}
if (radius <= 0) {
throw new IllegalArgumentException("invalid radius");
}
if (color == null) {
throw new NullPointerException("invalid color");
}
// the rest of the method is omitted
}| argument | must be | exception if not |
|---|---|---|
| nsides | at least 3 | IllegalArgumentException |
| radius | greater than zero | IllegalArgumentException |
| color | not null | NullPointerException |
Decide what valid means.
Why: The number of sides should be at least three, the radius should be greater than zero, and the color should not be null.
Reject anything else immediately.
Why: We throw an exception to indicate that one of the arguments is invalid.
Know what that does.
Why: By default, these exceptions terminate the program and display an error message along with the stack trace.
Put it in one place.
Why: Because we added this code to the most general constructor, we don't have to add it to the others.
Verify: Call new RegularPolygon(2) and check that the error names nsides rather than failing somewhere in the drawing code.
Why: That is the entire argument for validating early. Two sides would produce a degenerate shape that draws as a line, and you would be debugging the graphics rather than the call — the failure would be a long way from its cause.
Prediction
Three constructors, chained.
public RegularPolygon(int nsides) {
this(nsides, 50);
}
public RegularPolygon(int nsides, int radius) {
this(nsides, radius, Color.GRAY);
}| parameter | value |
|---|---|
| nsides | 6 |
| radius | ? |
| color | ? |
Predict first
What are the resulting values?
Correct: 6 sides, radius 50, colour GRAY
Why: Java calls the third constructor, which calls the second one, which calls the first one. Each link in the chain supplies one default, so a single argument produces a fully initialised object — and none of the defaults is written more than once.
Concept
Lesson 15b caught exceptions. This is the other side: creating and throwing one.
throw new IllegalArgumentException("invalid nsides");| part | means |
|---|---|
| throw | the statement that raises it |
| new IllegalArgumentException(...) | an ordinary object, created with new |
| the string | the message shown to whoever sees it |
| the effect | the constructor stops immediately |
An exception is an object, created with new like any other, and throw is the statement that sends it up the call chain. The exception type is chosen to describe the problem: IllegalArgumentException for a bad value, NullPointerException for a missing object.
Trap
Checking the same things three times.
public RegularPolygon(int nsides) {
if (nsides < 3) { throw new IllegalArgumentException(...); }
this(nsides, 50); // also checks nsides
}
public RegularPolygon(int nsides, int radius) {
if (nsides < 3) { throw new IllegalArgumentException(...); }
if (radius <= 0) { throw new IllegalArgumentException(...); }
this(nsides, radius, Color.GRAY); // checks again
}| problem | consequence |
|---|---|
| the same check in three places | three places to update |
| they can drift apart | one gets a fix, others do not |
| and it does not even compile | this(...) must be the first statement |
The last row is decisive: a this(...) call must be the first statement in a constructor, so the check cannot precede it anyway. Java's rule pushes you toward the right design.
Validate once, in the most general constructor.
public RegularPolygon(int nsides) {
this(nsides, 50); // delegates
}
public RegularPolygon(int nsides, int radius, Color color) {
if (nsides < 3) { throw new IllegalArgumentException(...); }
// ... the checks live only here
}| constructor | validates? | why |
|---|---|---|
| RegularPolygon(int) | no | it delegates |
| RegularPolygon(int, int) | no | it delegates |
| RegularPolygon(int, int, Color) | yes | every path ends here |
Because we added this code to the most general constructor, we don't have to add it to the others. Every chain terminates there, so one copy of the checks covers all three entry points — which is why chaining is worth more than three independent constructors.
Definition probe
Two keywords that both invoke a constructor.
Sort into buckets
Sort each purpose.
Fill the middle
Default the colour; reject too few sides.
Fill in the blanks
public RegularPolygon(int nsides, int radius) this}(nsides, radius, Color.GRAY);
}
// in the three-argument constructor:
if (nsides < 3) throw} new IllegalArgumentException("invalid nsides");
}
Why: this(...) delegates to another constructor in the same class and must be the first statement — which is why the validation lives in the constructor at the end of every chain rather than being repeated. throw raises the exception object, stopping the constructor immediately.
Real world
An invalid argument would probably cause a crash eventually anyway.
Discussion prompt
new RegularPolygon(2) without validation would produce a degenerate shape and maybe fail later. Why is failing immediately better?
Hint: Where would you look for the bug?
Answer:
Because the failure names the actual mistake. invalid nsides points at the call; a graphics glitch three screens later points nowhere.
Doing so prevents run-time errors later in the program, which makes the code easier to debug — and the further apart the cause and the symptom, the more expensive the debugging.
Fail fast, at the boundary. A constructor is where invalid data enters the object, so it is the cheapest place to stop it — and the same reasoning as Lesson 5b's input validation, one level up.
Comparison
Fill the blanks.
Comparison matrix
| this(...) | super(...) | |
|---|---|---|
| invokes a constructor in | the same class | the superclass |
| used for | default parameter values | initialising inherited attributes |
| must be | the first statement | the first statement |
| example in this lesson | RegularPolygon(6) → (6, 50) | DrawablePolygon → Polygon |
this also means | a reference to the current object | — (super has no such use) |
The last row is the one that trips people up. this is overloaded: a reference to the current object almost everywhere, and a constructor call in exactly one position — the first statement of a constructor.
Pattern
Constructor chaining with validation at the end of the chain.
// the most general constructor does the work and the checking
public RegularPolygon(int nsides, int radius, Color color) {
if (nsides < 3) {
throw new IllegalArgumentException("invalid nsides");
}
...compute the vertices...
}
// the others supply defaults and delegate
public RegularPolygon(int nsides, int radius) {
this(nsides, radius, Color.GRAY);
}
public RegularPolygon(int nsides) {
this(nsides, 50);
}| rule | reason |
|---|---|
| one constructor does the work | no duplicated logic |
| the others delegate with this(...) | each supplies one default |
| validate in the most general one | every chain ends there |
| this(...) must come first | Java enforces it |
| throw for an invalid argument | fail where the mistake is |
Math.cos and Math.sin take radians; a circle is 2π of them.Check
Work it out before you click.
public RegularPolygon(int nsides) {
this(nsides, 50);
}
public RegularPolygon(int nsides, int radius) {
this(nsides, radius, Color.GRAY);
}
RegularPolygon rp = new RegularPolygon(8);| parameter | value |
|---|---|
| nsides | 8 |
| radius | ? |
Check your understanding
What object is created?
Answer: A
Why: Each constructor supplies one default and delegates: 8 becomes (8, 50), which becomes (8, 50, GRAY). Because we provide only one integer argument, Java calls the third constructor, which calls the second one, which calls the first one — and only the last has a body, so there is one implementation and no duplicated logic.
this(...) is a complete constructor body; nothing further is required.Check
Work it out before you click.
double theta = 2.0 * Math.PI / nsides;
double x = radius * Math.cos(i * theta);| unit | |
|---|---|
| Math.cos expects | ? |
Check your understanding
Why 2π rather than 360?
Answer: A
Why: Every trigonometric function in java.lang.Math works in radians — there are no degree versions. Using 360 would pass an angle of, say, 60 radians instead of 60 degrees, producing an arbitrary value between −1 and 1 and a shape that is not a polygon. Nothing would crash, which is what makes it a nasty bug.
Check
Work it out before you click.
public RegularPolygon(int nsides, int radius, Color color) {
if (nsides < 3) {
throw new IllegalArgumentException("invalid nsides");
}
...
}
// the one- and two-argument constructors have no checks| call | reaches the three-arg constructor? |
|---|---|
| new RegularPolygon(2) | yes, via the chain |
Check your understanding
Is new RegularPolygon(2) caught?
Answer: A
Why: Because we added this code to the most general constructor, we don't have to add it to the others. Every chain terminates at the three-argument version, so one copy of the checks guards all three entry points — which is a large part of why chaining beats three independent constructors.
Real world
Some languages let you write RegularPolygon(int nsides, int radius = 50). Java does not.
Discussion prompt
Java has no default parameter values, so constructor chaining does the job instead. What does that cost, and what does it buy?
Hint: Count the constructors, and ask where the default lives.
Answer:
It costs verbosity: three constructors instead of one signature with defaults, and each one has to be written and maintained.
It buys one thing that matters — the default is written in code, in exactly one place, and can be any expression at all. A defaulted parameter in some languages is limited to constant values.
And it is the same pattern regardless of language. Even where defaults exist, a telescoping set of constructors that all delegate to one implementation is the standard shape — because there should be one place the real work happens.
Commit first
Commit to an answer and to your confidence.
Predict first
What does this(nsides, radius, Color.GRAY); do when written inside a constructor?
Correct: Invokes another constructor in the same class
Why: The keyword this, when used in a constructor, invokes another constructor in the same class. It has a similar syntax as the keyword super, which invokes a constructor in the superclass. No second object is created — the same object is being initialised, just by a different constructor. Note that this means something else everywhere outside this position: a reference to the current object. The constructor-call meaning applies only as the first statement of a constructor, and Java requires it to be first precisely so that no initialisation happens before the delegate runs.
Explain it
Two minutes, out loud.
Discussion prompt
A classmate has written three constructors for a class and copied the same validation into all three. Explain what they should do instead, and why Java's rules push them that way.
Hint: Where must a this(...) call go?
Answer:
Pick the constructor with the most parameters and put the real work and all the checks in it. Make the others supply defaults and delegate to it with this(...).
Then every path ends in the validating constructor, so one copy of the checks covers every entry point — and there is only one place to update when a rule changes.
Java's rules push you there anyway: a this(...) call must be the first statement in a constructor, so you cannot put a check before it even if you wanted to. The language makes the good design the only convenient one.
Exit ticket
One question before you close the deck.
Predict first
What is the difference between generalization and specialization?
Correct: Generalization extracts common code from existing classes into a new superclass; specialization extends an existing class to add or modify behaviour
Why: Generalization: the process of extracting common code from two or more classes and moving it into a superclass. Specialization: extending a class to add new attributes or methods, or to modify existing behavior. Both use extends — the names describe the direction of the work, not the syntax. CardCollection and Automaton were generalizations, written to hold code that already existed elsewhere; GridCanvas and DrawablePolygon are specializations of library classes their authors never saw. The third option is a tendency rather than a rule: you can generalise or specialise either.
Connect it up
One page, from memory.
Draw it
Draw the inheritance chain Object → Polygon → DrawablePolygon → RegularPolygon, writing beside each class only what it adds. Then draw a hexagon centred at the origin with radius r, mark theta = 2π/n, and write the two formulas that give vertex i. Below that, write the three RegularPolygon constructors with arrows showing how new RegularPolygon(6) reaches the one that does the work — and mark where the validation lives and why it lives there.
Recap
Four sections that name two kinds of inheritance, then specialise a library class twice.
| if you remember one thing | it is this |
|---|---|
| about inheritance | two directions, one keyword |
| about constructors | one does the work; the rest delegate |
| about arguments | reject bad ones where they arrive |
super().this(...) invokes another constructor in the same class, which is how Java does default arguments.Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.