by Laura Lemay
Methods are arguably the most important part of any object-oriented language. Whereas classes and objects provide the framework, and class and instance variables provide a way of holding that class's or object's attributes, the methods actually provide an object's behavior and define how that object interacts with other objects in the system.
Yesterday you learned a little about defining methods. With what you learned yesterday, you could create lots of Java programs, but you'd be missing some of the features 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 features, 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-that is, you can create methods that have the same name, but different signatures and different definitions. Method overloading allows instances of your class to have a simpler interface to other objects (no need for entirely different methods with different names that do essentially the same thing) and to behave differently based on the input to that method. For example, an overloaded draw() method could be used to draw just about anything, whether it were a circle or a point or an image. The same method name, with different arguments, could be used for all cases.
When you call a method in an object, Java matches up the method
name and the number and type of arguments to choose which method
definition to execute.
New Term |
Method overloading is creating multiple methods with the same name but with different signatures and definitions. Java uses the number and type of arguments to choose which method definition to execute. |
To create an overloaded method, all you need to do is create several different method definitions in your class, all with the same name, but with different parameter lists (either in number or type of arguments). Java allows method overloading as long as each parameter list is unique for the same method name.
Note that Java differentiates overloaded methods based on the number and type of parameters to that method, not on the method's return type. That is, 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. Also, the variable names you choose for each parameter to the method are irrelevant-all that matters is the number and the type.
Here's an example of creating an overloaded method. Listing 7.1
shows a simple class definition for a class called MyRect,
which defines a rectangular shape. The MyRect
class has four instance variables to define the upper-left and
lower-right corners of the rectangle: x1,
y1, x2,
and y2.
Note |
Why did I call it MyRect instead of just Rectangle? The java.awt package has a class called Rectangle that implements much of this same behavior. I called this class MyRect to prevent confusion between the two classes. |
Listing 7.1. The MyRect class.
1: class MyRect { 2: int x1 = 0; 3: int y1 = 0; 4: int x2 = 0; 5: int y2 = 0; 6: }
Note |
Don't try to compile this example yet. Actually, it'll compile just fine, but it won't run because it doesn't (yet) have a main() method. When you're finished building this class definition, the final version can be compiled and run. |
When a new instance of the myRect class is initially created, all its instance variables are initialized to 0. Let's define a buildRecpt() method that takes four integer arguments and "resizes" the rectangle to have the appropriate values for its corners, returning the resulting rectangle object (note that because the arguments have the same names as the instance variables, you have to make sure to use this to refer to them):
MyRect buildRect(int x1, int y1, int x2, int y2) { this.x1 = x1; this.y1 = y1; this.x2 = x2; this.y2 = y2; return this; }
What if you want to define a rectangle's dimensions in a different way-for example, by using Point objects rather than individual coordinates? You can overload buildRect() so that its parameter list takes two Point objects (note that you'll also need to import the java.awt.Point class at the top of your source file so Java can find it):
MyRect buildRect(Point topLeft, Point bottomRight) { x1 = topLeft.x; y1 = topLeft.y; x2 = bottomRight.x; y2 = bottomRight.y; return this; }
Perhaps you want to define the rectangle using a top corner and a width and height. You can do that, too. Just create a different definition for buildRect():
MyRect buildRect(Point topLeft, int w, int h) { x1 = topLeft.x; y1 = topLeft.y; x2 = (x1 + w); y2 = (y1 + h); return this; }
To finish up this example, let's create a method-called printRect()-to print out the rectangle's coordinates, and a main() method to test it all (just to prove that this does indeed work). Listing 7.2 shows the completed class definition with all its methods: three buildRect() methods, one printRect(), and one main().
Listing 7.2. The complete MyRect class.
1:import java.awt.Point; 2: 3:class MyRect { 4: int x1 = 0; 5: int y1 = 0; 6: int x2 = 0; 7: int y2 = 0; 8: 9: MyRect buildRect(int x1, int y1, int x2, int y2) { 10: this.x1 = x1; 11: this.y1 = y1; 12: this.x2 = x2; 13: this.y2 = y2; 14: return this; 15: } 16: 17: MyRect buildRect(Point topLeft, Point bottomRight) { 18: x1 = topLeft.x; 19: y1 = topLeft.y; 20: x2 = bottomRight.x; 21: y2 = bottomRight.y; 22: return this; 23: } 24: 25: MyRect buildRect(Point topLeft, int w, int h) { 26: x1 = topLeft.x; 27: y1 = topLeft.y; 28: x2 = (x1 + w); 29: y2 = (y1 + h); 30: return this; 31: } 32: 33: void printRect(){ 34: System.out.print("MyRect: <" + x1 + ", " + y1); 35: System.out.println(", " + x2 + ", " + y2 + ">"); 36: } 37: 38: public static void main(String args[]) { 39: MyRect rect = new MyRect(); 40: 41: System.out.println("Calling buildRect with coordinates 25,25 50,50:"); 42: rect.buildRect(25, 25, 50, 50); 43: rect.printRect(); 44: System.out.println("----------"); 45: 46: System.out.println("Calling buildRect w/points (10,10), (20,20):"); 47: rect.buildRect(new Point(10,10), new Point(20,20)); 48: rect.printRect(); 49: System.out.println("----------"); 50: 51: System.out.print("Calling buildRect w/1 point (10,10),"); 52: System.out.println(" width (50) and height (50):"); 53: 54: rect.buildRect(new Point(10,10), 50, 50); 55: rect.printRect(); 56: System.out.println("----------"); 57: } 58: }
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 this example, all 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 the behavior you need for that class.
In addition to regular methods, you can also define constructor methods in your class definition. Constructor methods are used to initialize new objects when they're created. Unlike regular methods, you can't call a constructor method by calling it directly; instead, constructor methods are called by Java automatically when you create a new object. As you learned on Day 4, "Working with Objects," when you use new, Java does three things:
New Term |
Constructor methods are special methods that are called automatically by Java to initialize a new object. |
If a class doesn't have any special constructor methods defined, you'll still end up with a new object, but you might have to set its instance variables or call other methods that the object needs to initialize itself. All the examples you've created up to this point have behaved like this.
By defining constructor methods in your own classes, you can set initial values of instance variables, call methods based on those variables or on other objects, or calculate initial properties of your object. You can also overload constructors, as you would regular methods, to create an object that has specific properties based on the arguments you give in the new expression.
Constructors look a lot like regular methods, with two basic differences:
For example, Listing 7.3 shows a simple class called Person. The constructor method for Person takes two arguments: a string object representing a person's name and an integer for the person's age.
Listing 7.3. The Person class.
1: class Person { 2: String name; 3: int age; 4: 5: Person(String n, int a) { 6: name = n; 7: age = a; 8: } 9: 10: void printPerson() { 11: System.out.print("Hi, my name is " + name); 12: System.out.println(". I am " + age + " years old."); 13: } 14: 15: public static void main (String args[]) { 16: Person p; 17: p = new Person("Laura", 20); 18: p.printPerson(); 19: System.out.println("--------"); 20: p = new Person("Tommy", 3); 21: p.printPerson(); 22: System.out.println("--------"); 23: } 24:}
Hi, my name is Laura. I am 20 years old. -------- Hi, my name is Tommy. I am 3 years old. --------
The person class has three methods: The first is the constructor method, defined in lines 5 to 8, which initializes the class's two instance variables based on the arguments to new. The Person class also includes a method called printPerson() so that the object can "introduce" itself, and a main() method to test each of these things.
Some constructors you write may be supersets of other constructors defined in your class; that is, 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 the this keyword as if it were a method name, with the arguments just after it, like this:
this(arg1, arg2, arg3...);
The arguments to this() are, of course, the arguments to the constructor.
Like regular methods, constructors can also take varying numbers and types of parameters, enabling you to create your object with exactly the properties you want it to have, or for it to be able to calculate properties from different kinds of input.
For example, the buildRect() methods you defined in the MyRect class earlier today would make excellent constructors because they're initializing an object's instance variables to the appropriate values. So, for example, instead of the original buildRect() method you had defined (which took four parameters for the coordinates of the corners), you could create a constructor instead. Listing 7.4 shows a new class, MyRect2, that has all the same functionality of the original MyRect, except with overloaded constructor methods instead of the overloaded buildRect() method. The output shown at the end is also the same output as for the previous MyRect class; only the code to produce it has changed.
Listing 7.4. The MyRect2 class (with constructors).
1: import java.awt.Point; 2: 3: class MyRect2 { 4: int x1 = 0; 5: int y1 = 0; 6: int x2 = 0; 7: int y2 = 0; 8: 9: MyRect2(int x1, int y1, int x2, int y2) { 10: this.x1 = x1; 11: this.y1 = y1; 12: this.x2 = x2; 13: this.y2 = y2; 14: } 15: 16: MyRect2(Point topLeft, Point bottomRight) { 17: x1 = topLeft.x; 18: y1 = topLeft.y; 19: x2 = bottomRight.x; 20: y2 = bottomRight.y; 21: } 22: 23: MyRect2(Point topLeft, int w, int h) { 24: x1 = topLeft.x; 25: y1 = topLeft.y; 26: x2 = (x1 + w); 27: y2 = (y1 + h); 28: } 29: 30: void printRect() { 31: System.out.print("MyRect: <" + x1 + ", " + y1); 32: System.out.println(", " + x2 + ", " + y2 + ">"); 33: } 34: 35: public static void main(String args[]) { 36: MyRect2 rect; 37: 38: System.out.println("Calling MyRect2 with coordinates 25,25 50,50:"); 39: rect = new MyRect2(25, 25, 50,50); 40: rect.printRect(); 41: System.out.println("----------"); 42: 43: System.out.println("Calling MyRect2 w/points (10,10), (20,20):"); 44: rect= new MyRect2(new Point(10,10), new Point(20,20)); 45: rect.printRect(); 46: System.out.println("----------"); 47: 48: System.out.print("Calling MyRect2 w/1 point (10,10)"); 49: System.out.println(" width (50) and height (50):"); 50: rect = new MyRect2(new Point(10,10), 50, 50); 51: rect.printRect(); 52: System.out.println("----------"); 53: 54: } 55: }
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 an object's method, Java looks for that method definition in the class of that object, and if it doesn't find a match with the right signature, it passes the method call up the class hierarchy until a definition is found. Method inheritance means that you can use methods in subclasses without having to duplicate the code.
However, there may be times when you want an object to respond to the same methods but have different behavior when that method is called. 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 one in the superclass.
To override a method, all you have to do is 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's superclasses. Because Java executes the first method definition it finds that matches the signature, this effectively "hides" the original method definition. Here's a simple example; Listing 7.5 shows a simple class with a method called printMe(), which prints out the name of the class and the values of its instance variables.
Listing 7.5. The PrintClass class.
1: class PrintClass { 2: int x = 0; 3: int y = 1; 4: 5: void printMe() { 6: System.out.println("x is " + x + ", y is " + y); 7: System.out.println("I am an instance of the class " + 8: this.getClass().getName()); 9: } 10: }
Listing 7.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 a z instance variable.
Listing 7.6. The PrintSubClass class.
1: class PrintSubClass extends PrintClass { 2: int z = 3; 3: 4: public static void main(String args[]) { 5: PrintSubClass obj = new PrintSubClass(); 6: obj.printMe(); 7: } 8: }
x is 0, y is 1 I am an instance of the class PrintSubClass
In the main() method of PrintSubClass,
you create a PrintSubClass
object and call the printMe()
method. Note that PrintSubClass
doesn't define this method, so Java looks for it in each of PrintSubClass's
superclasses-and finds it, in this case, in PrintClass.
Unfortunately, because printMe()
is still defined in PrintClass,
it doesn't print the z instance
variable.
Note |
There's an important feature of PrintClass I should point out: It doesn't have a main() method. It doesn't need one; it isn't an application. PrintClass is simply a utility class for the PrintSubClass class, which is an application and therefore has a main() method. Only the class that you're actually executing the Java interpreter on needs a main() method. |
Now, let's create a third class. PrintSubClass2 is nearly identical to PrintSubClass, but you override the printMe() method to include the z variable. Listing 7.7 shows this class.
Listing 7.7. The PrintSubClass2 class.
1: class PrintSubClass2 extends PrintClass { 2: int z = 3; 3: 4: void printMe() { 5: System.out.println("x is " + x + ", y is " + y + 6: ", z is " + z); 7: System.out.println("I am an instance of the class " + 8: this.getClass().getName()); 9: } 10: 11: public static void main(String args[]) { 12: PrintSubClass2 obj = new PrintSubClass2(); 13: obj.printMe(); 14: } 15: }
Now when you instantiate this class and call the printMe() method, the version of printMe() you defined for this class is called instead of the one in the superclass PrintClass (as you can see in this output):
x is 0, y is 1, z 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 may just want to add behavior to the original definition rather than erase it altogether. This is particularly useful where you end up duplicating behavior in both the original method and the method that overrides it; by being able to call the original method in the body of the overridden method, you can add only what you need.
To call the original method from inside a method definition, use the super keyword to pass the method call up the hierarchy:
void myMethod (String a, String b) { // do stuff here super.myMethod(a, b); // maybe do more stuff here }
The super keyword, somewhat like the this keyword, is a placeholder for this class's superclass. You can use it anywhere you can use this, but to refer to the superclass rather than to the current class.
For example, Listing 7.8 shows the two different printMe() methods used in the previous example.
Listing 7.8. The printMe() methods.
1: // from PrintClass 2: void printMe() { 3: System.out.println("x is " + x + ", y is " + y); 4: System.out.println("I am an instance of the class" + 5: this.getClass().getName()); 6: } 7: } 8: 9: //from PrintSubClass2 10: void printMe() { 11: System.out.println("x is " + x + ", y is " + y + ", z is " + z); 12: System.out.println("I am an instance of the class " + 13: this.getClass().getName()); 14: }
Rather than duplicating most of the behavior of the superclass's method in the subclass, you can rearrange the superclass's method so that additional behavior can easily be added:
// from PrintClass void printMe() { System.out.println("I am an instance of the class" + this.getClass().getName()); System.out.println("x is " + x); System.out.println("y is " + y); }
Then, in the subclass, when you override printMe(), you can merely call the original method and then add the extra stuff:
// From PrintSubClass2 void printMe() { super.printMe(); System.out.println("z is " + z); }
Here's the output of calling printMe() on an instance of the subclass:
I am an instance of the class PrintSubClass2 X is 0 Y is 1 Z is 3
Because constructors have the same name as the current class, you cannot technically override a superclass's constructors. If you want a constructor in a subclass with the same number and type of arguments as in the superclass, you'll have to define that constructor in your own class.
However, when you create your constructors you will almost always want to call your superclass's constructors to make sure that the inherited parts of your object get initialized the way your superclass intends them to be. By explicitly calling your superclasses constructors in this way you can create constructors that effectively override or overload your superclass's constructors.
To call a regular method in a superclass, you use the form super.methodname(arguments). Because with constructors you don't have a method name to call, you have to use a different form:
super(arg1, arg2, ...);
Note that Java has a specific rule for the use of super(): It must be the very first thing in your constructor definition. If you don't call super() explicitly in your constructor, Java will do it for you-using super() with no arguments.
Similar to using this(...) in a constructor, super(...) calls a constructor method for the immediate superclass with the appropriate arguments (which may, in turn, call the constructor of its superclass, and so on). Note that a constructor with that signature has to exist in the superclass in order for the call to super() to work. The Java compiler will check this when you try to compile the source file.
Note that you don't have to call the constructor in your superclass that has exactly the same signature as the constructor in your class; you only have to call the constructor for the values you need initialized. In fact, you can create a class that has constructors with entirely different signatures from any of the superclass's constructors.
Listing 7.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 instance variable (a string for the name) and defines a constructor to initialize x, y, and the name.
Listing 7.9. The NamedPoint class.
1: import java.awt.Point; 2: class NamedPoint extends Point { 3: String name; 4: 5: NamedPoint(int x, int y, String name) { 6: super(x,y); 7: this.name = name; 8: } 9: public static void main (String arg[]) { 10: NamedPoint np = new NamedPoint(5, 5, "SmallPoint"); 11: System.out.println("x is " + np.x); 12: System.out.println("y is " + np.y); 13: System.out.println("Name is " + np.name); 14: } 15:}
x is 5 y is 5 name is SmallPoint
The constructor defined here for NamedPoint (lines 5 through 8) calls Point's constructor method to initialize Point's instance variables (x and y). Although you can just as easily initialize x and y yourself, you may 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.
Finalizer methods are almost 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 named simply finalize(). The Object class 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() throws Throwable { super.finalize(); }
Note |
The throws Throwable part of this method definition refers to the errors that might occur when this method is called. Errors in Java are called exceptions; you'll learn more about them on Day 17, "Exceptions." For now, all you need to do is include these keywords in the method definition. |
Inside the body of that finalize() method, include any cleaning up you want to do for that object. You can also call super.finalize() to allow your class's superclasses to finalize your object, if necessary (it's a good idea to do so just to make sure that everyone gets a chance to deal with the object if they need to).
You can always call the finalize() method yourself at any time; it's just a plain method like any other. However, calling finalize() does not trigger an object to be garbage-collected. Only removing all references to an object will cause it to be marked for deleting.
Finalizer methods are best used for optimizing the removal of an object-for example, by removing references to other objects, by releasing external resources that have been acquired (for example, external files), or for other behaviors that may make it easier for that object to be removed. In most cases, you will not need to use finalize() at all. See Day 21, "Under the Hood," for more about garbage collection and finalize().
Today you have learned all kinds of techniques for using, reusing, defining, and redefining methods. You have learned how to overload a method name so that the same method can have different behaviors based on the arguments with which it's called. You've learned about constructor methods, which are used to initialize a new object when it's created. You have learned about method inheritance and how to override methods that have been defined in a class's superclasses. Finally, you have learned about finalizer methods, which can be used to clean up after an object just before that object is garbage-collected and its memory reclaimed.
Congratulations on completing your first week of Teach Yourself Java in 21 Days! Starting next week, you'll apply everything you've learned this week to writing Java applets and to working with more advanced concepts in putting together Java programs and working with the standard Java class libraries.
I created two methods with the following signatures:
int 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? | |
Method overloading in Java works only if the parameter lists are different-either in number or type of arguments. Return type is not relevant for method overloading. Think about it-if you had two methods with exactly the same parameter list, how would Java know which one to call? | |
Can I overload overridden methods (that is, can I create methods that have the same name as an inherited method, but a different parameter list)? | |
Sure! As long as parameter lists vary, it doesn't matter whether you've defined a new method name or one that you've inherited from a superclass. |