Class Hierarchy Relationships
Conceptually, the Student and Person classes we have defined form an inheritance hierarchy:
The root of every class hierarchy in Java starts with the class Object, and all other classes directly or indirectly inherit from it. A direct superclass is a superclass from which a subclass explicitly inherits using the extends keyword. An indirect superclass is any class above a direct superclass in the class hierarchy. In other words, Object is a direct superclass of Person but an indirect superclass of Student, and Person is a direct superclass of Student.
Some languages, like C++, support multiple-inheritance where a child class may have more than one direct superclass, but Java only supports single-inheritance. This means that a child class may only inherit from one direct superclass (i.e., may only extend one parent class). However, a parent class can have any number of child classes. Because of this, most class hierarchies in Java resemble tree-like structures. For example, here is a simplified class hierarchy for some common Swing classes:
As shown, a JFrame inherits directly from Frame, but indirectly from Window. It’s not possible for a JPanel to directly or indirectly inherit from Window, Frame, or JFrame or any other child classes of JComponent. We’ll learn a lot more about Swing in upcoming sessions.
“Is-a” vs. “Has-a”
By design, the classes at the top of a class hierarchy are naturally more general and those at the bottom are more specific. When a child class inherits from a parent class, their association is often referred to as an “is-a” relationship. That is to say, a Student “is-a” more specific kind of Person.
A different kind of association between classes is called composition or a “has-a” relationship. In a “has-a” relationship, a class has one or more instance variables that refer to objects of other classes. This is nothing new, we’ve used this type of relationship many times already! For example, in our Person class we have a String instance variable called name. We can say that the Person class “has-a” (or refers to) a String object. Again, this is not an inheritance relationship, but a composition relationship.
Organizing Classes into Packages
You already have experience working with packages, but not creating your own. A package is a collection of related classes. As programs grow from a few classes to hundreds or even thousands of classes, packages help to organize code and make it easier to find classes. In fact, all of the classes in the Java API are organized into packages. For example, we have worked with the Scanner class from the java.util package. To use the Scanner class in our code we had to import it like so:
1 |
import java.util.Scanner; |
Another example would be the java.lang package that contains the System and Math classes. Because these classes are so commonly used in Java they are automatically imported.
Let’s create a package called “people” to store our related Person and Student classes. The first step is to create a folder entitled “people”. Next, move the Person.java and Student.java source files into that folder. Once the files are physically in place we need to make some minor code changes.
To indicate that the Person and Student classes belong to the people package, add the following line as the first line of code in each file:
1 |
package people; |
This indicates that both classes are members of the same package. Other classes in our application might similarly reside in other packages (i.e., subfolders) that we define. The main() test code for our application would reside in the folder containing the people subfolder. Next, within our test code, we need to add import statements if we intend to access either of the classes from the people package:
1 2 |
import people.Person; import people.Student; |
Once this is done we can instantiate Person and Student objects as usual. The “people” part of the import statement simply indicates the name of the subfolder where the Person and Student classes are found.
Going back to the Scanner class, it is actually located a folder called “util” which resides within another folder called “java”. The “java” folder contains all of the Java API packages (i.e., in subfolders). If needed, you may also have multiple levels of subfolders to organize your packages.
Accessibility Modifiers
So far you have been introduced to the public and private access modifiers. To recap, an instance variable or method defined as public can be directly accessed by any class (in any package) in a Java program, including child classes – there are no limits.
Because encapsulation is such an important aspect of object-oriented design, you learned to use the private keyword to effectively hide instance variables and helper methods within an object so that they can only be accessed by code within the current class and nowhere else, even any child classes or classes from other packages. The only way to access a private instance variable is through the class’ public accessor or mutator methods, if provided.
Sometimes a middle ground can be helpful. There is a third access modifier in Java called protected. An instance variable or method defined as protected can be directly accessed by code in the current class, any subclasses, and any classes in the same package but not by classes in different packages.
If you don’t specify any access modifier, this is known as package-private. By default, such instance variables or methods can be accessed within the current class, and directly by classes in the same package only.
Here’s a table to summarize:
Access Modifier |
Visible from code in the same Class | Visible from classes in the same Package | Visible from subclasses (in any package) | Visible from classes in other packages |
public | Yes | Yes | Yes | Yes |
protected | Yes | Yes | Yes | No |
(no modifier) package-private |
Yes | Yes | No | No |
private | Yes | No | No | No |
You can see that public is the least restrictive, followed by protected, then (no modifier), and finally private. In terms of good design, it’s always best to choose the most restrictive access modifier that will suit your needs.
The protected Keyword
Let’s enhance the Person and Student classes to demonstrate the protected keyword.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
package people; public class Person { private String name; protected int age; // No-argument constructor method public Person( ) { this( "No Name Given!", 0 ); } // Parameterized constructor method public Person( String name, int age ) { this.name = name; this.age = age; } // Accessor for name instance variable public String getName() { return name; } // Mutator for name instance variable public void setName( String newName ) { name = newName; } // Returns a String representation of the current Person object @Override public String toString() { return "Person: " + name + " (age: " + age + ")"; } } |
Line 1 indicates that this class is a member of the people package; therefore it’s physically saved in a subfolder called “people”. On line 5 I have declared a protected age instance variable. This means that I can access age anywhere within the Person class, such as on lines 15 or 31. So far, this is similar to a private instance variable. Let’s look at the Student class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package people; public class Student extends Person { private int studentId; // Parameterized constructor method public Student( String name, int age, int studentId ) { // super( name, age ); // BEST! setName(name); // BETTER. this.age = age; this.studentId = studentId; } // Accessor for studentId instance variable public int getStudentId() { return studentId; } // Mutator for studentId instance variable public void setStudentId( int newStudentId ) { studentId = newStudentId; } // Returns a String representation of the current Student object @Override public String toString() { return super.toString() + ", is a Student with id #" + studentId; } } |
Line 1 says that the Student class code is in the same package (i.e., subfolder) as the Person class. Notice in the constructor that to access the private name instance variable from the Person class I must use the public mutator method provided by the Person class. Because it’s private I cannot change it directly.
Line 10 is different. Here, because age was defined as protected in the Person class I am allowed to access it directly as if it was an instance variable defined in the Student class.
Finally, consider the following TestPeople code that would be located in the folder containing the people subfolder.
1 2 3 4 5 6 7 8 9 |
import people.Student; public class TestPeople { public static void main( String[] args ) { Student s = new Student( "James", 16, 123456789); System.out.println(s); s.age = 4; // ERROR! } } |
Because the TestPeople class is not in the same package (i.e., subfolder) as the Student class, I need to import the Student class from the people package on line 1. Once this is done I can instantiate a Student object as usual. Line 7 would give a syntax error because we are trying to access the protected instance variable age from a class outside the people package.
If all of this makes your head spin don’t worry too much at this point, with experience you’ll master it.
Protected Members in UML
Let’s look at a revised UML diagram for our Person and Student classes:
Notice that protected instance variables are prefaced with a # sign (recall: public uses +, and private uses –). It’s also possible to have protected methods; these would also be prefaced with a # sign in UML.
You Try!
- Reorganize your FillableShape, Rectangle, and Oval classes into a package called shape2D. Your ShapeTest class should reside outside this package (i.e., sub-folder). Add package and import statements to your classes so that you can compile your code.