8.4.8 Examples of Method Declarations

The following examples illustrate some (possibly subtle) points about method declarations.

8.4.8.1 Example: Overriding

In the example:


class Point {

int x = 0, y = 0;

void move(int dx, int dy) { x += dx; y += dy; }
}
class SlowPoint extends Point {
int xLimit, yLimit;
void move(int dx, int dy) { super.move(limit(dx, xLimit), limit(dy, yLimit)); }
static int limit(int d, int limit) { return d > limit ? limit : d < -limit ? -limit : d; }
}

the class SlowPoint overrides the declarations of method move of class Point with its own move method, which limits the distance that the point can move on each invocation of the method. When the move method is invoked for an instance of class SlowPoint, the overriding definition in class SlowPoint will always be called, even if the reference to the SlowPoint object is taken from a variable whose type is Point.

8.4.8.2 Example: Overloading, Overriding, and Hiding

In the example:


class Point {

int x = 0, y = 0;

void move(int dx, int dy) { x += dx; y += dy; }

int color;
}
class RealPoint extends Point {
float x = 0.0f, y = 0.0f;

void move(int dx, int dy) { move((float)dx, (float)dy); }

void move(float dx, float dy) { x += dx; y += dy; }
}

the class RealPoint hides the declarations of the int instance variables x and y of class Point with its own float instance variables x and y, and overrides the method move of class Point with its own move method. It also overloads the name move with another method with a different signature (§8.4.2).

In this example, the members of the class RealPoint include the instance variable color inherited from the class Point, the float instance variables x and y declared in RealPoint, and the two move methods declared in RealPoint.

Which of these overloaded move methods of class RealPoint will be chosen for any particular method invocation will be determined at compile time by the overloading resolution procedure described in §15.11.

8.4.8.3 Example: Incorrect Overriding

This example is an extended variation of that in the preceding section:


class Point {

int x = 0, y = 0, color;

void move(int dx, int dy) { x += dx; y += dy; }

int getX() { return x; }

int getY() { return y; }
}
class RealPoint extends Point {
float x = 0.0f, y = 0.0f;

void move(int dx, int dy) { move((float)dx, (float)dy); }

void move(float dx, float dy) { x += dx; y += dy; }

float getX() { return x; }

float getY() { return y; }
}

Here the class Point provides methods getX and getY that return the values of its fields x and y; the class RealPoint then overrides these methods by declaring methods with the same signature. The result is two errors at compile time, one for each method, because the return types do not match; the methods in class Point return values of type int, but the wanna-be overriding methods in class RealPoint return values of type float.

8.4.8.4 Example: Overriding versus Hiding

This example corrects the errors of the example in the preceding section:


class Point {

int x = 0, y = 0;

void move(int dx, int dy) { x += dx; y += dy; }

int getX() { return x; }

int getY() { return y; }

int color;
}
class RealPoint extends Point {
float x = 0.0f, y = 0.0f;

void move(int dx, int dy) { move((float)dx, (float)dy); }

void move(float dx, float dy) { x += dx; y += dy; }

int getX() { return (int)Math.floor(x); }

int getY() { return (int)Math.floor(y); }
}

Here the overriding methods getX and getY in class RealPoint have the same return types as the methods of class Point that they override, so this code can be successfully compiled.

Consider, then, this test program:


class Test {

	public static void main(String[] args) {
		RealPoint rp = new RealPoint();
		Point p = rp;
		rp.move(1.71828f, 4.14159f);
		p.move(1, -1);
		show(p.x, p.y);
		show(rp.x, rp.y);
		show(p.getX(), p.getY());
		show(rp.getX(), rp.getY());
	}

static void show(int x, int y) { System.out.println("(" + x + ", " + y + ")"); }
static void show(float x, float y) { System.out.println("(" + x + ", " + y + ")"); }
}

The output from this program is:


(0, 0)
(2.7182798, 3.14159)
(2, 3)
(2, 3)

The first line of output illustrates the fact that an instance of RealPoint actually contains the two integer fields declared in class Point; it is just that their names are hidden from code that occurs within the declaration of class RealPoint (and those of any subclasses it might have). When a reference to an instance of class RealPoint in a variable of type Point is used to access the field x, the integer field x declared in class Point is accessed. The fact that its value is zero indicates that the method invocation p.move(1, -1) did not invoke the method move of class Point; instead, it invoked the overriding method move of class RealPoint.

The second line of output shows that the field access rp.x refers to the field x declared in class RealPoint. This field is of type float, and this second line of output accordingly displays floating-point values. Incidentally, this also illustrates the fact that the method name show is overloaded; the types of the arguments in the method invocation dictate which of the two definitions will be invoked.

The last two lines of output show that the method invocations p.getX() and rp.getX() each invoke the getX method declared in class RealPoint. Indeed, there is no way to invoke the getX method of class Point for an instance of class RealPoint from outside the body of RealPoint, no matter what the type of the variable we may use to hold the reference to the object. Thus, we see that fields and methods behave differently: hiding is different from overriding.

8.4.8.5 Example: Invocation of Hidden Class Methods

A hidden class (static) method can be invoked by using a reference whose type is the class that actually contains the declaration of the method. In this respect, hiding of static methods is different from overriding of instance methods. The example:


class Super {
	static String greeting() { return "Goodnight"; }
	String name() { return "Richard"; }
}

class Sub extends Super { static String greeting() { return "Hello"; } String name() { return "Dick"; } }
class Test { public static void main(String[] args) { Super s = new Sub(); System.out.println(s.greeting() + ", " + s.name()); } }

produces the output:

Goodnight, Dick

because the invocation of greeting uses the type of s, namely Super, to figure out, at compile time, which class method to invoke, whereas the invocation of name uses the class of s, namely Sub, to figure out, at run time, which instance method to invoke.

8.4.8.6 Large Example of Overriding

Overriding makes it easy for subclasses to extend the behavior of an existing class, as shown in this example:


import java.io.OutputStream;

import java.io.IOException;


class BufferOutput {

private OutputStream o;

BufferOutput(OutputStream o) { this.o = o; }

protected byte[] buf = new byte[512];

protected int pos = 0;
public void putchar(char c) throws IOException { if (pos == buf.length) flush(); buf[pos++] = (byte)c; }


	public void putstr(String s) throws IOException {
		for (int i = 0; i < s.length(); i++)
			putchar(s.charAt(i));
	}

public void flush() throws IOException { o.write(buf, 0, pos); pos = 0; }
}
class LineBufferOutput extends BufferOutput {
LineBufferOutput(OutputStream o) { super(o); }
public void putchar(char c) throws IOException { super.putchar(c); if (c == '\n') flush(); }
}
class Test { public static void main(String[] args)
throws IOException
{ LineBufferOutput lbo =
new LineBufferOutput(System.out); lbo.putstr("lbo\nlbo"); System.out.print("print\n"); lbo.putstr("\n"); } }

This example produces the output:


lbo
print
lbo

The class BufferOutput implements a very simple buffered version of an OutputStream, flushing the output when the buffer is full or flush is invoked. The subclass LineBufferOutput declares only a constructor and a single method putchar, which overrides the method putchar of BufferOutput. It inherits the methods putstr and flush from class Buffer.

In the putchar method of a LineBufferOutput object, if the character argument is a newline, then it invokes the flush method. The critical point about overriding in this example is that the method putstr, which is declared in class BufferOutput, invokes the putchar method defined by the current object this, which is not necessarily the putchar method declared in class BufferOutput.

Thus, when putstr is invoked in main using the LineBufferOutput object lbo, the invocation of putchar in the body of the putstr method is an invocation of the putchar of the object lbo, the overriding declaration of putchar that checks for a newline. This allows a subclass of BufferOutput to change the behavior of the putstr method without redefining it.

Documentation for a class such as BufferOutput, which is designed to be extended, should clearly indicate what is the contract between the class and its subclasses, and should clearly indicate that subclasses may override the putchar method in this way. The implementor of the BufferOutput class would not, therefore, want to change the implementation of putstr in a future implementation of BufferOutput not to use the method putchar, because this would break the preexisting contract with subclasses. See the further discussion of binary compatibility in §13, especially §13.2.

8.4.8.7 Example: Incorrect Overriding because of Throws

This example uses the usual and conventional form for declaring a new exception type, in its declaration of the class BadPointException:


class BadPointException extends Exception {
	BadPointException() { super(); }
	BadPointException(String s) { super(s); }
}

class Point {
	int x, y;
	void move(int dx, int dy) { x += dx; y += dy; }
}

class CheckedPoint extends Point { void move(int dx, int dy) throws BadPointException { if ((x + dx) < 0 || (y + dy) < 0) throw new BadPointException(); x += dx; y += dy; } }

This example results in a compile-time error, because the override of method move in class CheckedPoint declares that it will throw a checked exception that the move in class Point has not declared. If this were not considered an error, an invoker of the method move on a reference of type Point could find the contract between it and Point broken if this exception were thrown.

Removing the throws clause does not help:


class CheckedPoint extends Point {
	void move(int dx, int dy) {
		if ((x + dx) < 0 || (y + dy) < 0)
			throw new BadPointException();
		x += dx; y += dy;
	}
}

A different compile-time error now occurs, because the body of the method move cannot throw a checked exception, namely BadPointException, that does not appear in the throws clause for move.