This week you will need to submit your solutions.

Deadline: Thursday 11/1 11:59PM.

Objectives

  • To practice object oriented design thinking
  • To translate a word problem into a software architecture
  • To learn even more about objects and inheritance

1. UML and OOP Design

In this course we will use a simplified form of UML and will focus on Class diagrams.

  • Rectangles are used to define a class, with the name in the top section, a list of instance variables in the middle section, and functions in the bottom section.
  • We will use a arrow with a white triangular head to indicate the “is a” relationship, which corresponds to inheritance in Java. The arrow should point towards the parent class.
  • We will use a simple angled arrow to represent the “associated with” relationship. This means that the first class will be making function calls and possibly instantiating objects of the class it is pointing to.

You can draw UML diagrams with the tool of your choice, or do it on paper. I recommend Violet UML - download the Jar file and double click to run it. On a Mac you may need to right click and select Open to avoid security warnings.

Exercise 3.1: Draw a UML diagram to represent the Mail System with both voice and text message support. Display the image in your Readme.

Exercise 3.2: Draw a UML diagram to represent the Banking System. Display the image in your Readme.


2. Classes and Memory

Note:

  • Local variables and references are stored in the Stack, just like in C.
  • Objects created with new will be located in the heap.
    • But the reference variable pointing to that object could be anywhere!
  • Static variables are stored in the globals region of memory .

Exercise 3.3: List or draw out the memory contents for the seven slides (23-29) with StaticExample or DynamicExampleXXX and ObjX classes. You can either draw a picture with boxes and arrows for pointers (you do not need to show addresses), or you can clearly list out what memory is stored in each region (Stack, Heap, or Globals). Assume the garbage collector has not yet run.


3. Constructors, Classes, and Inheritance

A few words about nomenclature:

  • Java refers to dynamic objects as instances.
    • Thus, variables in a dynamic object are called instance variables.
  • Variables in a static object are called class variables.
  • Similarly, there are instance methods and class methods.
  • In other languages, the use of the term object may always refer to dynamic objects.
  • For Java programmers, when you get used to the differences between the types of objects, you will also get used to their nomenclature.

Constructors

A class can have multiple constructors, e.g.:

class ObjX {

 int i;
 String name;

 // This is the first constructor: only takes in one parameter for the name.
 public ObjX(String initialName) {
  name = initialName;
  i = 0;
 }

 // This is the second constructor: both name and ID are initialized.
 public ObjX(String initialName, int ID) {
  name = initialName;
  i = ID;
 }

 public String toString() {
  return "My name is " + name + " and my ID number is " + i;
 }

}

public class DynamicExample12 {

 public static void main(String[] argv) {
  // Use the first constructor.
  x = new ObjX("Ms. X");
  System.out.println(x);

  // Use the second one.
  x = new ObjX("Ms. X", 5);
  System.out.println(x);
 }

}

However, once a custom constructor has been defined that takes one or more arguments, Java will no longer allow you to use the default (no argument) constructor.

Thus this code will not work:

// The old way: without using constructors. Will not compile
ObjX x = new ObjX();
x.i = 5;
x.name = "Ms. X";
System.out.println(x);

Note:

  • A constructor executes just once when an object instance is created.
  • Thus, when the first instance is created above:
    ObjX x = new ObjX ("Ms. X");
    the first (one-parameter) constructor executes.

  • Similarly, when the second instance is created above
    x = new ObjX ("Ms. X", 5);
    the second constructor (with two parameters) executes.

  • Constructors are similar to methods in that they take parameters and execute like code.
  • But constructors are unlike methods in that:
  • They are declared differently: they must have the same name as the class and they cannot declare a return type.
  • They cannot be called as a regular method. This fails to compile:
      ObjX x = new ObjX ("Ms. X");
      x.ObjX ("Mr. Y");                 // Will not compile.
    
  • They execute only once for each instance, and execute at the time of creation. Thus, the constructor finishes execution by the time we get to the second line below:
      // Use the first constructor.
      ObjX x = new ObjX ("Ms. X");
      System.out.println (x);
    

