On this page:
1 Understanding Constructors
1.1 Overloading Constructors:   Assuring Data Integrity.
1.2 Overloading Constructors:   Providing Defaults.
1.3 Overloading Constructors:   Expanding Options.
2 Defining equality
2.1 Equality:   Getting started
2.2 Equality:   same  Mug
2.3 Equality:   same  Cup
2.4 Equality:   same  ILo  Cup
2.5 Equality:   same  Cup  Cabinet
2.6 Equality:   some math
8.10

Recitation 5: Custom Constructors and Equality

Goals:

Practice designing custom constructors and equality methods for union data types.

1 Understanding Constructors

This problem reviews and elaborates on the Date example from class. Start by designing a simple Date class with three integer fields representing the month, day and year of the date. Create an ExamplesDate class, and create at least three examples of dates. Create a run configuration for your project and make sure that your data examples compile.

1.1 Overloading Constructors: Assuring Data Integrity.

Java data definitions let us capture the types of our data, and so help catch some kinds of errors statically. But sometimes the types are not enough to capture the full meaning of data and the restrictions on what values can be used to initialize different fields. For example, representing calendar dates with three integers for the day, month, and year ensures that we can’t mistakenly use textual date descriptions. But we know that the month must be between 1 and 12, and the day must be between 1 and 31 (though there are additional restrictions on the day, depending on the month and whether we are in a leap year). We might also consider restricting the year to the range 1500—2100, to describe (semi-)recent history.

Suppose we make the following Date examples:

// Good dates Date d20100228 = new Date(2010, 2, 28); // Feb 28, 2010 Date d20091012 = new Date(2009, 10, 12); // Oct 12, 2009  
// Bad date Date dn303323 = new Date(-30, 33, 23);

Of course, the third example is just nonsense. While complete validation of dates (months, leap-years, etc...) is a study topic in itself, for the purposes of practicing constructors, we will simply make sure that the month is between 1 and 12, the day is between 1 and 31, and the year is between 1500 and 2100.

Did you notice the repetition in the description of validity? It suggests we start with a few helper methods (an early abstraction):

Do this quickly - do not spend much time on it - maybe do just the method validDay and leave the rest for later - for now just returning true regardless of the input. (Such temporary method bodies are called stubs, their goal is to make the rest of program design possible.)

Now change the Date constructor to the following:

Date(int year, int month, int day){
if (this.validYear(year)) {
this.year = year;
}
else {
throw new IllegalArgumentException("Invalid year: " + Integer.toString(year));
}
 
if (this.validMonth(month)) {
this.month = month;
}
else {
throw new IllegalArgumentException("Invalid month: " + Integer.toString(month));
}
 
if (this.validDay(day)) {
this.day = day;
}
else {
throw new IllegalArgumentException("Invalid day: " + Integer.toString(day));
}
}

What kinds or errors can happen at runtime? An illegal argument error is-a runtime error; a null pointer exception is-a runtime error, ... so IllegalArgumentException, NullPointerException and many others are subclasses of RuntimeException.

To signal an error or some other exceptional runtime condition, we throw an instance of the RuntimeException class. Actually, we can be more specific than merely "Something went wrong at runtime", and use an instance of the IllegalArgumentException, which is a subclass of the RuntimeException class used to indicate "An invalid argument was passed to a method or constructor".

If the program ever executes a statement like:

throw new ...Exception("... message ...");

Java stops the program and signals the error through the constructed instance of the Exception. For our purposes, this will simply terminate the program and print the given error message.

The tester library provides methods to test constructors that should throw exceptions:

boolean t.checkConstructorException(Exception e,
String className,
... constr args ...);

For example, the following test case verifies that our constructor throws the correct exception with the expected message, if the supplied year is 53000:

Don’t worry for now about how providing the name of a class is sufficient to allow the tester library to construct your class. The tester library uses many advanced features of Java to make it easier to write tests; we’ll encounter and explain some of them later in the course.

Another of those advanced features is visible here too: notice that the tester library can test for the presence of an error, and continue running subsequent tests. Other than the tester library, no code that we write in this course can do this.

