Previous Page TOC Index Next Page Home


Day 7

More About Methods

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 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.

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, that make your objects and classes more efficient and easier to understand. Today, you'll learn about these additional features, including the following:

Creating Methods with the Same Name, Different Arguments

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 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, Java matches up the method name and 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 with the same name, based on the number and type of parameters to that method, not on its return type. That is, if you try to create two methods with the same name, 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 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? Java's 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.

class MyRect {
    int x1 = 0;
    int y1 = 0;
    int x2 = 0;
    int y2 = 0;
}

When a new instance of the myRect class is initially created, all its instance variables are initialized to 0. Let's define a buildRect() 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 need to import the 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. 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 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.

import java.awt.Point;
class MyRect {
    int x1 = 0;
    int y1 = 0;
    int x2 = 0;
    int y2 = 0;
    MyRect buildRect(int x1, int y1, int x2, int y2) {
        this.x1 = x1;
        this.y1 = y1;
        this.x2 = x2;
        this.y2 = y2;
        return this;
    }
    MyRect buildRect(Point topLeft, Point bottomRight) {
        x1 = topLeft.x;
        y1 = topLeft.y;
        x2 = bottomRight.x;
        y2 = bottomRight.y;
        return this;
    }
    MyRect buildRect(Point topLeft, int w, int h) {
        x1 = topLeft.x;
        y1 = topLeft.y;
        x2 = (x1 + w);
        y2 = (y1 + h);
        return this;
    }
    void printRect(){
        System.out.print("MyRect: <" + x1 + ", " + y1);
        System.out.println(", " + x2 + ", " + y2 + ">");
    }
    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("—————");
    }
}

Here's the output of this Java program:

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.

Constructor Methods

In addition to regular methods, you can also define constructor methods in your class definition.

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 by calling it directly; instead, constructor methods are called by Java automatically. Here's how it works: when you use new to create a new instance of a class, Java does three things:

If a class doesn't have any special constructor methods defined, you'll still end up with an object, but you might have to set its instance variables or call other methods that 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 call methods 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 to new.

Basic Constructors

Constructors look a lot like regular methods, with two basic differences:

For example, Listing 7.3 shows a simple class called Person, with a constructor that initializes its instance variables based on the arguments to new. The class also includes a method for the object to introduce itself, and a main() method to test each of these things.

class Person {
    String name;
    int age;
    Person(String n, int a) {
        name = n;
        age = a;
    }
    void printPerson() {
        System.out.print("Hi, my name is " + name);
        System.out.println(". I am " + age + " years old.");
    }
    public static void main (String args[]) {
        Person p;
        p = new Person("Laura", 20);
        p.printPerson();
        System.out.println("————");
        p = new Person("Tommy", 3);
        p.printPerson();
        System.out.println("————");
    }
}

Here's the output for this example program:

Hi, my name is Laura. I am 20 years old.
————
Hi, my name is Tommy. I am 3 years old.
————

Calling Another Constructor

Some constructors you write may be a superset of another constructor 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 this form:

this(arg1, arg2, arg3...);

The arguments to this are, of course, the arguments to the constructor.

Overloading Constructors

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 what they're doing is initializing an object's instance variables to the appropriate values. So, instead of the original buildRect() method you had defined (which took four parameters for the coordinates of the corners), you can create a constructor instead. Listing 7.4 shows a new class, called MyRect2, that has all the same functionality of the original MyRect, except with overloaded constructor methods instead of the buildRect() method.

import java.awt.Point;
class MyRect2 {
    int x1 = 0;
    int y1 = 0;
    int x2 = 0;
    int y2 = 0;
    MyRect2(int x1, int y1, int x2, int y2) {
        this.x1 = x1;
        this.y1 = y1;
        this.x2 = x2;
        this.y2 = y2;
    }
    MyRect2(Point topLeft, Point bottomRight) {
        x1 = topLeft.x;
        y1 = topLeft.y;
        x2 = bottomRight.x;
        y2 = bottomRight.y;
    }
    MyRect2(Point topLeft, int w, int h) {
        x1 = topLeft.x;
        y1 = topLeft.y;
        x2 = (x1 + w);
        y2 = (y1 + h);
    }
    void printRect() {
        System.out.print("MyRect: <" + x1 + ", " + y1);
        System.out.println(", " + x2 + ", " + y2 + ">");
    }
    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("—————");
    }
}

Here's the output for this example program (it's the same output from the previous example; 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>
—————

Overriding Methods

When you call a method on an object, Java looks for that method definition in the class of that object, and 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.

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.

Creating Methods that Override Existing Methods

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.

class PrintClass {
    int x = 0;
    int y = 1;
    void printMe() {
        System.out.println("X is " + x + ", Y is " + y);
        System.out.println("I am an instance of the class " +
        this.getClass().getName());
    }
}

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.

class PrintSubClass extends PrintClass {
    int z = 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 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.

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.

class PrintSubClass2 extends PrintClass {
    int z = 3;
    void printMe() {
        System.out.println("x is " + x + ", y is " + y +
               ", z is " + z);
        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 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

Calling the Original Method

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 those printMe() methods used in the previous example.

// from PrintClass
void printMe() {
        System.out.println("X is " + x + ", Y is " + y);
        System.out.println("I am an instance of the class" +
               this.getClass().getName());
    }
}
//from PrintSubClass2
    void printMe() {
        System.out.println("X is " + x + ", Y is " + y + ", Z is " + z);
        System.out.println("I am an instance of the class " +
               this.getClass().getName());
    }

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

Overriding Constructors

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 you've got. Much of the time, this is fine, because when your class's constructor is called, the constructor with the same signature for all your superclass is also called, so initialization of all the parts of a class you inherit can happen.

However, when you're defining constructors for your own class, you may want to change how your object is initialized, not only by initializing new variables your class adds, but also to change the contents of variables that are already there. You do this by explicitly calling your superclass's constructors, and then changing whatever you like.

To call a regular method in a superclass, you use super.methodname(arguments). Because with constructors you don't have a method name to call, however, you have to use a different form:

super(arg1, arg2, ...);

Similar to using this(...) in a constructor, super(...) calls the constructor method for the immediate superclass (which may, in turn, call the constructor of its superclass, and so on).

For example, 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.

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: }

The constructor defined here for NamedPoint (lines 6 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

Finalizer methods are like 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 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() {
    ...
}

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.

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 acquired, 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 for more about garbage collection and finalize().

Summary

Today, you learned all kinds of techniques for using, reusing, defining, and redefining methods. You 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 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's 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.

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.

Q&A

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 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?

Q: 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)?

A: Sure! As long as a parameter lists vary, it doesn't matter whether you've defined a new method name or one that you've inherited from a superclass.

Previous Page TOC Index Next Page Home