Method overloading

Bruce Eckel's Thinking in Java Contents | Prev | Next

In Java, another factor forces the overloading of method names: the constructor. Because the constructor’s name is predetermined by the name of the class, there can be only one constructor name. But what if you want to create an object in more than one way? For example, suppose you build a class that can initialize itself in a standard way and by reading information from a file. You need two constructors, one that takes no arguments (the default constructor), and one that takes a String as an argument, which is the name of the file from which to initialize the object. Both are constructors, so they must have the same name – the name of the class. Thus method overloading is essential to allow the same method name to be used with different argument types. And although method overloading is a must for constructors, it’s a general convenience and can be used with any method.

//: Overloading.java
// Demonstration of both constructor
// and ordinary method overloading.
import java.util.*;
 
class Tree {
  int height;
  Tree() {
    prt("Planting a seedling");
    height = 0;
  }
  Tree(int i) {
    prt("Creating new Tree that is "
        + i + " feet tall");
    height = i;
  }
  void info() {
    prt("Tree is " + height
        + " feet tall");
  }
  void info(String s) {
    prt(s + ": Tree is "
        + height + " feet tall");
  }
  static void prt(String s) {
    System.out.println(s);
  }
}
 
public class Overloading {
  public static void main(String[] args) {
    for(int i = 0; i < 5; i++) {
      Tree t = new Tree(i);
      t.info();
      t.info("overloaded method");
    }
    // Overloaded constructor:
    new Tree();
  }
} ///:~ 

A Tree object can be created either as a seedling, with no argument, or as a plant grown in a nursery, with an existing height. To support this, there are two constructors, one that takes no arguments (we call constructors that take no arguments default constructors [17]) and one that takes the existing height.

Distinguishing overloaded methods

//: OverloadingOrder.java
// Overloading based on the order of
// the arguments.
 
public class OverloadingOrder {
  static void print(String s, int i) {
    System.out.println(
      "String: " + s +
      ", int: " + i);
  }
  static void print(int i, String s) {
    System.out.println(
      "int: " + i +
      ", String: " + s);
  }
  public static void main(String[] args) {
    print("String first", 11);
    print(99, "Int first");
  }
} ///:~ 

The two print( ) methods have identical arguments, but the order is different, and that’s what makes them distinct.

Overloading with primitives

Primitives can be automatically promoted from a smaller type to a larger one and this can be slightly confusing in combination with overloading. The following example demonstrates what happens when a primitive is handed to an overloaded method:

//: PrimitiveOverloading.java
// Promotion of primitives and overloading
 
public class PrimitiveOverloading {
  // boolean can't be automatically converted
  static void prt(String s) { 
    System.out.println(s); 
  }
 
  void f1(char x) { prt("f1(char)"); }
  void f1(byte x) { prt("f1(byte)"); }
  void f1(short x) { prt("f1(short)"); }
  void f1(int x) { prt("f1(int)"); }
  void f1(long x) { prt("f1(long)"); }
  void f1(float x) { prt("f1(float)"); }
  void f1(double x) { prt("f1(double)"); }
 
  void f2(byte x) { prt("f2(byte)"); }
  void f2(short x) { prt("f2(short)"); }
  void f2(int x) { prt("f2(int)"); }
  void f2(long x) { prt("f2(long)"); }
  void f2(float x) { prt("f2(float)"); }
  void f2(double x) { prt("f2(double)"); }
 
  void f3(short x) { prt("f3(short)"); }
  void f3(int x) { prt("f3(int)"); }
  void f3(long x) { prt("f3(long)"); }
  void f3(float x) { prt("f3(float)"); }
  void f3(double x) { prt("f3(double)"); }
 
  void f4(int x) { prt("f4(int)"); }
  void f4(long x) { prt("f4(long)"); }
  void f4(float x) { prt("f4(float)"); }
  void f4(double x) { prt("f4(double)"); }
 
  void f5(long x) { prt("f5(long)"); }
  void f5(float x) { prt("f5(float)"); }
  void f5(double x) { prt("f5(double)"); }
 
  void f6(float x) { prt("f6(float)"); }
  void f6(double x) { prt("f6(double)"); }
 
  void f7(double x) { prt("f7(double)"); }
 
  void testConstVal() {
    prt("Testing with 5");
    f1(5);f2(5);f3(5);f4(5);f5(5);f6(5);f7(5);
  }
  void testChar() {
    char x = 'x';
    prt("char argument:");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
  }
  void testByte() {
    byte x = 0;
    prt("byte argument:");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
  }
  void testShort() {
    short x = 0;
    prt("short argument:");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
  }
  void testInt() {
    int x = 0;
    prt("int argument:");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
  }
  void testLong() {
    long x = 0;
    prt("long argument:");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
  }
  void testFloat() {
    float x = 0;
    prt("float argument:");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
  }
  void testDouble() {
    double x = 0;
    prt("double argument:");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
  }
  public static void main(String[] args) {
    PrimitiveOverloading p = 
      new PrimitiveOverloading();
    p.testConstVal();
    p.testChar();
    p.testByte();
    p.testShort();
    p.testInt();
    p.testLong();
    p.testFloat();
    p.testDouble();
  }
} ///:~ 