t.checkConstructorException(
// the expected exception new IllegalArgumentException("Invalid year: 53000"),
 
// the *name* of the class (as a String) whose constructor we invoke "Date",
 
// the arguments for the constructor 53000, 12, 30);

Run your program with this test. Now change the test by providing an incorrect message, incorrect exception (e.g. NoSuchElementException), or by supplying arguments that do not cause an error, and see that the test(s) fail.

Java provides the class RuntimeException with a number of subclasses that can be used to signal different types of dynamic errors. Later we will learn how to handle errors and design new subclasses of the class RuntimeException to signal errors specific to our programs.

Do Now!

Revise the constructor above to remove the remaining duplication: Revise validNumber to return the given number if it lies in the range, rather than true, and throw a given error message if it doesn’t, rather than return false. Revise the constructor to use this helper three times. Confirm that your tests still pass. Do you still need validDay, validYear or validMonth?

1.2 Overloading Constructors: Providing Defaults.

When entering dates for the current year it is tedious to continually enter 2019. We can provide an additional constructor that only requires the month and day, assuming the year should be 2019. Note that this constructor too should ensure that the month and day are valid; just because we’re specifying a good year doesn’t mean the other fields are correct! How might we do this, without duplicating code? After all, we’ve already written a constructor that handles the error checking; we shouldn’t have to write that code again.

In lecture, we’ve seen how to invoke constructors on a superclass when one class inherits from another. But our Date class isn’t inheriting from anything; we want to reuse another constructor we’ve already defined on this class. We can use the following syntax to achieve this:

Date(int month, int day){
this(2019, month, day);
}

This syntax is analogous to using super(...) to invoke a constructor on the superclass, but instead means “invoke another (overloaded) constructor on this class”.

Add examples that use only the month and day to see that the constructor works properly. Include tests with invalid month, day or year values as well.

1.3 Overloading Constructors: Expanding Options.

The user may want to enter the date in the form: Jan 20 2013. To make this possible, we can add another constructor:

Date(String month, int day, int year){
...
}

Our first task is to convert a String that represents a month into a number. We can do it in a helper method getMonthNo:

// Convert a three letter month into the numeric value int getMonthNo(String month){
if(month.equals("Jan")){ return 1; }
else if (month.equals("Feb")){ return 2; }
else if (month.equals("Mar")){ return 3; }
else if (month.equals("Apr")){ return 4; }
...
else {
throw new IllegalArgumentException("Invalid month");
}
}

(There may be more efficient ways to provide the list of valid names for the months; for now we are just focusing on the fact that this is possible.)

Ideally, we would like to use this method as follows:

Date(String month, int day, int year){
// Invoke the primary constructor, with a valid month this(year, this.getMonthNo(month), day);
}

But Java will not permit us to invoke a method on our object while still in the middle of invoking another constructor on our object. (Which happens first, evaluating the call to getMonthNo or the call to the other constructor?) So instead, we have to temporarily provide dummy data and immediately fix it:

Date(String month, int day, int year){
// Invoke the primary constructor, with a valid (but made-up) month this(year, 1, day);
 
// Re-initialize the month to the given one this.month = this.getMonthNo(month);
}

Complete the implementation, and check that it works correctly.

2 Defining equality

Consider the following data definition of a cabinet of cups:

import java.awt.Color;
 
class CupCabinet {
int shelves;
ILoCup cups;
 
CupCabinet(int shelves, ILoCup cups) {
this.shelves = shelves;
this.cups = cups;
}
}
 
interface ILoCup {}
 
class MtLoCup implements ILoCup {}
 
class ConsLoCup implements ILoCup {
ICup first;
ILoCup rest;
 
ConsLoCup(ICup first, ILoCup rest) {
this.first = first;
this.rest = rest;
}
}
 
interface ICup {}
 
abstract class ACup implements ICup {
int oz;
 
ACup(int oz) {
this.oz = oz;
}
}
 
class Mug extends ACup {
String text; //the quirky text on a mug (possibly empty) Color color;
 
Mug(int oz, String text, Color color) {
super(oz);
this.text = text;
this.color = color;
}
}
 
class Goblet extends ACup {
boolean hasJewels; //does this goblet have jewels?  
Goblet(int oz, boolean hasJewels) {
super(oz);
this.hasJewels = hasJewels;
}
}
 
class Glass extends ACup {
int chips; //the number of chips on this potentially jagged glass  
Glass(int oz, int chips) {
super(oz);
this.chips = chips;
}
}
2.1 Equality: Getting started

Make a healthy amount of examples of cups of all sorts, as well as list of cups and cup cabinets. Be sure to create examples that are similar but not exactly the same.

2.2 Equality: sameMug

In the Mug class, design the method sameMug which determines if a given mug is the same as this one. Test this method. Warning: what type will you have to give your mugs in your examples to test this method?

Then, lift sameMug to the ICup interface. Where should you implement it so the behavior works as we want it to for Goblets and Glasses? Then, test it on all sorts of ICups.

Once you’re done with that, repeat the process for sameGoblet and sameGlass.

2.3 Equality: sameCup

In the ICup interface, design the sameCup method, which determines if a given cup is the same as this one. Does the implementation belong in the abstract class? Why or why not? Once you’ve implemented it, test on cup pairings of all kind: two mugs, two goblets, two glasses, a mug and a glass, a glass and a mug, etc. etc.

2.4 Equality: sameILoCup

Using a similar methodology to the one given above, implement sameLoCup in the ILoCup interface and sublasses. An abstract class proved helpful for sameCup; would it for sameLoCup? Why or why not?

As always, be sure to test your method on a variety of pairs of lists of cups. Cover as many cases as you can think of.

2.5 Equality: sameCupCabinet

Finally, implement and test sameCupCabinet.

2.6 Equality: some math

For anyone who’s curious, these same methods are equivalence relations. An equivalence relation is a function that takes two inputs of the same type and outputs a boolean. It has three properties:

These properties are called reflexivity, symmetry, and transitivity, respectively. Can you think of any equivalence relation over the integers besides the standard ==? How many distinct equivalence relations exist over a finite datatype with n elements?