In a nonobject-oriented programming language, functions are generally thought of as the most important elements: they are the means by which the program performs its operations. In an object-oriented language, the only way to write a function is
to write a class method. Whereas classes and objects provide the framework, and class variables and data members provide a way of holding that class' or object's attributes and state, it is the methods that actually provide an object's behavior and define
how that object interacts with other objects in the system.
VJ++ doesn't allow you to define a function that is not a method of a class, whereas C++ does.
Yesterday, you learned a little bit about defining methods. With what you learned yesterday, you could create some simple VJ++ programs, but you'd still be missing some characteristics of methods that make them really powerful, and that make your
objects and classes more efficient and easier to understand. Today, you'll learn about these additional characteristics of methods, including the following:
Yesterday, you learned how to create methods with a single name and a single signature. Methods in Java can also be overloaded; this means you can create methods that have the same name, but different signatures and different definitions. Method
overloading enables instances of your class to have a simpler interface to other objects (no need for entirely different methods that do essentially the same thing) and to behave differently based on the input to that method.
When you call a method in an object and choose which method definition to execute, Java matches up the method name, number of arguments, and data type (int, String, byte, and so on) of those arguments.
To create an overloaded method, create several different method definitions in your class, all with the same name, but with different parameter lists (either in the number of arguments or the type of arguments). Java allows method overloading as long
as each parameter list is unique for the same method name.
For example, you could have a Dates() class with ReturnDate methods to accept input parameters as a string, number, or a string and a Boolean. Each could return a date string formatted a specific way, or as in the last parameter example, could return
whether the data was a valid date or not.
Note that Java differentiates overloaded methods with the same name, based on the number and type of parameters to that method, not on its return type. If you try to create two methods with the same name and same parameter list, but different return
types, you'll get a compiler error. The variable names you choose for each parameter to the method are irrelevant; all that matters is the number of arguments and the data type.
Here's an example of creating an overloaded method. Listing 8.1 shows a simple class definition for a class called MyRect, which defines a rectangular shape. The MyRect class has four data members to define the upper-left and lower-right corners of the
rectangle: m_iX1, m_iY1, m_iX2, and m_iY2.
Why did we call it MyRect? Java's awt package has a class called Rectangle that implements much of this same behavior. We called this class MyRect to prevent confusion between the two classes.
Listing 8.1. The MyRect class.
class MyRect { int m_iX1 = 0; int m_iY1 = 0; int m_iX2 = 0; int m_iY2 = 0; }
When a new instance of the MyRect class is initially created, all its data members are initialized to 0. Now define a buildRect() method that takes four integer arguments (iNewX1, iNewY1, iNewX2, and iNewY2)and resizes the rectangle to have the
appropriate values for its corners, returning the resulting rectangle object.
MyRect buildRect(int iNewX1, int iNewY1, int iNewX2, int iNewY2) { m_iX1 = iNewX1; m_iY1 = iNewY1; m_iX2 = iNewX2; m_iY2 = iNewY2; return this; }
What if you wanted to have the option of defining a rectangle's dimensions in a different way? For example, you could use Point objects rather than individual coordinates. You can overload buildRect() so that its parameter list takes two Point objects.
Note that you'll need to import the Point class at the top of your source file so Java can find it.
import java.awt.Point; // top of the source file MyRect buildRect(Point pntTopLeft, Point pntBottomRight) { m_iX1 = pntTopLeft.x; m_iY1 = pntTopLeft.y; m_iX2 = pntBottomRight.x; m_iY2 = pntBottomRight.y; return this; }
A single point is represented by an x and y coordinate in two-dimensional space.
Perhaps you want to also have the ability to define the rectangle using a top corner, a width, and a height. To accomplish this, just create a different definition for buildRect():
MyRect buildRect(Point pntTopLeft, int iWidth, int iHeight) { m_iX1 = pntTopLeft.x; m_iY1 = pntTopLeft.y; m_iX2 = (m_iX1 + iWidth); m_iY2 = (m_iY1 + iHeight); return this; }
To finish this example, create a method to print out the rectangle's coordinates, and a main() method to test it all (just to prove that this does indeed work). Listing 8.2 shows the completed class definition with all of its methods.
Create a new VJ++ project workspace for the following example. Open a new code window and type the application, save the file, add it to the project, compile it, and then test it using JVIEW.
Listing 8.2. The complete MyRect class.
import java.awt.Point; class MyRect { int m_iX1 = 0; int m_iY1 = 0; int m_iX2 = 0; int m_iY2 = 0; MyRect buildRect(int iNewX1, int iNewY1, int iNewX2, int iNewY2) { m_iX1 = iNewX1; m_iY1 = iNewY1; m_iX2 = iNewX2; m_iY2 = iNewY2; return this; } MyRect buildRect(Point pntTopLeft, Point pntBottomRight) { m_iX1 = pntTopLeft.x; m_iY1 = pntTopLeft.y; m_iX2 = pntBottomRight.x; m_iY2 = pntBottomRight.y; return this; } MyRect buildRect(Point pntTopLeft, int iWidth, int iHeight) { m_iX1 = pntTopLeft.x; m_iY1 = pntTopLeft.y; m_iX2 = (m_iX1 + iWidth); m_iY2 = (m_iY1 + iHeight); return this; } void printRect() { System.out.print("MyRect: <" + m_iX1 + ", " + m_iY1); System.out.println(", " + m_iX2 + ", " + m_iY2 + ">"); } public static void main(String args[]) { MyRect rect = new MyRect(); System.out.println("Calling buildRect with coordinates 25,25 50,50:"); rect.buildRect(25, 25, 50, 50); rect.printRect(); System.out.println(""); System.out.println("Calling buildRect w/points (10,10), (20,20):"); rect.buildRect(new Point(10,10), new Point(20,20)); rect.printRect(); System.out.println(""); System.out.print("Calling buildRect w/1 point (10,10),"); System.out.println(" width (50) and height (50)"); rect.buildRect(new Point(10,10), 50, 50); rect.printRect(); System.out.println(""); } } // end of class MyRect
Here's the output of the preceding VJ++ application:
Calling buildRect with coordinates 25,25 50,50: MyRect: <25, 25, 50, 50> Calling buildRect w/points (10,10), (20,20): MyRect: <10, 10, 20, 20> Calling buildRect w/1 point (10,10), width (50) and height (50) MyRect: <10, 10, 60, 60>
As you can see from the preceding example, all of the buildRect() methods work based on the arguments with which they are called. You can define as many versions of a method as you need to in your own classes to implement each unique behavior you need
for that class.
In addition to regular methods, which are called by other methods within your own classes or from within other classes, you can also define constructor methods in your class definition. These methods are called only when a new instance (object) of your
class is created.
A constructor method is a special kind of method that determines how an object is initialized when it's created.
Unlike regular methods, you can't call a constructor method directly; instead, constructor methods are automatically called by Java when the new object is created. It's important to note here that only Java can call a constructor method. When you use
new to create a new instance of a class, Java does three things:
Even if a class doesn't have any special constructor methods defined, no errors will be generated and you will still end up with an object. But you might have to set its data members, or call other methods that the class needs to initialize itself. All
the examples you've created up to this point have not had constructor methods and, as you have seen, they have all (hopefully) worked quite well.
By defining constructor methods in your own classes, you can set initial values of data members, call methods based upon those variables, create new objects based on other classes, call methods in other objects, calculate initial properties of your
object, or perform any operation that you can within a normal method. You can also overload constructors, as you would regular methods, to create an object that has specific properties based upon the arguments passed when new is called.
Constructors look a lot like regular methods, with two basic differences:
For example, Listing 8.3 shows a simple class called Person, with a constructor that initializes its data members based upon the arguments used when new is called. Person also includes a method for the object to introduce itself, and a main() method to
test each of the other two methods.
Listing 8.3. The Person class.
class Person { String m_sName; int m_iAge; Person(String sNewName, int iNewAge) { m_sName = sNewName; m_iAge = iNewAge; } void printPerson() { System.out.print("Hi, my name is " + m_sName); System.out.println(". I am " + m_iAge + " years old."); } public static void main (String args[]) { Person psrPerson; psrPerson = new Person("Dustin", 15); psrPerson.printPerson(); System.out.println(""); psrPerson = new Person("Zachary", 10); psrPerson.printPerson(); System.out.println(""); } }
Here's the output for the preceding example program:
Hi, my name is Dustin. I am 15 years old. ---- Hi, my name is Zachary. I am 10 years old. ----
Some constructors you write might be a superset of another constructor defined in your class; they might have the same behavior plus a little bit more. Rather than duplicating identical behavior in multiple constructor methods in your class, it makes
sense to be able to just call that first constructor from inside the body of the second constructor. Java provides a special syntax for doing this. To call a constructor defined on the current class, use this form:
this(arg1, arg2, arg3...);
The arguments used in the call to this are the arguments that would be used in a call to new. Yes, this seems confusing, and it is. Take a look at the following example and this concept should make more sense:
class Student { String m_sName; int m_iAge; String m_sGrade = "na"; Student(String sNewName, int iNewAge) { m_sName = sNewName; m_iAge = iNewAge; m_sGrade = "Incomplete"; } Student(String sNewName, int iNewAge, String sGrade) { this(sNewName, iNewAge); m_sGrade = sGrade; } void printStudent() { System.out.println("Name " + m_sName + " Age " + m_iAge + " Grade " + m_sGrade); } public static void main (String args[]) { Student stdStudent; stdStudent = new Student("Liz", 20, "A+"); stdStudent.printStudent(); System.out.println(""); stdStudent = new Student("John", 19); stdStudent.printStudent(); System.out.println(""); stdStudent = new Student("Pete", 21, "C-"); stdStudent.printStudent(); System.out.println(""); stdStudent = new Student("David", 19, "D"); stdStudent.printStudent(); System.out.println(""); } }
Can you guess what the output of this system will be? Try it. The answer is located at the end of this chapter.
Like regular methods, constructors can also take varying numbers and data types as parameters. This flexibility enables you to create your object with the properties you want it to have, or to be able to calculate properties from different input forms.
For example, the buildRect() methods you defined in the MyRect class from Listing 8.2 would make excellent constructors. Why? Because what they're doing is initializing an object's data members to the appropriate values. So, instead of the original
buildRect() method you defined (which took the four corner coordinate parameters), you can create a constructor instead. Listing 8.4 shows a new class called MyRect2, that has all the same functionality as the original MyRect, except it uses overloaded
constructor methods instead of the buildRect() method.
Listing 8.4. The MyRect2 class (with constructors).
import java.awt.Point; class MyRect2 { int m_iX1 = 0; int m_iY1 = 0; int m_iX2 = 0; int m_iY2 = 0; MyRect2(int iNewX1, int iNewY1, int iNewX2, int iNewY2) { m_iX1 = iNewX1; m_iY1 = iNewY1; m_iX2 = iNewX2; m_iY2 = iNewY2; } MyRect2(Point pntTopLeft, Point pntBottomRight) { m_iX1 = pntTopLeft.x; m_iY1 = pntTopLeft.y; m_iX2 = pntBottomRight.x; m_iY2 = pntBottomRight.y; } MyRect2(Point topLeft, int w, int h) { m_iX1 = topLeft.x; m_iY1 = topLeft.y; m_iX2 = (m_iX1 + w); m_iY2 = (m_iY1 + h); } void printRect() { System.out.print("MyRect: <" + m_iX1 + ", " + m_iY1); System.out.println(", " + m_iX2 + ", " + m_iY2 + ">"); } public static void main(String args[]) { MyRect2 rect; System.out.println("Calling MyRect2 with coordinates 25,25 50,50:"); rect = new MyRect2(25, 25, 50,50); rect.printRect(); System.out.println(""); System.out.println("Calling MyRect2 w/points (10,10), (20,20):"); rect= new MyRect2(new Point(10,10), new Point(20,20)); rect.printRect(); System.out.println(""); System.out.print("Calling MyRect2 w/1 point (10,10),"); System.out.println(" width (50) and height (50)"); rect = new MyRect2(new Point(10,10), 50, 50); rect.printRect(); System.out.println(""); } } // end of class MyRect2
Here's the output for MyRect2. It's the same output as from the original MyRect; only the code to produce it has changed:
Calling MyRect2 with coordinates 25,25 50,50: MyRect: <25, 25, 50, 50> Calling MyRect2 w/points (10,10), (20,20): MyRect: <10, 10, 20, 20> Calling MyRect2 w/1 point (10,10), width (50) and height (50) MyRect: <10, 10, 60, 60>
When you call a method on an object, Java looks for that method definition in the class of that object. If it doesn't find one, it passes the method call up the class hierarchy until a method definition is found. Method inheritance enables you to
define and use methods repeatedly in subclasses without having to duplicate the code itself.
There might be times, however, when you want an object to respond to the same methods, but have different behavior than the original method in the superclass. In this case, you can override that method. Overriding a method involves defining a method in
a subclass that has the same signature as a method in a superclass. Then, when that method is called, the method in the subclass is found and executed instead of the original method in the superclass.
To override a method, create a method in your subclass that has the same signature (name, return type, and parameter list) as a method defined by one of your class' superclasses. Because VJ++ executes the first method definition it finds that matches
the signature, this effectively "hides" the original method definition. Listing 8.5 is a simple class with a method called printMe(), which prints out the name of the class and the values of its data members. It doesn't show how to override a
method, but is a setup for the following examples that do.
Listing 8.5. The PrintClass class.
class PrintClass { int m_iX = 0; int m_iY = 1; void printMe() { System.out.println("m_iX is " + m_iX + ", m_iY is " + m_iY); System.out.println("I am an instance of the class " + this.getClass().getName()); } }
Listing 8.6 shows a class called PrintSubClass that is a subclass of (extends) PrintClass. The only difference between PrintClass and PrintSubClass is that the latter has an m_iZ data member. Again, this is a setup for what's to follow.
Listing 8.6. The PrintSubClass class.
class PrintSubClass extends PrintClass { int m_iZ = 3; public static void main(String args[]) { PrintSubClass obj = new PrintSubClass(); obj.printMe(); } }
Here's the output from PrintSubClass:
X is 0, Y is 1 I am an instance of the class PrintSubClass
In the main() method of PrintSubClass, you created a PrintSubClass object and called the printMe() method. Note that PrintSubClass doesn't define the printMe() method, so Java looks for it in each of PrintSubClass' superclassesand finds it, in
this case, in PrintClass. Unfortunately, because printMe() is still defined in PrintClass, it doesn't print the m_iZ data member.
Now create a third class. PrintSubClass2 is nearly identical to PrintSubClass, but you override the printMe() method to include the m_iZ variable. Listing 8.7 shows this class and, in combination with the two previous examples, shows how to override a
method.
Listing 8.7. The PrintSubClass2 class.
class PrintSubClass2 extends PrintClass { int m_iZ = 3; void printMe() { System.out.println("m_iX is " + m_iX + ", m_iY is " + m_iY + ", m_iZ is " + m_iZ); System.out.println("I am an instance of the class " + this.getClass().getName()); } public static void main(String args[]) { PrintSubClass2 obj = new PrintSubClass2(); obj.printMe(); } }
Now when you instantiate this class and call the printMe() method, the version of printMe() you defined for PrintSubClass2 is called instead of the one in the superclass PrintClass (as you can see in this output):
m_iX is 0, m_iY is 1, m_iZ is 3 I am an instance of the class PrintSubClass2
Usually, there are two reasons why you want to override a method that a superclass has already implemented:
You've already learned about the first one; by overriding a method and giving that method a new definition, you've hidden the original method definition. But sometimes you might just want to add behavior to the original definition rather than hide it
and ignore its intended functionality. VJ++'s capability to do exactly that is particularly useful when you would otherwise end up duplicating behavior in both the original method and the method that overrides it. VJ++ gives you the ability to call the
original method in the body of the overridden method so that you can add only what additional functionality is required.
To call the original method from inside a method definition, use the super keyword to pass the method call up the hierarchy of classes. Here is a quick example of the concept:
void myMethod (String a, String b) { // do stuff here super.myMethod(a, b); // call the myMethod method in the superclass(s) // maybe do more stuff here }
The super keyword, which is like the this keyword, is a placeholder for the preceding class' superclass. You can use it anywhere you can use this, but it refers to the superclass rather than to the current object.
For example, Listing 8.8 shows the printMe() methods used in the previous example.
Listing 8.8. The printMe methods.
// from PrintClass void printMe() { System.out.println("m_iX is " + m_iX + ", m_iY is " + m_iY); System.out.println("I am an instance of the class" + this.getClass().getName()); } //from PrintSubClass2 void printMe() { System.out.println("m_iX is " + m_iX + ", m_iY is " + m_iY + ", m_iZ is " + m_iZ); System.out.println("I am an instance of the class " + this.getClass().getName()); }
Rather than duplicating most of the behavior of the superclass' method in the subclass, you can rearrange the superclass' method so that additional behavior can easily be added in its subclasses:
// from PrintClass void printMe() { System.out.println("I am an instance of the class" + this.getClass().getName()); System.out.println("m_iX is " + m_iX); System.out.println("m_iY is " + m_iY); }
Then, in the subclass, when you override printMe, you can merely call the original method and then add the code to print the additional data members that have been added to the new class:
// From PrintSubClass2 void printMe() { super.printMe(); System.out.println("m_iZ is " + m_iZ); }
The following is the output of calling printMe() on an instance of the subclass:
I am an instance of the class PrintSubClass2 m_iX is 0 m_iY is 1 m_iZ is 3
Constructors cannot technically be overridden. Because they always have the same name as the current class, you're always creating new constructors instead of inheriting the ones defined in superclasses. Most of the time this is fine, because when your
class' constructor is called, the constructor with the same signature as all of your superclasses is also called. This insures proper initialization of your inherited subclass.
However, when you're defining constructors for your own class, you might want to change how your object is initialized, not only by initializing new variables your class adds, but also by changing the contents of variables that are already there. You
do this by explicitly calling your superclass' constructors, and then changing whatever you like.
To call a regular method in a superclass, you use super.methodname(arguments). This, of course, cannot work with constructors because there is not a method name to call. Instead, you use the super keyword again.
super(arg1, arg2, ...);
Similar to using this(...) in a constructor, super(...) calls the constructor method for the immediate superclass. (This might, in turn, call the constructor of its superclass, and so on.)
For example, Listing 8.9 shows a class called NamedPoint, which extends the class Point from Java's awt package. The Point class has only one constructor, which takes an x and a y argument and returns a Point object. NamedPoint has an additional data
member (a string for the name) and defines a constructor to initialize x, y, and the name.
Listing 8.9. The NamedPoint class.
1: import java.awt.Point; 2: 3: class NamedPoint extends Point 4: { 5: String m_sName; 6: 7: NamedPoint(int iX, int iY, String sName) 8: { 9: super(iX,iY); 10: m_sName = sName; 11: } 12: }
The constructor defined here for NamedPoint (lines 7 through 11) calls Point's constructor method to initialize Point's data members (x and y). Although you can just as easily initialize x and y yourself, you might not know what other things Point is
doing to initialize itself, so it's always a good idea to pass constructors up the hierarchy to make sure everything is set up correctly.
Passing constructors up the hierarchy also helps to ensure that each class provides only the functionality specific to that class. For example, suppose you are developing Class B and Jeff is developing Class A. Also suppose that Class B is inherited
from Class A. (Class A is Class B's superclass.) If Jeff needs to add a variable to Class A that needs to be initialized at startup, all Jeff has to do is modify Class A with the new data and the changes will automatically be reflected in Class B (via
inheritance). However, if you have modified the constructor in Class B, so as not to call the original constructor in Class A, the new variable that Jeff defined for Class A will not be initialized and your application will not work until you modify Class
B to incorporate the new variable.
Finalizer methods are the opposite of constructor methods; whereas a constructor method is used to initialize an object, finalizer methods are called just before the object is garbage-collected and its memory reclaimed.
The finalizer method is simply finalize(). The class Object, from which all other classes in VJ++ are inherited, defines a default finalizer method, which does nothing. To create a finalizer method for your own classes, override the finalize() method
using this signature:
protected void finalize() { ... }
Include any cleaning up you want to do for that object inside the body of that finalize() method. If necessary, you can also call super.finalize() to allow your class' superclasses to finalize your object.
You can always call the finalize() method yourself at any time; it's simply a method like any other. However, calling finalize() does not force an object to be removed from memory and garbage-collected. Only removing all references to an object will
cause it to be marked for deletion.
Finalizer methods are best used for optimizing the removal of an object, such as removing references to other objects or releasing external resources that have been acquired. They might also be used for other behaviors that make it easier for that
object to be removed. In most cases, you will not need to use finalize() at all. See Day 21 for more about garbage collection and finalize().
Today, you learned all kinds of techniques for using, reusing, defining, and redefining methods. You learned how to overload a method, so that the same method can have different behaviors based on the arguments with which it's called. You learned about
constructor methods, which are used to initialize a new object when it's created. You learned about method inheritance and how to override methods that have been defined in a class' superclasses. Finally, you learned about finalizer methods that can be
used to clean up after an object, just before that object is garbage-collected and its memory reclaimed.
Q: I created two methods with the following signatures:
int total(int arg1, int arg2, int arg3) {...} float total(int arg1, int arg2, int arg3) {...}
The Java compiler complains when I try to compile the class with these method definitions. But their signatures are different. What have I done wrong?
A: Method overloading in Java works only if the parameter lists are differenteither in the number of arguments or the data type of the arguments. Return type is not relevant for method overloading. Java (like any non-cognitive machine)
gets confused. It doesn't know which method to call when it sees two methods with exactly the same parameter list. So, instead of staying confused, the compiler throws it back to you, with your cognitive brain, for you to help Java out of its quandary.
Q: Can I overload overridden methods? (In other words, can I create methods that have the same name as an inherited method, but a different parameter list?)
A: Sure! As long as the parameter lists vary, it doesn't matter whether you've defined a new method name or one that you've inherited from a superclass.
A: Answer to the code in the section Calling Another Constructor.
Name Liz Age 20 Grade A+ ---- Name John Age 19 Grade Incomplete ---- Name Pete Age 21 Grade C- ---- Name David Age 19 Grade D ----