Instance variables and local variables are different in a few important ways. Local variables are defined within a method and are only visible to code within that method. Instance variables are defined at a class-level (outside methods) and are visible to any method within the class and even outside the class unless we properly encapsulate them.
Once a local variable has been declared it has an undefined value until we assign it an initial value. Instance variables are different here too. When you instantiate an object, its instance variables are automatically given default initial values. Let’s experiment with our Fraction class again:
> Fraction f = new Fraction()
> f.getNum()
0
> f.getDen()
0
In this case we see that our integer num and den instance variables are automatically initialized to 0. We did not have to worry about this in our code, Java provides a special method called a default constructor to handle this initialization for us. It is this default constructor, Fraction() that is being called in the first line above after the keyword new.
Instance variables of different data types have different starting values: int gets set to 0, boolean to false, double to 0.0, and any object reference variables get set to null.
Constructors
Sometimes these default values are fine, and definitely safer than being undefined, but often it would be better if we could control what our instance variables get initialized to when we create the object. For example, it would be better if den did not have a default value of 0. To do this we can provide our own constructor method. Add the following method to our Fraction class:
1 2 3 4 |
public Fraction( int initialNum, int initialDen ) { num = initialNum; den = initialDen; } |
This kind of constructor is called a parameterized constructor because it accepts parameters to initialize the starting values of instance variables in the object.
Style-wise, it makes sense for constructor methods to be the first ones declared in a class (after declaration of instance variables) so that they are easy to spot. Like accessor and mutators methods, constructor methods must be declared public to be callable when instantiating an object. You’ll notice that there is no return type for a constructor, not even void. The name of the constructor must match the class name exactly including case.
To call this constructor in main() we can instantiate a Fraction object like so:
1 |
Fraction f = new Fraction(2,3); |
The new keyword instantiates a Fraction object in memory. The constructor call Fraction(2,3) initializes the num and den instance variables, respectively. The assignment (=) symbol assigns a reference (memory address) of the new Fraction object to the reference variable f. We can then use this reference variable to access the other public methods of the Fraction object as usual.
It’s important to know that if you write you own constructor, Java no longer allows you to call the default constructor. So we can no longer do this:
1 |
Fraction f = new Fraction(); // ERROR! (Default constructor not provided automatically anymore) |
The Java compiler will complain that there is no such method defined. If you still want to have the default constructor behaviour, you can provide an overloaded constructor method called a no-argument constructor. For example, you could add this method:
1 2 3 4 |
public Fraction() { num = 1; den = 1; } |
You don’t need to initialize all of your instance variables within a constructor, only those that you want to have a different starting value than 0 (for int), false (for Boolean), 0.0 (for double), and null for reference variables. Java will still initialize any instance variables that you don’t with these starting values.
The this Keyword
Consider the following code and see if you can spot the ambiguity:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class Fraction { private int num; private int den; // No-Argument constructor method public Fraction() { num = 1; den = 1; } // Parameterized constructor method public Fraction( int num, int den ) { num = num; den = den; } |
The ambiguity is within the paramaterized constructor. The problem is that we have instance variables called num and den, and parameter variables with the same names. So, in line 13, which num is num referring to – the parameter variable num, or the instance variable num? It turns out that the parameter variable takes precedence over the instance variable. So the statement num = num is actually reassigning the parameter variable the same value it contains. Since the parameter variables num and den no longer exist when the constructor exits, lines 13 and 14 actually do nothing useful.
To solve this problem, Java includes a special keyword called this.
1 2 3 4 5 |
// Parameterized constructor method public Fraction( int num, int den ) { this.num = num; this.den = den; } |
The this keyword is used to refer to things within the current object. So, this.num refers to the instance variable num, not the parameter variable. Now the code makes sense and actually does what we intended!
Reusing Constructors
By this point you understand that code reuse is an important theme in programming. The this keyword can also be used to refer to other constructors with in the current object. This might sound like an odd idea, but it’s actually very useful because it means that we can call one constructor from another like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class Fraction { private int num; private int den; // No-Argument constructor method public Fraction() { this( 1, 1 ); } // Parameterized constructor method public Fraction( int num, int den ) { this.num = num; this.den = den; } |
One line 7 we use the this keyword to call the parameterized constructor to initialize num and den to 1 – code reuse in action!
Reusing Mutators
You may have noticed in our parameterized constructor that we didn’t do any sanity checking to ensure that the denominator is not 0. We already have code that will do this in our setDen() mutator method. Rather than just cut-and-paste the code from setDen() into our constructor, let’s reuse the setDen() method by calling it from our constructor:
1 2 3 4 |
public Fraction( int num, int den ) { setNum( num ); setDen( den ); } |
This is another great example of code reuse. The setDen() method contains 7 lines of code that we can take advantage of in our constructor. If we ever need to improve our sanity checking code we only need to focus on the setDen() method and any other methods that call it will benefit from the changes.
Introducing UML
When designing object-oriented code, professional developers often use a graphical notation to represent classes and their relationships in a standardized way. The industry standard is called Unified Modeling Language (UML).
In UML, each class is drawn as a rectangle with three horizontal compartments. Here is the UML diagram for our Fraction class:
The top section contains the name of the class (centered, bold). The middle compartment contains the instance variables num and den (i.e., attributes). The minus (–) sign indicates that these two instance variables are private and therefore cannot be directly accessed from outside the object. As we know, it’s usually undesirable to have public instance variables, but if we did they would be prefaced with the plus (+) sign indicating this.
The bottom compartment lists the methods defined by the class. UML is not specific to any particular O-O language, including Java, so the format of method signatures is a little different than we are used to. Here our method names are prefaced with a plus (+) sign to indicate that they are public. The return type for void method is left blank in UML, and constructors are commonly represented like void methods.
We have not seen an example of this yet, but it’s possible to have some methods that are declared private, prefaced with a minus (-) sign in UML. Although private methods cannot be called outside the current object they can be called by other methods within the object. For this reason, private methods are often called helper methods. We’ll see examples of helper methods later.
You Try!
- Create a class called Date that includes three pieces of information as instance variables — a month (type int), a day (type int), and a year (type int). Your class should have a constructor that initializes all three instance variables. Provide a set and get method for each instance variable. In your constructor and mutators, do some basic sanity checking to ensure that the argument(s) passed make sense. For example, a month should be between 1 and 12, day between 1 and 31, and the year should be a positive 4 digit value — if not, set these instance variables to some reasonable values. Provide a toString() method that returns the date as a string in the format “dd/mm/yy”. Write a test application named DateTest that demonstrates class Date‘s capabilities. Draw a UML diagram for your Date class.
- Enhance the Rectangle class you worked on last time by writing a parameterized constructor to initialize the x1, y1, x2, y2, and filled instance variables. Don’t forget to reuse the sanity checking provided by your mutator methods. Also write a no-argument constructor that sets x1, y1, x2, and y2 to 0 and the filled attribute to false. Reuse your parameterized constructor for efficiency using this(). Update main() in your RectangleTest class to demonstrate the functionality of both constructors. Create a UML diagram for your Rectangle class.