Lab 6: Inheritance
We've just learned about inheritance, in which one class can extend another, adding (or slightly changing) functionality. We're going to play with this today using a custom string class. We are going to make other subclasses that extend or modify functionality.
The class diagram to the right shows how the various classes relate to each other.
This image is a subset of a diagram from UML (Unified Modeling Language).
You will see these frequently. You can here more about them
here if you are interested.
Note
Pay attention to exact file names, class names, and method names.
Use the names the names that are requested in this lab.
Misnamed files, classes, or methods will not be graded.
Step 1: Our Base Class
The parent class is often called the 'Base Class' or 'superclass'. Our base class is named ICString. It creates a String-like object. Copy the code below and save it to a file named ICString.java in a directory named Lab06.
public class ICString {
private char[] charArray;
public ICString(String s) {
charArray = s.toCharArray();
}
public int length() {
return charArray.length;
}
public String toString() {
return new String(charArray);
}
public static void main(String[] args) {
ICString s = new ICString("Test");
System.out.println("ICString: " + s);
System.out.println("length: " + s.length());
}
}
Compile and run the code from the command-line.
Here are some things to notice:
- charArray is private. This means that no other class will be able to access it directly. This will become a problem for us in a few minutes.
- 'public String toString()' is a special function that is defined in java.lang.Object. This is the ultimate base class that all objects in Java inherit from. Whenever you directly print an object, this is the function that creates the String to get printed. It gets called by the first println above in the main() function. Take a minute to read about this method here.
- The main() method in this class is used for testing purposes. ICString is a data class - it serves no purpose to run it as a program. This allows us to use the main() method for providing output and debugging our code. You should create a main() method for testing in each of the string classes that we create in this lab.
Step 2: Our first subclass
The Java String class is sometimes difficult to work with because it is immutable (once initialized, it cannot be changed.) We are going to create our own string class that can be changed.
Create a file named MString.java ("Mutable String") with the following Requirements:
- MString extends class ICString
- MString has one constructor that accepts a String as its only argument
- MString has a main() function that creates and prints an MString as an example, just like in ICString above
At this point MString has the same functionality as ICString. It just has a different name.
Compile and test MString until you get it working properly.
Step 3: Changing characters in the string
The Requirement for this step is to add a new function to MString:
public boolean setCharAt(int i, char c)
- 'i' is the index of the char in charArray that is to be changed
- 'c' is the character to be added to charArray
- This method overwrites the original char - the length of charArray does not change
- The method returns false if 'i' is outside the legal range of indices into charArray. It should not attempt to set a char at an illegal location.
- The method returns true if 'i' is a legal index and a char was changed
- You should notice a problem with this method right away: charArray is private and cannot be modified by child classes. This is good - the author utilized the 'Principle of Least Privilege' (described here) when it was originally designed. Now that we need to access it from a child class, we need to open its permissions some more. In keeping with the PLP, change ICString.charArray to be protected rather than private.
Add some test code to the MString main() method to verify that the char is being overwritten properly, and that the code does not crash if you request to overwrite an illegal index.
Step 4: Our second subclass
We now have a mutable string class. This is pretty useful. We are now going to inherit again to take advantage of this new functionality.
Imagine that we wanted a string that was always upper-cased. We want to be able to create the string with both upper and lower-case letters, but the string's characters will always be stored and displayed as capitals. We are going to do this by inheriting from MString and using that class's built-in mutability to keep our letters upper-cased.
Create a file named UString.java ("Uppercase String") with the following Requirements:
- UString has a private void toUpper() method that iterates over the charArray and sets each lowercase letter to a capital. Use a search engine to find an appropriate mechanism for doing this.
- toUpper() actually modifies the charArray - it does not return a separate string
- UString has one constructor that accepts a String as its only argument
- UString's constructor runs toUpper() during initialization to make sure all the characters are uppercase
- Add some test code to main() to verify that this works
Compile and test the above code. You should, for example, be able to create a UString object initialized to 'Mixed Case' and see 'MIXED CASE' when you print it out.
Step 5: Hardening our second subclass
There is a problem with the above description and requirements for UString: It is still possible for UString to be able to hold a lowercase character. Can you see this logic hole? We are going to plug it.
Add the following test case to UString's main method:
UString test1 = new UString("test one");
System.out.println(test1);
test1.setCharAt(0, 't');
System.out.println(test1);
This test should print out "tEST ONE", which has an illicit lowercase character
Our challenge is to fix setCharAt() without modifying MString, and without completely reimplementing the functionality in UString. One thing about class inheritance - you should never reimplement functionality if you do not absolutely have to. Doing so adds extra headaches for maintenance.
Here are the Requirements for this step:
- Add a public boolean setCharAt(int i, char c) method to UString
- This method must not access charArray itself to modify the character - it must call super.setCharAt(i, c) to do that
- This method must call toUpper() after changing the char to make sure it is still uppercased
- This method must return a boolean value indicating whether a character has been changed, just like its parent
- Ask if you are not sure what these Requirements mean
Re-run the test above. It should now print "TEST ONE".
Step 6: Our third subclass
We are going to create a sibling class for UString named LString ("Lowercase String")
Here are the Requirements for this step:
- LString extends MString
- LString is much like UString, except that it makes all of its string lowercase instead of uppercase
- LString must have the same public interface as UString: a constructor, setCharAt(int, char), and main(String[])
- LString must have a private method named toLower() that performs the opposite function as its sibling's toUpper() method
- You are on you own for the remainder of the implementation and testing of LString
Step 7: More functionality for MString
The Requirement for this step is to add a new function to MString:
public void reverse()
- This method reverses the order of the characters in charArray
- charArray is actually modified - the method does not return a separate string
- You are on your own for actually implementing this functionality
- Add some test code to main() to verify that this works. Make sure to test with both even and odd lengths of characters.
Once you have verified that the reverse() method works in MString, try running it in LString:
LString test2 = new LString("Backwards");
test2.reverse()
System.out.println(test2);
This should output "sdrawkcab"
*** This is the whole point of inheritance - we just added a new piece of functionality to MString, and we got the same functionality for free in LString and UString.***
Step 8: So where do static variables fit in?
By now, I am sure you have found yourself wondering how inheritance affects a static variable. Let's do an experiment to find out.
Add the following variable definition to your MString class:
public static String staticTest = "set from MString";
Run this from UString.main():
System.out.println("\nOriginal static string value ...");
System.out.println(MString.staticTest);
System.out.println("\nChanging static string ...");
UString.staticTest = "set from UString";
System.out.println(MString.staticTest);
System.out.println(UString.staticTest);
MString m = new MString("x");
System.out.println(m.staticTest);
System.out.println("\nChanging static string again...");
m.staticTest = "set from m";
System.out.println(MString.staticTest);
System.out.println(UString.staticTest);
System.out.println(m.staticTest);
Make sure you understand why the above code outputs the way it does before your move on. Add some more experiments until you figure it out.
Step 9: Ruggedization testing
You do not need to create a Lab6.java file for this exercise. Your instructors will generate their own for testing. The test program will import each of the above classes and check that they create the expected output.
*** Your classes and methods must match the above Requirements exactly for this to work ***
The actual cases we use for testing are kept secret, but the test code will look something like the following. Go ahead and use it for your own sanity-check:
public class Lab06 {
public static void main(String[] args) {
// Test 0 - ICString
ICString i = new ICString("test");
System.out.println(i);
System.out.println(i.length());
System.out.println();
// Test 1 - MString
MString m = new MString("Mutable String");
System.out.println(m);
boolean b;
b = m.setCharAt(0, 'X');
System.out.println(m + " " + b);
b = m.setCharAt(-1, 'z');
System.out.println(m + " " + b);
b = m.setCharAt(13, 'Y');
System.out.println(m + " " + b);
b = m.setCharAt(14, 'z');
System.out.println(m + " " + b);
m.reverse();
System.out.println(m);
System.out.println();
// Test 2 - UString
UString u = new UString("Upper-Case String");
System.out.println(u);
b = u.setCharAt(1, 'x');
System.out.println(u + " " + b);
b = u.setCharAt(-1, 'A');
System.out.println(u + " " + b);
u.reverse();
System.out.println(u);
System.out.println();
// Test 3 - LString
LString l = new LString("Lower-Case String");
System.out.println(l);
b = l.setCharAt(1, 'X');
System.out.println(l + " " + b);
b = l.setCharAt(-1, 'A');
System.out.println(l + " " + b);
l.reverse();
System.out.println(l);
System.out.println();
}
}
What to submit
Submit the following files:
- ICString.java
- MString.java
- UString.java
- LString.java
All of the above must be in a directory named 'Lab06'
Delete any *.class or other unneeded files prior to submitting
When you run the submit script, the directory and its contents get zipped and sent to your instructor. Ask for help early if you are having problems running the submit script.
Make sure that you run the correct submit script for your instructor:
- CDR Blenkhorn: /courses/blenk/submit Lab06
- Dr. Taylor: /courses/taylor/submit Lab06