Other facts about constructors:

  • Any number of them can be defined, as long as they have different signatures.
  • When you don’t define your own constructors, Java puts one in for you (but you don’t see the code, of course).
    • This is the “default no-parameter” constructor.
    • This is why the parentheses are part of the syntax.
  • When a programmer defines constructors, the Java compiler takes away the default no-parameter constructor
    • It now becomes a compiler error to try and use one.
  • You can of course, define your own no-parameter constructor.

Why use constructors at all, especially if you can initialize values directly?

  ObjX x = new ObjX ();
  x.i = 5;
  • First, it’s bad practice to place initialization code like this outside the class.
  • The right place for initialization code is the class itself, in the constructor.
  • Most often a class is written by one programmer, and used by another.
    • The creator of the class shouldn’t allow others to modify data in order to prevent bugs.
    • Data should be declared private, and set via the constructor or setter methods since they can easily perform error checking before setting the data.

Some Real Classes

Let’s consider the code in some real Java library classes. Their structure can give you ideas about how to structure your own code.

First, let’s look at some string code:

    String s = "hello";
    int len = s.length();
    String s2 = s.substring(0,4);  // Extract first 4 chars  

Now that we know String’s are objects, let’s examine the library definition of String:

First, the object declaration:

public final class String
    extends Object implements Serializable {
        //... stuff ...
}

Here,

  • The keyword final prevents inheritance from this object.
  • The keyword extends says String inherits from a class called Object.
  • The keyword implements says String handles the Serializable interface
  • (More about interfaces later).

Next, let’s list some (only some) of String’s members:

public final class String
    extends Object implements Serializable {

    // Some, but not all, constructors:
    public String();
    public String (String s);
    public String (char[ ] char_array);
    //... many other constructors.

    // Some Class (static) methods:
    public static String valueOf (char[] char_array);
    public static String valueOf (boolean b);
    public static String valueOf (int i);
    public static String valueOf (double d);
    //... many others not shown ...

    // Some instance (dynamic) methods:
    public char charAt (int index);
    public int compareTo (String s);
    public boolean equals (String s);
    public boolean equalsIgnoreCase (String s);
    public String toLowerCase ();
    public String toUpperCase ();
    public String trim ():
    // ... many others ...

}

Note:

  • String has several constructors (11 in all).
  • String has many Class (static) methods.
  • String has many instance methods.

Next, let’s look at the Math object:

  public final class Math extends Object {
    // No constructors at all.

    // Constants: (actual value not specified)
    public static final double E;
    public static final double PI;

    // Some Class methods:
    public static int abs (int i);
    public static double abs (double d);
    public static double exp (double d);
    public static double log (double d);
    public static double floor (double d); // Returns double!
    public static double ceil (double d);  // Returns double
    public static long round (double d);

    // No instance methods

  }

Note:

  • Math is in the package java.lang.
  • Math has no constructors. In fact it is completely static (no instance methods or data).
  • Math’s class methods are the usual mathematical functions we expect.
  • Thus, this is an example of pure encapsulation.
  • Minor point: floor and ceil methods return double not an int like you might expect.

Inheritance

We can use inheritance to have one class build upon the functionality provided by another.

Consider this example:

public class Person {

 // Data
 String name;
 String ssn;

 // Constructors.
 public Person(String nameIn, String ssnIn) {
  name = nameIn;
  ssn = ssnIn;
 }

 public Person() {
  name = ssn = "Not initialized";
 }

 // Accessors.
 public String getName() {
  return name;
 }

 public String getSSN() {
  return ssn;
 }

 public String toString() {
  return "Person: Name=" + name + ", ssn=" + ssn;
 }

 // Testing:
 public static void main(String[] argv) {
  Person p = new Person("George", "111-11-1234");
  System.out.println(p);
 }

}

We will use inheritance to extend this class. Inheritance can be used for several reasons:

