Lab 3: Static vs. object classes; writing custom/blueprint classes
- Creating the necessary folder
- A couple of reminders
- Working with Roman numerals
- Task 1: Reviewing a static class for Roman numerals
- Task 2: Implementing a class for RomanNumeral objects
- Task 3: Memory diagrams and the ArrayBag class
- Task 4: The Set class
- Optional Post Lab tasks (on Classes and Objects)
- Task 1: Creating a client program for your two classes
- Task 2: Writing another custom/blueprint class
- Post Lab tasks (on Inheritance)
- Task 1: Understanding inheritance
- Task 2: Understanding polymorphism
- Optional Post Lab tasks (on Recursion)
- Task 1: Recursion, the basics
- Task 2: Tracing a method that makes multiple recursive calls
- Task 3: More practice with recursive methods
Creating the necessary folder
Make sure to create a subfolder called
lab3
within your cs112
folder, and put all of the files for this
lab in that folder.
A couple of reminders
-
Attendance in lab is not dependent on whether you finish the lab. Feel free to work on the lab at your own pace to get the most out of the material. Solutions will always be posted after all of the labs have been held, and you can check your solutions with those posted.
-
Working in pairs or small groups in lab is encouraged. We can learn much from each other, so feel free to pair up as directed by your lab TA.
Working with Roman numerals
Recall that a long time ago, numbers were represented with Roman numerals. The value of each Roman numeral symbol is as follows:
- I = 1
- V = 5
- X = 10
- L = 50
- C = 100
- D = 500
- M = 1000
We can represent different numbers by “stringing” together the symbols. Here are some examples:
- IX = 9
- VI = 6
- XX = 20
Given that each symbol has an individual numeric value, we can convert from Roman numeral to decimal using the following rules:
-
Reading left to right, symbols with higher values will generally appear first. In this case, the overall value is given by taking the sum of the individual symbol values from left to right.
-
If a single lower-value symbol is placed directly to the left of a higher-value symbol, the lower value is subtracted from the total instead, according to the following rules:
- I can only subtract from V and X.
- X can only subtract from L and C.
- C can only subtract from D and M.
- V, L, and D can never subtract.
In this lab we will begin by studying a static class called
RomanNumeralStatic
that provides several static methods for
performing operations on Roman numerals. We refer to this class as a
static class because we do not have to create an instance/object of this
class to invoke its methods.
After we have reviewed this collection of static methods, we will then create an object-oriented version of this same class so we can compare and contrast the differences between them.
Task 1: Reviewing a static class for Roman numerals
Begin by downloading this file:
RomanNumeralStatic.java
Make sure to put the file in your lab3
folder. If your
browser doesn’t allow you to specify where a file should be
saved, try right-clicking on its link above and choosing Save
as... or Save link as..., which should produce a dialog box
that allows you to choose the correct folder for the file.
In VS Code, open your lab3
folder using File->Open Folder,
and then click on the name of the file in the Explorer Pane to open
an editor window for it.
Notice that several methods of the class have been written. Further note that one of the methods has been declared to be private and not public. Why do you think this is the case?
-
The
main
method contains code which tests theconvert
method of this class. This method takes in a string representing a Roman numeral and returns its integer equivalent.The signature of this method is:
public static int convert(String romanNum)
Here are a few examples of the test calls that are provided:
System.out.println(convert("X")); System.out.println(convert("LXXXXIX")); System.out.println(convert("CDV"));
Running the program, you should see the following expected output:
10 99 405
-
The
main
method also contains code to test theadd
method of this class. This method accepts two Roman numerals asString
arguments and returns the sum of their values as an integer. The signature of this method is:public static int add(String romanNum1, String romanNum2)
Here are a few examples of the test calls that are provided:
System.out.println(add("X", "X")); System.out.println(add("XI", "CDV")); System.out.println(add("LXXXXIX", "I"));
Running your program, you should see the following expected output:
20 416 100
Task 2: Implementing a class for RomanNumeral
objects
Classes made up of only static methods are useful, but they don’t take
full advantage of the Object Oriented Programming (OOP) paradigm that
Java is based on. To illustrate the difference, let’s convert our
static RomanNumeralStatic
class into an object-oriented version of
this class.
To begin, consider the following:
- What are the key aspects of a Roman numeral?
- What should the attributes of this class be?
- What are the behaviours that this class should implement?
Thinking through this will help us identify the data we need to store and the methods we need to write.
A Roman numeral is a string which has a corresponding numeric value. In order to properly represent a Roman numeral then, each object we create should contain exactly two fields or data members: the string that represents the Roman numeral, and the decimal value associated with it.
In addition, the class will need to include a number of non-static
or instance methods that belong to objects of the class. We make
these methods non-static so that they will have access to the fields
of the RomanNumeral
object on which they are invoked.
Once you have discussed the features of RomanNumeral
objects,
build your class as follows:
-
Select File->New File, which will open up an empty editor window. Then select File->Save, and give the new file the name
RomanNumeral.java
. -
Write the class header, and then declare the data members of the class as private fields (aka instance variables).
-
Write a constructor that takes in a
String
that you can assume is a valid Roman numeral. Initialize the fields based on theString
that is passed in.To help you complete this method, consider what methods you have available to you in the
RomanNumeralStatic
class. Can one of those methods help here? If so, how would you call it? -
Write a
toString
method that returns the string that represents theRomanNumeral
object. We discussed this type of method in lecture. -
Write an
equals
method that checks if twoRomanNumeral
objects are equivalent. We discussed this type of method in lecture. -
Write an
add
method that takes in anotherRomanNumeral
object and returns an integer that is the sum of the calledRomanNumeral
(i.e., the object on which the method is invoked) and the otherRomanNumeral
that is passed in. -
Write a
main
method to test your class. Here’s one simple test that you could include:RomanNumeral r1 = new RomanNumeral("X"); RomanNumeral r2 = new RomanNumeral("IX"); System.out.println( r1 ); System.out.println( r2 );
Expand your
main
method to test each of the methods you have written. For example:System.out.print( "Testing for equality: the objects are " ); if ( !r1.equals(r2) ) System.out.print( "not " ); System.out.println( "equal!" ); // Invoke the add method to add two RomanNumeral objects. // Note that the output should be a decimal integer. System.out.println( r1.add(r2) );
Task 3: Memory diagrams and the ArrayBag
class
Let’s take a closer look at the ArrayBag
class from lecture, which
you will be modifying in Problem Set
3.
Recall that this class is one possible implementation of a data
structure known as a bag, which is a simple collection of items in
which the order of the items doesn’t matter. A good analogy is a bag
of candy!
-
Download the file for this class to your
lab3
folder:
ArrayBag.java
Open your
lab3
folder as needed, and click on the name of the file in the Explorer Pane to open an editor window for it. -
What are the fields of the
ArrayBag
class, and why did we include them in our definition? -
Note that there isn’t a
getItem()
method for accessing a specific item in the bag. Instead, there is a method calledgrab()
which accesses a random item in the bag. Why does this make sense, given the characteristics of a bag? -
In lecture, we looked at the
add()
method, which adds a single item to theArrayBag
. Let’s draw a memory diagram (stack and heap) of anArrayBag
object calledb
as theadd()
method is called on it for the first time. Show the addition of the string"don't blink"
-
Write a simple
main()
method in which you do the following:-
Complete the following statement to create an
ArrayBag
object with the default maximum size:ArrayBag b = ...
-
Add
"don't blink"
to thatArrayBag
. - Add
"baggy"
to thatArrayBag
.
-
-
After performing these operations, output what your
ArrayBag
looks like:System.out.println(b)
You should expect to see the following:
{don't blink, baggy}
Note that the
toString()
method of theArrayBag
class is being invoked. This method produces and returns the string representation of all the items in the bag referenced byb
. -
Let’s say that we now want to “grab” one of the items that we just added to
b
. What happens when you do the following?String s = b.grab();
Why does what you see make sense in light of the rules of polymorphism?
-
We can make this work by using an operation known as a type cast:
String s = (String)b.grab();
This doesn’t actually change the type of the underlying object. It just reassures the compiler that the assignment will be valid. If the item being returned were not a
String
object, an exception would be thrown. -
Add a non-static method called
hasMoreRoom()
that takes as a parameter anotherArrayBag
calledother
, and that returnstrue
if the calledArrayBag
has more room left for items (i.e., more unused array elements) thanother
does, andfalse
otherwise.If
other
isnull
, the method should throw anIllegalArgumentException
.For example:
ArrayBag b1 = new ArrayBag(10); ArrayBag b2 = new ArrayBag(12); System.out.println(b2.hasMoreRoom(b1));
should output:
true
and
b2.add("hello"); b2.add("world"); System.out.println(b2.hasMoreRoom(b1));
should output:
false
Hint: Because this method is part of the
ArrayBag
class, it can directly access the private fields of theArrayBag
that is passed in as a parameter.
Task 4: The Set
class
Let’s take a closer look at the Set
which you will be
writing in Problem Set
3.
Optional Post Lab tasks (on Classes and Objects)
Task 1: Creating a client program for your two classes
Create a new program TestRomanNumerals.java
. This program only
needs a main
method that we are going to use to write and test
the code we have written in our two previous classes.
Much of the code that we will write in this method we have already
written in the main
methods local to each class. However
depending on whether or not we are using the static API or creating
instance objects, the methods may not be called the same way
as they were in the main
method of their own class file.
Add code in the main
method of this class to accomplish each of the
following:
-
Create two
String
objects for Roman numerals. Invoke the staticconvert
andadd
methods of ourRomanNumeralStatic
class, passing the appropriate arguments to see the decimal equivalents of the two Roman numeral strings that you created and then to add them together. -
Create at least two instances of
RomanNumeral
objects, add them together, and print out the results. -
Create an array of
String
objects that represent Roman numerals. For each element of the array, compute and print out the corresponding integer value. -
Create an array of
RomanNumeral
objects. For each element of the array, compute and print out the corresponding integer value.
Task 2: Writing another custom/blueprint class
Our RomanNumeral
class allowed us to create our own custom data type
– i.e., our own type of object. This type of class is sometimes
referred to as a blueprint class to distinguish it from a class that
simply serves as a container for a program.
In this task, we will implement a blueprint class for Point
objects,
each of which will represent a single point with integer coordinates
(x, y).
-
Begin by downloading
Point.java
Make sure to put the file in your
lab3
folder.In VS Code, open your
lab3
folder as needed using File->Open Folder, and then click on the name of the file in the Explorer Pane to open an editor window for it.Read through the file to understand what is already provided.
-
Use File->New File to create a separate test program that you can use to create and modify
Point
objects. Give your test class an appropriate name, and don’t forget that the name of the class must match the name of the file.At the start of your test program’s
main
method, add a line in which you fill in the blanks below to create aPoint
object with an x-coordinate of -2 and a y-coordinate of 5.______ p1 = ________________;
-
The current version of the
Point
class does not employ appropriate encapsulation. To see this, add the following statements to your test program’smain
method after you create your objectp1
:System.out.println("x = " + p1.x); System.out.println("y = " + p1.y);
Running your program now should produce:
x = -2 y = 5
In a properly encapsulated class, code that is outside of a class should not be able to directly access its fields. Make the necessary changes to prevent this, and then try to rerun your test program.
If you’ve been successful, your code should no longer compile.
-
To provide indirect access to the fields, you must add a public accessor method and a public mutator method for each field.
Note that each coordinate can take on any integer value, which means that the mutators don’t need to perform any error-checking, and that we also don’t need to add any error-checking to the constructor. However, keep in mind that ordinarily you do need error-checking in your mutators and constructors.
-
Add the following statement to your
main
method. What do you expect to see when you execute your program?System.out.println(p1);
To see something more descriptive, uncomment the
toString()
method in thePoint
class, and then save and rerun your program.Note that we don’t need to actually call the
toString()
method. Rather, it is called on our behalf whenever we print an object of the class. -
Add an accessor method called
quadrant
that returns the number of the quadrant (if any) in which the calledPoint
object (this
) falls. The method should return:- 1 if both coordinates are positive
- 2 if the x coordinate is negative and the y coordinate is positive
- 3 if both coordinates are negative
- 4 if the x coordinate is positive and the y coordinate is negative
- 0 if the point lies on the x-axis or y-axis
Example 1:
Point p1 = new Point(3, 4); System.out.println(p1.quadrant());
Running this code, you should expect to see:
1
Example 2:
Point p3 = new Point(-3, -4); System.out.println(p3.quadrant());
You should expect to see:
3
Example 3:
Point p5 = new Point(0, -4); System.out.println(p5.quadrant());
You should expect to see:
0
-
Although most of the methods in a blueprint class are non-static (meaning that we can think of them as being inside each object of the class), blueprint classes can also include static methods (which belong to the class as a whole).
A non-static method is preferred if the method needs to access to the fields of a particular called object. However, if a method does not need a called object – i.e., if it makes more sense to pass in all of the information that it needs as parameters – then we typically make it static.
Write a static method
closestToOrigin()
that takes twoPoint
objects and returns thePoint
that is closest to the origin.Hint: Make use of the
distanceFromOrigin()
method that everyPoint
has!Because the method is static, we must prepend the class name to call it from outside the class:
Point p1 = new Point(3, 4); Point p2 = new Point(2, -1); System.out.println(Point.closestToOrigin(p1, p2));
You should expect to see:
(2, -1)
Post Lab tasks (on Inheritance)
Task 1: Understanding inheritance
As we discussed in lecture, a class can extend another class. Let’s consider another example of this together.
Begin by downloading these files, making sure to save
them all in your lab3
folder:
In VS Code, open your lab3
folder using File->Open Folder,
and then click on the name of each file in the Explorer Pane to open
an editor window for it.
Review each file and take note of how they are related.
Note: The Cat
class will not compile until we first make some
changes to it, so don’t try to compile and run anything yet!
-
Which class is the superclass? Which is the subclass? What does it mean that the
Cat
class extends theAnimal
class? -
The
Cat
class cannot directly access the fields it inherits fromAnimal
. Why not? -
The subclass constructor typically calls the superclass constructor to initialize the inherited fields. Write a constructor for the
Cat
class above. It should take as parameters the cat’s name and a boolean indicating whether it is short-haired, and it should call the superclass constructor to initialize the inherited fields.Update the test program to create an instance of class
Cat
.Cat c = new Cat("Kitty", false);
-
To manipulate the inherited fields, the subclass can use the inherited accessor and mutator methods. Write a
toString
method for theCat
class above. It should return a string consisting of the cat’s name followed by either" (short-haired)"
or" (long-haired)"
.Update the test program to test your method:
System.out.println(c);
-
The subclass can override an inherited method, replacing it with a version that is more appropriate. Write an
isSleeping
method for theCat
class. It should reflect the fact that cats seem to sleep all of the time!Update the test program to test your method.
-
Let’s say that we now want to define a class called
Abyssinian
for cats that belong to that particular breed of short-haired cat. Which class should it extend? -
Go ahead and create a class named
Abyssinian
, defining it so that it extends the correct class. Use File->New File to create the necessary file for it, and save it using the correct name.Abyssinian
should not have any new fields of its own. However, it should include:-
a constructor that takes only a name, and that calls the superclass constructor to initialize the inherited fields. When making that call, make sure that it reflects the fact that Abyssinians are short-haired.
-
an
isExtroverted()
method that overrides the inherited version and replaces it with one that reflects the fact that Abyssinian cats are known to be extroverted.
Once your class is created, go ahead and test out the methods in your main program.
-
-
Another possible class for this hierarchy of animals is the
Dog
class, which you should examine now, although you don’t need to open it in your IDE. In addition to its inherited fields and methods, it has aboolean
fieldisSmall
, and methodsisSmall()
andbark()
. -
Let’s say that we have created an
Abyssinian
object and assigned it to the variablea
:Abyssinian a = new Abyssinian("Abby");
For each of the following method calls:
-
Indicate whether it will compile. Because the variable
a
is declared to be of typeAbyssinian
, a method call usinga
will compile if there is a corresponding method insideAbyssinian
objects – either defined in theAbyssinian
class itself or inherited from a superclass. A method call will not compile if there is no corresponding method in objects of that class. -
If the method call will compile, specify which version of the method will be called. In other words, in which class can we find the version of the method that will be called?
Here are the calls to test:
-
a.getNumLegs()
-
a.isExtroverted()
-
a.isSleeping(12, 30)
-
a.isSmall()
-
a.toString()
-
a.equals(a)
-
-
You will notice a static method defined in the
Animal
class namedprintAnimalName
. How can you call this method in your main program to print out the names of all the animal objects you have created? Note that it is astatic
method. How does this differ from the other methods of the class?Update the test program to test this method.
Task 2: Understanding polymorphism
Your work for this task should go on the piece of paper that we give you. Please show your paper to a staff member before you leave the lab.
Thanks to a feature of Java called polymorphism, we can do something like this:
ClassA myObject = new ClassB(...);
Where ClassB
extends ClassA
, or equivalently, ClassB
is a subclass
of ClassA
. Specifying a more general type for myObject
than the
actual type of the object can be useful when writing a
method that needs to take more than one type of object as a parameter,
or when creating an array of objects of different but related types.
For example, if we wanted to have an array containing different types of animal objects, we could define the array as follows:
Animal[] zoo = new Animal[10];
Then, any element of the array could be of type Animal
or any subclass
of Animal
. In other words, this would be allowed:
zoo[0] = new Dog(...); zoo[1] = new Cat(...); zoo[2] = new Abyssinian(...);
Consider the following class headers:
public class A extends B { ... } public class B extends C { ... } public class C { ... } public class D extends C { ... }
-
Draw an inheritance hierarchy for these classes.
-
Which of these assignments would be allowed, taking into account the rules of polymorphism?
-
B myObj = new A();
-
B myObj = new C();
-
C myObj = new A();
-
A myObj = new B();
-
D myObj = new B();
-
Optional Post Lab tasks (on Recursion)
Task 1: Recursion, the basics
In class we’ve begun talking about how recursive methods work, how to understand recursive methods, and how to trace them out. With lab we’ll be talking a bit about how to design recursive functions or how to break down problems to fit the recursive mold.
We’ll begin by talking about something everyone is familar with, determining if a string is a palindrome.
- Download the template code RecurPalindrome.java.
Complete the method
rPalindrome
which should implement a recursive solution to determine if a string is a palindrome. For simplicity you can asume that the input string is an english word without any spaces or special characters. The method should ultimately returntrue
if the input string is a palindrome, orfalse
otherwise. You can use the starter code provided.
Task 2: Tracing a method that makes multiple recursive calls
A number of the algorithms that we consider in CS 112 involve recursive methods in which a given invocation of the method may make two or more recursive calls. To understand how this type of algorithm works, it helps to use a diagram known as a call tree.
Let’s work together to develop a call tree for the execution of the following recursive method. (The method allows us to recursively generate the nth integer in the Fibonacci sequence, although you don’t need to be familiar with that sequence to understand this problem.)
public static int fib(int n) { if (n == 0 || n == 1) { return 1; } else { int prev1 = fib(n - 2); int prev2 = fib(n - 1); return prev1 + prev2; } }
Note: We are using a slightly modified version of the Fibonacci sequence that begins with 1 instead of 0.
Assume that we begin with the following initial call to this method:
fib(4)
-
Let’s draw a call tree for the sequence of calls that are involved in computing the result of
fib(4)
. As we do so, we’ll number each call of the method in the order in which they are made. -
The order in which the calls are made is not the same as the order in which the calls return. A given invocation of the method can’t return until both of the calls that it makes (
fib(n - 2)
andfib(n - 1)
) return.Underneath your call tree, list the order in which the calls return, as well as their return values. When you refer to a given call in this part of the problem, use its number from the call tree.
For example, the initial call always returns last, so the last line in this part of the problem should look like this:
call 1 (fib(4)) returns ...
where you replace the
...
with its return value. -
Re-write the recursive
fib
method so that it has only onereturn
statement at the end of the method. Hint: Use a variable for the return value, and initialize that variable to the value that would be returned in the base case. Then modify that variable as needed by making the recursive calls.Does the revised version of the method work the same way?
Task 3: More practice with recursive methods
Write a recursive method to solve each of the following:
-
Find the product of the integers from 1 through
n
(this is called the factorial function). Write a recursive method that takes in an integern
and computes (and returns) the factorial of that integer. Ifn
is zero, return 1.What happens if you try to compute the factorial of a large number? Why do you think this is the case? How could we accommodate larger results?
-
Count the number of times that a specific number occurs in an array of integers. To do so, you should write a recursive method with the following header:
public static int countN(int n, int[] arr, int index) {
It should determine the number of times that the integer
n
appears in the portion of the array ofarr
that goes from the specifiedindex
to the end of the array.For example, if we have:
int[] a = {5, 3, 5, 2, 7, 3, 5};
then:
-
countN(5, a, 0)
should return3
, because there are 3 occurrences of the integer5
in the entire array (i.e., the portion of the array that goes from index 0 to the end of the array). -
countN(5, a, 1)
should return2
, because there are 2 occurrences of the integer5
in the portion of the array that goes from index 1 to the end of the array. -
countN(5, a, 3)
should return1
, because there is 1 occurrence of the integer5
in the portion of the array that goes from index 3 to the end of the array.
Note that the inclusion of the
index
parameter is what will allow you to reduce the problem when you make the recursive call! -
-
Find the minimum element in an array of integers. Write a recursive method that takes in an array of integers and whatever other parameters are needed, and that returns the smallest integer in the array.
Hint: You should use an additional parameter to keep track of what portion of the array the current call is focused on, just as we did in the previous problem.
-
Challenge: The greatest common denominator is a common problem in mathematics. Given two positive numbers, what is the largest factor that divised both integers? Write a recursive method that takes in two integers and finds their greatest common denominator. (Hint: Look up Euclid’s algorithm online.)