Java is what is called an object-oriented language. We saw right from our first example in 1-1 that Java requires methods to be organized into classes. We have also seen with some classes (e.g., Scanner) that we had to create an object (using the new keyword) of the class to be able to call the methods defined by it. This is what Object-Oriented Programming (OOP) is all about – designing classes that contain related data and methods, from which we can then instantiate objects to solve problems by calling their methods.
But what about classes like Math, where we did not need to instantiate a Math object to access the methods in the class? And why don’t we need to instantiate our class that contains the main() method? These are excellent questions! I have intentionally left these details out because the programs we have written have been simple enough that we did not need to burden ourselves with the details.
Until now! As programs become larger and more complex, having a single class is rarely enough. We need to know how to define our own classes, just like we learned to define our own methods. The benefits of OOP are similar to the benefits of using methods, especially code reuse. In the next series of topics we’ll cover everything you need to know about OOP with Java.
The Three Pillars of OOP
For a language like Java to be called object-oriented, it needs to support 3 important characteristics:
- Encapsulation: This means combining data and methods together within an object. The goal of encapsulation is to carefully control how the data within the object is accessed or modified by code outside the object.
- Inheritance: This means that we can define a new (“child”) class based on an existing (“parent”) class. The new class is similar to the original class but adds more specialized data and methods to make the new class more specialized.
- Polymorphism: It’s possible for a child class to redefine a method that was inherited from its parent class, such that both the child and parent have a method with the exact same signature. We’ll see later how the JVM knows which version of the method to call.
Don’t stress too much about these for now, all will become clear through examples. We’ll come back to these definitions again later.
Classes and Objects
Let’s start with an analogy. If you were to build a house the first thing you would need is a blueprint, you would not just run out and start hammering boards together. A blueprint is a carefully thought-out design or description of what a physical house should look like. Obviously, you can’t actually live in a blue print, but you could use it to construct one or more real houses.
Similarly, a class is like a blueprint and an object is an instance of a class. A class describes the data and methods that an object of that class will have when instantiated. A class on its own is not very useful to us, but objects are because we can use them to access the methods they provide to do useful things in our code.
In OOP terminology, the data (variables) within an object are often referred to as attributes, and the methods are called behaviours.
Encapsulation
Encapsulation means that we are combining data and methods together within an object. Since a class is a collection of related methods, it makes a lot of sense that one would include the data that those methods operate on as part of the same class.
In math, a fraction is an expression of the form num / den along with various operations that we can apply to fractions such as addition, multiplication, and comparison.
Let’s define a Fraction class:
1 2 3 4 5 6 7 8 9 |
public class Fraction { public int num; public int den; // Display the fraction on screen public void printIt() { System.out.println( num + " / " + den ); } } |
Line 1 defines the name of our new class. Lines 2 and 3 define two variables that will be used to represent the numerator and denominator of our Fraction. These variables are called instance variables because they are defined at the class level, outside of any methods. Each instance of an object of the Fraction class will have these two instance variables, although the data values they store can be different.
The scope of an instance variable is from the line it is declared until the closing } of the class. This means that instance variables can be directly accessed by any method of the class. So far I have only defined one method called printIt() to display the fraction. Try compiling this class.
The public keyword indicates that the instance variables and the method are directly accessible by code outside a Fraction object. Once the class is compiled we can conveniently interact with it in Dr. Java:
> Fraction f = new Fraction()
> f.printIt()
0 / 0
> f.num = 2
2
> f.den = 3
3
> f.printIt()
2 / 3
Obviously it’s important to define methods within a class as public so that they can be called from outside the object, but the opposite is true for instance variables. Consider the following interaction:
> Fraction f = new Fraction()
> f.printIt()
0 / 0
> f.num = 2
2
> f.den = 0
0
> f.printIt()
2 / 0
In math, the value of 2 / 0 is undefined! As the designers of this class, it’s important that we do whatever we can to ensure the integrity of the data encapsulated within.
A real-world analogy of encapsulation would be a digital alarm clock. Inside the clock are thousands of electronic components and data to keep track of time. However, these internal components are not directly accessible to users of the clock. Instead, the manufacturer encloses them in a plastic box and provides a few buttons that the user can interact with to manipulate the clock settings in a well-defined, controlled way.
Proper encapsulation means that we need to control how we allow the variables within an object of our class to be accessed and/or modified by code outside the object. To achieve this we declare the instance variables as private instead of public.
1 2 |
private int num; private int den; |
This change will have no impact on the printIt() method because it’s within the same class as num and den. But, if we now try to access num or den from outside the object we are not allowed:
> Fraction f = new Fraction()
> f.num = 2
Static Error: No field in Fraction has name ‘num’
> System.out.println(f.den)
Static Error: No field in Fraction has name ‘den’
The error messages we are getting indicate that it is no longer possible to access num and den directly anymore. In terms of proper class design, this is a good thing. The public and private keywords are called access modifiers because they control whether something can be accessed from code outside the object (public) or only code within it (private).
Accessors and Mutators
Now that we have effectively hidden the num and den instance variables within the Fraction class using the private keyword, we need to provide public methods that will allow access and/or modify this data from outside the object in a controlled way.
To allow an instance variable to be accessed (read) from an object we must include a special method called an accessor. And to enable controlled modification of an instance variable we must include a special method called a mutator. Let’s add these to our revised Fraction 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 30 31 32 33 34 35 36 |
public class Fraction { private int num; private int den; // Accessor for numerator instance variable public int getNum() { return num; } // Accessor for denominator instance variable public int getDen() { return den; } // Mutator for numerator instance variable public void setNum(int newNumerator) { num = newNumerator; } // Mutator for denominator instance variable public void setDen(int newDenominator) { // Sanity checking ensures a non-zero denominator if (newDenominator == 0) { System.err.println( "Attempt to set denominator to 0 ignored, setting to 1 by default." ); den = 1; } else { den = newDenominator; } } // Display the fraction on screen public void printIt() { System.out.println( num + " / " + den ); } } |
There are two accessor methods on lines 5 to 13, one for each instance variable. In terms of programming style, an accessor method name should always begin with the word get followed by the name of the variable it is associated with, using proper camelCase. Notice that both methods are declared as public. Declaring them private would not make sense as they would not be callable from outside the object, the whole point of an accessor! They also both return an int that matches the data type of our num and den instance variables.
The mutator methods are declared on lines 15 through 30. Similar to an accessor, a mutator method name should always begin with the word set followed by the name of the variable it is associated with, using camelCase. Both mutator methods need to be declared as public, and they are both void because their role is not to return a value but to change one of our instance variables. In the case of setNum() the job is very straightforward, the numerator can be any valid integer.
But in the case of setDen() we want to ensure that a user of our class can never set the den variable to 0. To accomplish this we add some logic to the setDen() method to check the parameter and if it’s 0 we report this to the user and default the den to 1 instead. This kind of logic is called sanity checking and is very important for mutator methods.
As the designer of a class you don’t have to provide accessor and mutator methods for all of your instance variables – only the ones that you’d like code outside the object to be able to interact with. Even though accessor and mutator methods tend to be very short it’s proper style to provide a comment for each one.
The toString() Method
The first method that we included in our Fraction class was the printIt() method. Being able to display the data within an object in a readable way is a handy feature for nearly any class. Even if you don’t need to use such a method in your finished product it can be very helpful for debugging purposes.
Java supports a special method called the toString() method whose sole purpose is to return the String representation of an object. That String can then be printed or referred to later in a program. Let’s replace the printIt() method with a toString() method:
1 2 3 4 |
// Returns a String representation of the current Fraction object public String toString() { return num + "/" + den; } |
The method signature must be exactly as shown above. It must be public to be accessible outside the object, it must return a String, it must be called toString, and it must not accept any parameters. Design-wise you should never print anything in a toString() method, always return a String.
To call an object’s toString() method you only need to state the name of its reference variable in your code. The JVM will automatically call toString() for you if it exists within the object. Here’s our revised test program:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class FractionTest { public static void main(String[] args) { Fraction f = new Fraction(); f.setNum(2); System.out.println( "Numerator was just set to " + f.getNum() ); f.setDen(0); // defaults to 1 System.out.println( "Denominator was just set to " + f.getDen() ); System.out.println( "toString() method returned: \"" + f + "\"" ); f.setDen(3); System.out.println(f); } } |
On line 9 and 11 when we use the reference variable f in the println() statement the toString() method is called and the String it returns is displayed.
If an object does not have a toString() method, and you try to print the object as above, what will be printed is the memory address of the object instead – not very useful information. Try removing the toString() method code and run the test program again to see what happens.
You Try!
- Explain the difference between a local variable, parameter variable, and instance variable in terms of: (a) their purpose, (b) where they are defined, and (c) their scope.
- Why is encapsulation so important to the Object-Oriented Programming paradigm?
- Add a method to our Fraction class called toDouble(). The method should return a double value representation of the current Fraction object.
- Every pixel (dot) on the screen has an (x, y) coordinate. In Java, screen coordinates start from (0, 0) in the top-left corner, x-coordinates increase to the right, and y-coordinates increase downward.Later in this course we are going to create a simple paint program with a mouse-controlled graphical user interface (GUI). The program will support the drawing of various 2D shapes, including rectangles.Create a class called Rectangle with properly encapsulated integer coordinate values for x1, y1, x2, y2, as well as a boolean instance variable called filled to keep track of whether the current Rectangle object should be filled-in or not.Create accessor and mutator methods for all five private instance variables, as well as a toString() method. Your mutator methods should ensure that x1, y1, x2, and y2 are not set to values smaller than 0. If an attempt is made to do this, report it using System.err and default the invalid coordinate to 0 instead.Finally, write a RectangleTest class to demonstrate how to instantiate a Rectangle object, set its attributes, and display information about the object.