final Instance Variables
We covered creating constants using the final keyword in 1-3. Using the same keyword it’s possible to define a constant within an object. For example, let’s define a constant value of 1 as the default denominator value for a Fraction object:
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 37 38 39 40 41 42 43 44 45 46 47 48 49 |
public class Fraction { private int num; private int den; private final int DEFAULT_DENOMINATOR = 1; // No-Argument constructor method public Fraction() { setNum( 1 ); setDen( DEFAULT_DENOMINATOR ); } // Parameterized constructor method public Fraction( int num, int den ) { setNum( num ); setDen( 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 = DEFAULT_DENOMINATOR; } else { den = newDenominator; } } // Returns a String representation of the current Fraction object public String toString() { return num + "/" + den; } } |
The DEFAULT_DENOMINATOR constant is defined on line 4 using the final keyword. Any attempt to modify this constant in the code will give a Java compiler error. It’s also private such that it can only be accessed within the current Fraction object.
On lines 9 and 38 we now use this constant to set the value of the den instance variable. There are benefits to using constants like this. For one, constants enhance the readability of the code. Secondly, if we decide to change the default denominator value we only need to change it once on line 4 and it will have an effect on lines 9 and 38 automatically. All of this makes the code easier to maintain.
Static Methods
Recall that the Math class did not require us to instantiate a Math object to be able to call its methods. If you look in the Java API for the Math class you’ll see the reason for this. All of the Math class methods are declared as static.
Static methods in Java are special because they belong to the class and not an object (i.e., instance) of it. This means that to call a static method you only need to provide the class name followed by a dot (.) followed by the static method name you’d like to call. You do not need to instantiate an object of the class. From your experience using the Math class you can probably appreciate the convenience of this.
However, when defining your own static methods there are some limitations you need to be aware of. Since you don’t need to instantiate an object to call a static method it means that any instance variables or methods that would normally be associated with an object of the class are not accessible to static methods. In other words, a static method may not call a non-static method or access any instance variables defined in its class. This makes sense, because instance variables and non-static methods only exist as part of an object.
There is one type of variable that a static method can access, however. It’s called a static class variable.
Static Class Variables
Every object we instantiate from a class has the same instance variables but with unique values for each object. For example, every Fraction object has a num and den instance variable, but each Fraction object can have different num and den values.
Sometimes it can be useful if all objects of a class share a single value of an instance variable. This way if one object of the class changes the value of the instance variable, all other objects of the class access the same value. These kinds of variables are called static class variables. A common application of this is keeping track of the number of objects of a class that have been instantiated.
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 |
public class Fraction { private int num; private int den; private final int DEFAULT_DENOMINATOR = 1; private static int numberOfFractions = 0; // No-Argument constructor method public Fraction() { setNum( 1 ); setDen( DEFAULT_DENOMINATOR ); numberOfFractions++; } // Parameterized constructor method public Fraction( int num, int den ) { setNum( num ); setDen( den ); numberOfFractions++; } // Accessor for numberOfFractions static class variable public static int getNumberOfFractions() { return numberOfFractions; } |
On line 5 we define numberOfFractions as a private static int with an initial value of 0. The static keyword indicates that all objects of this class will share the same copy of this variable in memory. Since it’s private, an accessor has been added on lines 22 to 24. In this case, I’ve chosen to make the accessor static too so that I can call this method without having to instantiate a Fraction object to do so. I can call it directly using the class name if I choose. Making the accessor static is optional, it’s just a design decision I’ve made in this case.
Here’s a test program:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class FractionTest { public static void main( String[] args ) { System.out.println( "# of Fractions: " + Fraction.getNumberOfFractions() ); Fraction f1 = new Fraction( 2, 3 ); System.out.println( f1 ); System.out.println( "# of Fractions: " + f1.getNumberOfFractions() ); Fraction f2 = new Fraction(); System.out.println( f2 ); System.out.println( "# of Fractions: " + f2.getNumberOfFractions() ); } } |
# of Fractions: 0
2/3
# of Fractions: 1
1/1
# of Fractions: 2
On line 3 we call the static method getNumberOfFractions() by prefacing it with Fraction. followed by the method name. Because it is static, getNumberOfFractions() may only access static class variables. In this case it returns the value 0 because I have not created any Fraction objects yet.
On line 5 we instantiate our first Fraction object. The constructor will add 1 to the value of the static class variable numberOfFractions. On line 7 we call the getNumberOfFactions() method again, this time using the f1 object. We could have also used the class name instead, both ways work and return the value 1. On line 9 we instantiate a second Fraction object. Again, its constructor will add one to numberOfFractions such that line 11 will return a value of 2.
In summary, you can see that both Fraction objects share the same value of the numberOfFractions static class variable, and that the static method getNumberOfFractions can be called using the class name and/or an object of the class.
Why main() is Declared static
The test code above demonstrates something else that we have seen since our very first example in 1-1. The main() method itself is declared as static! We now understand enough about static methods to appreciate why this is so.
Ordinarily, to call a method you must have an object of a class instantiated. The problem is when the JVM needs to run the main() method no FractionTest object has been (or can be) created yet – so how can the JVM run main() then? By declaring main() as static, it allows the JVM to call main() without requiring a FractionTest object to be instantiated first. This may seem like an odd thing to do, but it solves a potential “chicken and egg” problem.
Because main() is static, any other methods in the same class as main() must also be declared static. If not, the main() method would not be able to call them.
We still have not talked about the String[] args part of the main() method signature, we’ll get to this in 3-2!
Static Final Class Variables
Recall that the Math class also included two constants PI and E. If you consult the Java API for the Math class you will see that these are both declared as public static final. By now you should understand what this means. The public keyword makes them accessible outside the Math class, static means they can be accessed without an object of the class, and final means that they are constants and may not be assigned a new value. You may define your own static final class variables in this way.
Revised UML Diagram
Here is a revised UML diagram reflecting the changes above:
Notice that final instance variables names are all UPPER_CASE just as they should be in Java code. Static class variables and methods should be underlined.
You Try!
- Consider the following class definition and its test class. Try to predict the output of the CounterTest program, then run it in Dr. Java to see if you are correct.
1234567891011121314151617public class Counter {private int counter1;private static int counter2;public void increment() {counter1++;counter2++;}public int getCounter1() {return counter1;}public int getCounter2() {return counter2;}}
1234567891011121314public class CounterTest {public static void main(String[] args) {Counter c1 = new Counter();Counter c2 = new Counter();c1.increment();c1.increment();c2.increment();System.out.println(c1.getCounter1());System.out.println(c1.getCounter2());System.out.println(c2.getCounter1());System.out.println(c2.getCounter2());}} - Draw a UML diagram for the following code:
1234567891011121314151617181920212223242526272829303132333435363738394041424344public class Student {private String name;private double finalMark;private final int STUDENT_ID;private static int nextID = 1;public static final double STARTING_MARK = 50.0;public Student(String name) {this(name, STARTING_MARK);}public Student(String name, double startingMark) {this.name = name;STUDENT_ID = getNextID();finalMark = startingMark;}public String getName() {return name;}public int getStudentID() {return STUDENT_ID;}public double getFinalMark() {return finalMark;}public void setName(String newName) {name = newName;}public void setFinalMark(double newMark) {finalMark = newMark;}public static int getNextID() {int id = nextID;nextID++;return id;}} - Continue improving your Rectangle class! Add a private static variable to keep track of the number of Rectangle objects that have been instantiated. Include an accessor method to return this value, but no mutator method so that the value cannot be manipulated by code outside the object.
As we will see later, to draw a Rectangle in Java we require the (x, y) coordinate of the upper-left corner of the rectangle, and its width and height (in pixels). Depending on the parameters passed to the constructor, the upper-left coordinate may not be (x1, y1), it could actually be (x2, y2). Write public methods called getUpperLeftX() and getUpperLeftY() that compares x1 with x2, and y1 with y2 and returns the smaller of the two values. In other words, these methods will always return the x and y coordinate of the upper-left corner of the Rectangle.
Next, write public methods called getWidth() and getHeight() that calculate and return the absolute difference between x1 and x2, and y1 and y2, respectively.
Next, write a public calcArea() method that reuses the getWidth() and getHeight() methods to calculate and return the area (in pixels) of the current rectangle object as a double value.
Finally, update the UML diagram for your Rectangle class.