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—
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):
Design the method validNumber that consumes a number and the low and high bound and returns true if the number is within the bounds (inclusive on the low end, exclusive on the high end).
Design the methods validDay, validMonth, and validYear designed in a similar manner.
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.
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:
x equals x
if a equals b, then b equals a
if a equals b and b equals c, then a equals c
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?