Home > Articles > Programming > Java

Java Methods

  • Print
  • + Share This
  • 💬 Discuss
This chapter is from the book

Item 26: Use overloading judiciously

Here is a well-intentioned attempt to classify collections according to whether they are sets, lists, or some other kind of collections:

					//Broken - incorrect use of overloading!
public class CollectionClassifier {
    public static String classify(Set s) {
        return "Set";
    }

    public static String classify(List l) {
        return "List";
    }

    public static String classify(Collection c) {
        return "Unknown Collection";
    }

    public static void main(String[] args) {
        Collection[] tests = new Collection[] {
            new HashSet(),          // A Set
            new ArrayList(),        // A List
            new HashMap().values()  // Neither Set nor List
        };

        for (int i = 0; i < tests.length; i++)
            System.out.println(classify(tests[i]));
    }
}

You might expect this program to print “Set,” followed by “List” and “Unknown Collection,” but it doesn't; it prints out “Unknown Collection” three times. Why does this happen? Because the classify method is overloaded, and the choice of which overloading to invoke is made at compile time. For all three iterations of the loop, the compile-time type of the parameter is the same: Collection. The run-time type is different in each iteration, but this does not affect the choice of overloading. Because the compile-time type of the parameter is Collection, the only applicable overloading is the third one, classify(Collection), and this overloading is invoked in each iteration of the loop.

The behavior of this program is counterintuitive because selection among overloaded methods is static, while selection among overridden method is chosen at run time, based on the run-time type of the object on which the method is invoked. As a reminder, a method is overridden when a subclass contains a method declaration with exactly the same signature as a method declaration in an ancestor. If an instance method is overridden in a subclass and this method is invoked on an instance of the subclass, the subclass's overriding method executes, regardless of the compile-time type of the subclass instance. To make this concrete, consider the following little program:

class A {
    String name() { return "A"; }
}

class B extends A {
    String name() { return "B"; }
}

class C extends A {
    String name() { return "C"; }
}

public class Overriding {
    public static void main(String[] args) {
        A[] tests = new A[] { new A(), new B(), new C() };

        for (int i = 0; i < tests.length; i++)
            System.out.print(tests[i].name());
    }
}

The name method is declared in class A and overridden in classes B and C. As you would expect, this program prints out “ABC” even though the compile-time type of the instance is A in each iteration of the loop. The compile-time type of an object has no effect on which method is executed when an overridden method is invoked; the “most specific” overriding method always gets executed. Compare this to overloading, where the run-time type of an object has no effect on which overloading is executed; the selection is made at compile time, based entirely on the compile-time types of the parameters.

In the CollectionClassifier example, the intent of the program was to discern the type of the parameter by dispatching automatically to the appropriate method overloading based on the run-time type of the parameter, just as the name method did in the “ABC” example. Method overloading simply does not provide this functionality. The way to fix the program is to replace all three overloadings of classify with a single method that does an explicit instanceof test:

public static String classify(Collection c) {
   return (c instanceof Set ? "Set" :
           (c instanceof List ? "List" : "Unknown Collection"));
}

Because overriding is the norm and overloading is the exception, overriding sets people's expectations for the behavior of method invocation. As demonstrated by the CollectionClassifier example, overloading can easily confound these expectations. It is bad practice to write code whose behavior would not be obvious to the average programmer upon inspection. This is especially true for APIs. If the typical user of an API does not know which of several method overloadings will get invoked for a given set of parameters, use of the API is likely to result in errors. These errors will likely manifest themselves as erratic behavior at run time, and many programmers will be unable to diagnose them. Therefore you should avoid confusing uses of overloading.

Exactly what constitutes a confusing use of overloading is open to some debate. A safe, conservative policy is never to export two overloadings with the same number of parameters. If you adhere to this restriction, programmers will never be in doubt as to which overloading applies to any set of parameters. This restriction is not terribly onerous because you can always give methods different names instead of overloading.