If you view the output of this program, you’ll see that the constant value 5 is treated as an int, so if an overloaded method is available that takes an int it is used. In all other cases, if you have a data type that is smaller than the argument in the method, that data type is promoted. char produces a slightly different effect, since if it doesn’t find an exact char match, it is promoted to int.

What happens if your argument is bigger than the argument expected by the overloaded method? A modification of the above program gives the answer:

//: Demotion.java
// Demotion of primitives and overloading
 
public class Demotion {
  static void prt(String s) { 
    System.out.println(s); 
  }
 
  void f1(char x) { prt("f1(char)"); }
  void f1(byte x) { prt("f1(byte)"); }
  void f1(short x) { prt("f1(short)"); }
  void f1(int x) { prt("f1(int)"); }
  void f1(long x) { prt("f1(long)"); }
  void f1(float x) { prt("f1(float)"); }
  void f1(double x) { prt("f1(double)"); }
 
  void f2(char x) { prt("f2(char)"); }
  void f2(byte x) { prt("f2(byte)"); }
  void f2(short x) { prt("f2(short)"); }
  void f2(int x) { prt("f2(int)"); }
  void f2(long x) { prt("f2(long)"); }
  void f2(float x) { prt("f2(float)"); }
 
  void f3(char x) { prt("f3(char)"); }
  void f3(byte x) { prt("f3(byte)"); }
  void f3(short x) { prt("f3(short)"); }
  void f3(int x) { prt("f3(int)"); }
  void f3(long x) { prt("f3(long)"); }
 
  void f4(char x) { prt("f4(char)"); }
  void f4(byte x) { prt("f4(byte)"); }
  void f4(short x) { prt("f4(short)"); }
  void f4(int x) { prt("f4(int)"); }
 
  void f5(char x) { prt("f5(char)"); }
  void f5(byte x) { prt("f5(byte)"); }
  void f5(short x) { prt("f5(short)"); }
 
  void f6(char x) { prt("f6(char)"); }
  void f6(byte x) { prt("f6(byte)"); }
 
  void f7(char x) { prt("f7(char)"); }
 
  void testDouble() {
    double x = 0;
    prt("double argument:");
    f1(x);f2((float)x);f3((long)x);f4((int)x);
    f5((short)x);f6((byte)x);f7((char)x);
  }
  public static void main(String[] args) {
    Demotion p = new Demotion();
    p.testDouble();
  }
} ///:~ 

Here, the methods take narrower primitive values. If your argument is wider then you must cast to the necessary type using the type name in parentheses. If you don’t do this, the compiler will issue an error message.

Overloading on return values

It is common to wonder “Why only class names and method argument lists? Why not distinguish between methods based on their return values?” For example, these two methods, which have the same name and arguments, are easily distinguished from each other:

void f() {}
int f() {}

Default constructors

//: DefaultConstructor.java
 
class Bird {
  int i;
}
 
public class DefaultConstructor {
  public static void main(String[] args) {
    Bird nc = new Bird(); // default!
  }
} ///:~ 

The line

new Bird();

creates a new object and calls the default constructor, even though one was not explicitly defined. Without it we would have no method to call to build our object. However, if you define any constructors (with or without arguments), the compiler will not synthesize one for you:

class Bush {
  Bush(int i) {}
  Bush(double d) {}
}

Now if you say:

new Bush();

the compiler will complain that it cannot find a constructor that matches. It’s as if when you don’t put in any constructors, the compiler says “You are bound to need some constructor, so let me make one for you.” But if you write a constructor, the compiler says “You’ve written a constructor so you know what you’re doing; if you didn’t put in a default it’s because you meant to leave it out.”

The this keyword

If you have two objects of the same type called a and b, you might wonder how it is that you can call a method f( ) for both those objects:

class Banana { void f(int i) { /* ... */ } }
Banana a = new Banana(), b = new Banana();
a.f(1);
b.f(2);

If there’s only one method called f( ), how can that method know whether it’s being called for the object a or b?

To allow you to write the code in a convenient object-oriented syntax in which you “send a message to an object,” the compiler does some undercover work for you. There’s a secret first argument passed to the method f( ), and that argument is the handle to the object that’s being manipulated. So the two method calls above become something like:

Banana.f(a,1);
Banana.f(b,2);

This is internal and you can’t write these expressions and get the compiler to accept them, but it gives you an idea of what’s happening.