1) Inheritance can be used to create a “new and improved” Person object, say, by adding new data and methods:

class PersonV2 extends Person {

 // New data.
 int age;

 // New constructor.
 public PersonV2(String nameIn, String ssnIn, int ageIn) {
  name = nameIn;
  ssn = ssnIn;
  age = ageIn;
 }

 // New accessor function
 public int getAge() {
  return age;
 }

 // Testing.
 public static void main(String[] argv) {
  PersonV2 p = new PersonV2("Elaine", "333-33-4567", 35);
  System.out.println(p + ", age=" + p.getAge());
 }
}

Note:

  • In this example, Person is the parent class (super class) and PersonV2 is the child class (subclass).
  • Alternatively, PersonV2 derives or inherits from Person.
  • Superclass members are accessible from the subclass, e.g.,
public PersonV2 (String nameIn, String ssnIn, int ageIn)
{
    name = nameIn;   // "name" is defined in Person
    ssn = ssnIn;     // "ssn" is defined in Person
    age = ageIn;
}

and

System.out.println(p + ", age=" + p.getAge());
// Uses the toString() function in the superclass.

2) Inheritance can be used to re-write an existing class by overriding superclass methods, for example:

class PersonV3 extends Person {

 // New data.
 int age;

 // New constructor.
 public PersonV3(String nameIn, String ssnIn, int ageIn) {
  name = nameIn;
  ssn = ssnIn;
  age = ageIn;
 }

 // New accessor function
 public int getAge() {
  return age;
 }

 // Overriding the toString() method:
 public String toString() {
  return "PersonV3: name=" + name + ", ssn=" + ssn +
   ", age=" + age;
 }

 // Testing.
 public static void main(String[] argv) {
  PersonV3 p = new PersonV3("Kramer", "666-66-1234", 38);

  // The newer toString() overrides the superclass' toString()
  System.out.println(p);
 }
}

3) The most important use of inheritance is polymorphism.

  • Suppose we want to distinguish between two types of Person’s: customers and employees:
  • First, we extend Person to create a Customer.
class Customer extends Person {

 // New data.
 int loyalty; // Number of years as customer.

 // New constructor.
 public Customer(String nameIn, String ssnIn, int loyaltyIn) {
  name = nameIn;
  ssn = ssnIn;
  loyalty = loyaltyIn;
 }

 // Override toString()
 public String toString() {
  return "Customer: name=" + name + ", " + loyalty +
   " years with the company";
 }

 // A new method.
 public double discountRate() {
  if ((loyalty >= 1) && (loyalty < 5)) {
   return 0.95; // 5% discount.
  } else if ((loyalty >= 5) && (loyalty < 10)) {
   return 0.75; // 25% discount.
  } else if (loyalty >= 10) {
   return 0.50; // 50% discount.
  } else {
   return 1.0; // no discount.
  }
 }
}

Next, we derive Employee from Person:

class Employee extends Person {

 int salary;

 // New constructor calling parent's constructor
 public Employee(String nameIn, String ssnIn, int salaryIn) {
  super(nameIn, ssnIn);
  salary = salaryIn;
 }

 public void paySalary() {
   System.out.println(name + " was just paid $" + salary);
 }

 // Override the toString() method in Person.
 public String toString() {
  return "Employee: name=" + name + ", salary=" + salary;
 }
}

Finally, let’s write some code to test these objects:

public class TestPoly {

 public static void main(String[] argv) {
  Customer c = new Customer("John", "123-12-1234", 2);
  System.out.println(c); // What gets printed?

  Employee e = new Employee("Paul", "234-23-2345", 50000);
  System.out.println(e); // What gets printed?

  // Declare some Person variables.
  Person p1, p2, p3, p4;

  // Person variables can hold Person instances
  p1 = new Person("George", "345-45-3456");
  p2 = new Person("Ringo", "456-45-4567");

  // ... as well as subclass instances
  p3 = c; // c is a Customer.
  p4 = e; // e is an Employee.

  System.out.println("Person data: ");

  // Which toString() is called?
  System.out.println(p1);
  System.out.println(p2);

  // Which toString() is called?
  System.out.println(p3);

  // Which toString() is called?
  System.out.println(p4);

 }
}

You can run the code here.

Note:

  • Customer and Employee both derived from Person.
    • You can derive as many subclasses as desired from a single class, but each class can only have one parent.
  • Customer and Employee both define their own constructors.
    • These will be called instead of the parent constructor.
  • The new constructor in Employee calls the constructor of the parent by using the super(...) function.
    • This calls the constructor in the parent class that matches the passed arguments.
    • If used, super(...) must be the first line of the child’s constructor.
    • Think of it like you need to initialize the parent’s fields before you begin to initialize the child’s.
    • This is called constructor chaining.
  • The child classes can override methods in the parent class, such as toString()
    • The toString() function actually originates in the Object class, from which all others derive. It is automatically called whenever an object needs to be turned into a string, such as when you use an object in a println call.

Obviously, Person instances can be created and assigned to Person variables:

p1 = new Person ("George", "345-45-3456");
p2 = new Person ("Ringo", "456-45-4567");

What is interesting is that subclass instances can also be assigned:

p3 = c;        // c is a Customer.
p4 = e;        // e is an Employee.

This is similar in spirit to assigning an int to a long:

int i = 5;
long j = i;    // i is automatically cast into a long.
Person p = c;  // Customer c is cast into a Person.

BUT, what is really interesting is that even after casting, the object instance seems to remember what it was:

// Call to toString() in Customer.
System.out.println (p3);

// Call to toString() in Employee.
System.out.println (p4);

The output for these two calls is:

Customer: name=John, 2 years with the company
Employee: name=Paul, salary=50000

Note:

  • p3 is really a Customer.
  • p4 is really an Employee.
  • The correct toString() function is called in each case!
  • This feature is known as polymorphism.
  • A superclass can be subclassed into many “morphs”.
  • Each morph has access to its own functions and data even when treated as a superclass type.

Parent Child Relationships

When using polymorphism we need to take special care and think about the types of our references. Consider:

Employee e = new Employee("Bill", "123-23-4578", 60000);
Person p = e;
System.out.println(p); // will correctly call the Employee version of toString()
p.payEmployee(); // will NOT compile
e.payEmployee() // this work
((Employee) p).payEmployee(); // this will work as long as p is an employee
  • In the code above, p is of type Person, so we can only use it to access members (data or methods) named in the Person class. Java will correctly use the Employee version of those methods, but the method or data must be named in Person to be used.
  • If we want to access a class member of the Employee class (such as payEmployee()) we need to use a reference of type Employee. That means we either must use e directly, or we need to cast p into an Employee.
    • What happens if you cast to a type which is NOT correct for the object? Is this caught at runtime or compile time?

Exercise 3.4: In what cases does polymorphism work correctly for the lines below? Explain why or why not for each line.

Employee e0 = new Employee ("Gullible Gus", "555-55-5678", 50000);
Employee e1 = new Customer ("Gullible Gus", "555-55-5678", 5);
Employee e2 = new Person("Zany Zoe", "112-12-1212");
Person p1 = new Customer ("Gullible Gus", "555-55-5678", 5);
Person p2 = (Person) e0;
Customer c1 = (Person) p1;
Customer c2 = p1;
Person p3 = (Employee) e0;
Person p4 = e0; // note, in the original version of this page this said p3 = ...

Remember to treat the Parent-Child relationship as the “is a” relationship. In our case, an Employee is a type of Person, and a Customer is a type of Person, but a Customer is not a type of Employee, and we cannot assume that a Person is a type of Employee. The last relationship might be true in some cases, which is why we need to use a cast to make it explicit in our code.


Previous - Next

© 2003, Rahul Simha (revised 2017). Further revised by Tim Wood 2018.