For example, consider the class ObjectOutputStream. It has a variant of its write method for every primitive type and for several reference types. Rather than overloading the write method, these variants have signatures like writeBoolean(boolean), writeInt(int), and writeLong(long). An added benefit of this naming pattern, when compared to overloading, is that it is possible to provide read methods with corresponding names, for example, readBoolean(), readInt(), and readLong(). The ObjectInputStream class does, in fact, provide read methods with these names.

For ), but that isn't always practical. On the bright side, with constructors you don't have to worry about interactions between overloading and overriding, as constructors can't be overridden. Because you'll probably have occasion to export multiple constructors with the same number of parameters, it pays to know when it is safe to do so.

Exporting multiple overloadings with the same number of parameters is unlikely to confuse programmers if it is always clear which overloading will apply to any given set of actual parameters. This is the case when at least one corresponding formal parameter in each pair of overloadings has a “radically different” type in the two overloadings. Two types are radically different if it is clearly impossible to cast an instance of either type to the other. Under these circumstances, which overloading applies to a given set of actual parameters is fully determined by the run-time types of the parameters and cannot be affected by their compile-time types, so the major source of confusion evaporates.

For example, ArrayList has one constructor that takes an int and a second constructor that takes a Collection. It is hard to imagine any confusion over which of these two constructors will be invoked under any circumstances because primitive types and reference types are radically different. Similarly, BigInteger has one constructor that takes a byte array and another that takes a String; this causes no confusion. Array types and classes other than Object are radically different. Also, array types and interfaces other than Serializable and Cloneable are radically different. Finally, Throwable, as of release 1.4, has one constructor that takes a String and another takes a Throwable. The classes String and Throwable are unrelated, which is to say that neither class is a descendant of the other. It is impossible for any object to be an instance of two unrelated classes, so unrelated classes are radically different.

There are a few additional examples of pairs of types that can't be converted in either direction [JLS, 5.1.7], but once you go beyond these simple cases, it can become very difficult for the average programmer to discern which, if any, overloading applies to a set of actual parameters. The specification that determines which overloading is selected is complex, and few programmers understand all of its subtleties [JLS, 15.12.1-3].

Occasionally you may be forced to violate the above guidelines when retrofitting existing classes to implement new interfaces. For example, many of the value types in the Java platform libraries had “self-typed” compareTo methods prior to the introduction of the Comparable interface. Here is the declaration for String's original self-typed compareTo method:

public int compareTo(String s);

With the introduction of the Comparable interface, all of the these classes were retrofitted to implement this interface, which involved adding a more general compareTo method with this declaration:

public int compareTo(Object o);

While the resulting overloading is clearly a violation of the above guidelines, it causes no harm as long as both overloaded methods always do exactly the same thing when they are invoked on the same parameters. The programmer may not know which overloading will be invoked, but it is of no consequence as long as both methods return the same result. The standard way to ensure this behavior is to have the more general overloading forward to the more specific:

public int compareTo(Object o) {
    return compareTo((String) o);
}

A similar idiom is sometimes used for equals methods:

public boolean equals(Object o) {
    return o instanceof String && equals((String) o);
}

This idiom is harmless and may result in slightly improved performance if the compile-time type of the parameter matches the parameter of the more specific overloading. That said, it probably isn't worth doing as a matter of course (Item 37).

While the Java platform libraries largely adhere to the advice in this item, there are a number of places where it is violated. For example, the String class exports two overloaded static factory methods, valueOf(char[]) and valueOf(Object), that do completely different things when passed the same object reference. There is no real justification for this, and it should be regarded as an anomaly with the potential for real confusion.

To summarize, just because you can overload methods doesn't mean you should. You should generally refrain from overloading methods with multiple signatures that have the same number of parameters. In some cases, especially where constructors are involved, it may be impossible to follow this advice. In that case, you should at least avoid situations where the same set of parameters can be passed to different overloadings by the addition of casts. If such a situation cannot be avoided, for example because you are retrofitting an existing class to imple ment a new interface, you should ensure that all overloadings behave identically when passed the same parameters. If you fail to do this, programmers will not be able to make effective use of the overloaded method or constructor, and they won't understand why it doesn't work.

  • + Share This
  • 🔖 Save To Your Account

Discussions

comments powered by Disqus