Suppose you’re inside a method and you’d like to get the handle to the current object. Since that handle is passed secretly by the compiler, there’s no identifier for it. However, for this purpose there’s a keyword: this. The this keyword – which can be used only inside a method – produces the handle to the object the method has been called for. You can treat this handle just like any other object handle. Keep in mind that if you’re calling a method of your class from within another method of your class, you don’t need to use this; you simply call the method. The current this handle is automatically used for the other method. Thus you can say:

class Apricot {
  void pick() { /* ... */ }
  void pit() { pick(); /* ... */ }
}

Inside pit( ), you could say this.pick( ) but there’s no need to. The compiler does it for you automatically. The this keyword is used only for those special cases in which you need to explicitly use the handle to the current object. For example, it’s often used in return statements when you want to return the handle to the current object:

//: Leaf.java
// Simple use of the "this" keyword
 
public class Leaf {
  private int i = 0;
  Leaf increment() {
    i++;
    return this;
  }
  void print() {
    System.out.println("i = " + i);
  }
  public static void main(String[] args) {
    Leaf x = new Leaf();
    x.increment().increment().increment().print();
  }
} ///:~ 

Because increment( ) returns the handle to the current object via the this keyword, multiple operations can easily be performed on the same object.

Calling constructors from constructors

//: Flower.java
// Calling constructors with "this"
 
public class Flower {
  private int petalCount = 0;
  private String s = new String("null");
  Flower(int petals) {
    petalCount = petals;
    System.out.println(
      "Constructor w/ int arg only, petalCount= "
      + petalCount);
  }
  Flower(String ss) {
    System.out.println(
      "Constructor w/ String arg only, s=" + ss);
    s = ss;
  }
  Flower(String s, int petals) {
    this(petals);
//!    this(s); // Can't call two!
    this.s = s; // Another use of "this"
    System.out.println("String & int args");
  }
  Flower() {
    this("hi", 47);
    System.out.println(
      "default constructor (no args)");
  }
  void print() {
//!    this(11); // Not inside non-constructor!
    System.out.println(
      "petalCount = " + petalCount + " s = "+ s);
  }
  public static void main(String[] args) {
    Flower x = new Flower();
    x.print();
  }
} ///:~ 

The constructor Flower(String s, int petals) shows that, while you can call one constructor using this, you cannot call two. In addition, the constructor call must be the first thing you do or you’ll get a compiler error message.

This example also shows another way you’ll see this used. Since the name of the argument s and the name of the member data s are the same, there’s an ambiguity. You can resolve it by saying this.s to refer to the member data. You’ll often see this form used in Java code, and it’s used in numerous places in this book.

In print( ) you can see that the compiler won’t let you call a constructor from inside any method other than a constructor.

The meaning of static

With the this keyword in mind, you can more fully understand what it means to make a method static. It means that there is no this for that particular method. You cannot call non- static methods from inside static methods [18] (although the reverse is possible), and you can call a static method for the class itself, without any object. In fact, that’s primarily what a static method is for. It’s as if you’re creating the equivalent of a global function (from C). Except global functions are not permitted in Java, and putting the static method inside a class allows it access to other static methods and to static fields.

Some people argue that static methods are not object-oriented since they do have the semantics of a global function; with a static method you don’t send a message to an object, since there’s no this. This is probably a fair argument, and if you find yourself using a lot of static methods you should probably rethink your strategy. However, statics are pragmatic and there are times when you genuinely need them, so whether or not they are “proper OOP” should be left to the theoreticians. Indeed, even Smalltalk has the equivalent in its “class methods.”


[17] In some of the Java literature from Sun they instead refer to these with the clumsy but descriptive name “no-arg constructors.” The term “default constructor” has been in use for many years and so I will use that.

[18] The one case in which this is possible occurs if you pass a handle to an object into the static method. Then, via the handle (which is now effectively this), you can call non- static methods and access non- static fields. But typically if you want to do something like this you’ll just make an ordinary, non- static method.



Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • With JRebel, developers get to see their code changes immediately, fine-tune their code with incremental changes, debug, explore and deploy their code with ease (both locally and remotely), and ultimately spend more time coding instead of waiting for the dreaded application redeploy to finish. Every time a developer tests a code change it takes minutes to build and deploy the application. JRebel keeps the app server running at all times, so testing is instantaneous and interactive.

  • On-demand Event Event Date: April 22, 2014 Database professionals -- whether developers or DBAs -- can often save valuable time by learning to get the most from their new or existing productivity tools. Whether you're responsible for managing database projects, performing database health checks and reporting, analyzing code, or measuring software engineering metrics, it's likely you're not taking advantage of some of the lesser-known features of Toad from Dell. Attend this eSeminar with Dell Software's …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds