Covariant Specialization in Java

Suchitra Gupta
Jeff Hartkopf
Suresh Ramaswamy

This article was originally published in JOOP (The Journal of Object-Oriented Programming), May 2000, Volume 13, Number 2, by SIGS Publications.

Covariant specialization is a form of multiple polymorphism supported by some object-oriented programming languages for overriding a method by varying its parameter types. Particularly useful in frameworks that support dynamic behavior, covariant specialization is a convenient mechanism in a variety of situations. Although Java does not directly support it, it is possible to achieve a similar effect without language extensions by employing Java's rich reflection capabilities, while preserving run-time type safety and efficiency. This article shows how this can be done in Java, first providing a practical definition of covariant specialization, then describing the technique via sample code. It concludes with applications of covariant specialization and issues encountered in implementing and applying the technique.


Most object-oriented languages support specialization through inheritance. In this form of specialization, the subclass typically provides an alternate implementation for an inherited method supporting an identical signature. In this article we discuss an extension of this form of specialization where the inherited method can be further specialized based on the parameter types in its signature.

In typed languages, objects can have multiple types determined by the interfaces they support. It is useful to distinguish two specific kinds of types. The inherent type of an object is the type from which it was instantiated; in Java this is the object's class. The declared type of an object is the type of the expression through which it is referenced; in Java this is the type of the object reference.

Polymorphism is the ability to substitute an object of one type where an object of a different type is expected. Most typed object-oriented languages including Java and C++ support polymorphism via subtyping, where an object of one type can be substituted wherever its supertype is expected. This is because a subtype is guaranteed to support all the operations of its supertype.

Java and C++ allow a class to override a method from its superclass, to support polymorphism, where the method implementation selected depends on the inherent type of the receiver, the object on which the method is invoked. Java and C++ support a form of inheritance or specialization known as invariant specialization, where the signature of the overriding method must match exactly the signature of the overridden method. Since we cannot vary the signature, both languages support only single polymorphism where the method implementation executed depends on the inherent type of the receiver, and not on the inherent types of the arguments.

Some object-oriented languages such as Eiffel and CLOS support multiple polymorphism, where the method implementation executed depends both on the inherent type of the receiver and the inherent types of the arguments [6]. In order to support inheritance-based multiple polymorphism, the language must allow variant specialization, where the signature of the overriding method may be different than that of the overridden method. Covariant specialization is a special form of variant specialization where the types of the parameters in the overriding method are subtypes of the corresponding parameter types in the overridden method. In another form of variant specialization known as contravariant specialization, parameter types in the overriding method are supertypes of the corresponding parameter types in the overridden method [2]. The latter form is less useful in practice, so this article focuses on covariant specialization.

Consider the example depicted in Figure 1. A store accepts money in payment for goods, but the policy of each store defines the types of money it accepts. In the example, McDonalds accepts only cash, while Sears accepts cash and various credit cards. To support this behavior, the McDonalds and Sears classes covariantly specialize the Store class providing specialized accept methods, each of which applies to a particular type of money. For the purposes of this example, we will assume that the constructors for Money and its subclasses take a monetary amount as a parameter.

Figure 1: Covariance with Stores and Money

If the example shown in Figure 1 is mapped directly to Java, the resulting code is syntactically legal but the semantics are not what we want. The accept methods in McDonalds and Sears do not override the accept method in Store, they simply overload it. Consider the following code fragment:

Store store = new Store();
McDonalds mac = new McDonalds();
Store macAsStore = new McDonalds();
Money money = new Money(2.00);
Cash cash = new Cash(2.00);
Money cashAsMoney = new Cash(2.00);

Table 1 shows the results of invoking the accept method on each store object, passing each money object as an argument. The Method Invoked column shows the line of code invoking accept. The Invariance column shows the actual method implementation executed in Java. The Covariance column shows the method implementation that would be executed if Java supported covariant specialization.

Method Invoked Method Implementation Executed
Invariance Covariance
store.accept(money) Store.accept(Money) Store.accept(Money)
store.accept(cash) Store.accept(Money) Store.accept(Money)
store.accept(cashAsMoney) Store.accept(Money) Store.accept(Money)
mac.accept(money) Store.accept(Money) Store.accept(Money)
mac.accept(cash) McDonalds.accept(Cash) McDonalds.accept(Cash)
mac.accept(cashAsMoney) Store.accept(Money) McDonalds.accept(Cash)
macAsStore.accept(money) Store.accept(Money) Store.accept(Money)
macAsStore.accept(cash) Store.accept(Money) McDonalds.accept(Cash)
macAsStore.accept(cashAsMoney) Store.accept(Money) McDonalds.accept(Cash)
Table 1: Contrasting Invariance and Covariance

In the first three rows of Table 1, accept is invoked on a Store object. For example, in the first row, store.accept(money) means that accept is invoked on a Store object with a Money object as an argument. In both invariance and covariance, the accept implementation provided by Store is executed. In the second row, the argument becomes a Cash object. Again, the accept(Money) implementation is executed in both cases, because Cash is a type of Money and Store does not provide an explicit accept(Cash) implementation. The third row is similar.

In the next set of three rows, accept is invoked on a McDonalds object. In the fourth row, accept(Money) is again executed in both invariance and covariance, since the argument is of type Money. In the fifth row, the argument is a Cash object, and the explicit accept(Cash) implementation is executed. In the sixth row we see the first difference between invariance and covariance. Here, the argument is a Cash object whose declared type is Money. The Java compiler treats the argument as a Money object, hence executing accept(Money) in the invariance case. With covariance, since the inherent type of the argument is Cash, the accept(Cash) implementation is executed. It is important to note that without special support, this is not possible in Java.

In the last three rows, we now add the complexity of the receiver having a different inherent and declared type. With invariant specialization, the method signature that can be dispatched is restricted to the set of methods supported by the declared type of the receiver. The receiver is a McDonalds object but assigned to a Store reference, so Java treats it as a Store for method selection, hence resulting in execution of the accept(Money) implementation regardless of the inherent type of the argument. The results in the last row are particularly important, since this is the situation a programmer frequently faces when using a framework, as discussed in the Applicability section. In the invariance case, there are no overridden methods, so the accept method from the Store class is executed, ignoring the inherent type of the argument. In the covariance case, the method executed depends on the inherent type of both the receiver and the argument: macAsStore is treated as an instance of McDonalds and not just Store, and cashAsMoney is treated as an instance of Cash and not just Money.

In the Invariance column, one may conceptually arrive at the results as follows. Determine the set of method signatures supported by the declared type of the receiver. From this set, select one signature based on the declared type of the argument. Finally, invoke the appropriate implementation of this signature based on the inherent type of the receiver. This is readily apparent if McDonalds overrides the accept(Money) method and macAsStore.accept(cashAsMoney) is invoked: instead of executing Store.accept(Money), Java executes McDonalds.accept(Money), but it still does not execute McDonalds.accept(Cash).

In the Covariance column, one may conceptually arrive at the results as follows. Determine the set of method signatures supported by the declared type of the receiver. Add to this set all covariantly specialized method signatures supported by the inherent type of the receiver. From this set, select one signature based on the inherent type of the argument. Finally, invoke the appropriate implementation of this signature based on the inherent type of the receiver.

With these concepts in mind, we now turn to the technique for implementing covariant specialization in Java, followed by some examples of its usefulness. For more detailed treatments of the concepts presented here, see [1] and [2].


Other approaches for supporting covariant specialization in Java require extensions to the language [1]. Our approach relies on Java's reflection capabilities to provide covariant behavior at run time without language extensions, thus maintaining compatibility with existing compilers and virtual machines. The technique also does not break the existing object model of Java, it merely extends the behavior by taking into account information available at run time. Furthermore, use of the technique does not affect the behavior of code that does not take advantage of it.

We first present the basic technique using our store example, which is simplified by the fact that the participating methods take only a single parameter. Once the reader has a grasp of the essential idea, we explain how to support multiple parameters.

Technique with Single Parameter

The technique uses the inherent types of the arguments to select the appropriate covariantly specialized method at run time, while normally the Java compiler selects the method based on the declared types of the arguments. In either case, method selection also relies on the inherent type of the receiver and ignores the return type. This technique uses reflection on the receiver and the argument passed to determine the implementation to invoke.

The code that implements covariance resides in a common ancestor of the classes that participate in covariant specialization of a method. For every method that is covariantly specialized, the ancestor class provides an implementation for the most general form of the method, which is responsible for selecting and invoking the appropriate implementation provided by a derived class. The following code fragment shows how the accept method in the Store class would be implemented to dispatch the appropriate method implementation provided by a derived class.

public abstract class Store {
  public void accept(Money money) {
    try {
      // look up method
      Class inherentArgTypes[] = { money.getClass() };
      Class inherentReceiverType = this.getClass();
      Method method = inherentReceiverType.getDeclaredMethod("accept",

      // invoke method
      Object args[] = { money };
      method.invoke(this, args);
    } catch (Exception e) {
      // handle exceptions as appropriate

The first three statements in the try block look for an accept method on the receiver that takes a parameter with the appropriate subtype of Money. The second call to getClass returns the class object for the inherent type of the receiver. The call to getDeclaredMethod takes the name of the method and an array of the types of the arguments, inherentArgTypes. This array contains just one element, which is the class object for the inherent type of the argument to the accept method. The getDeclaredMethod method is invoked on inherentReceiverType to retrieve the method handle for the method to be executed. If no matching method is found, an exception is thrown. Finally, the remaining statements build an array of the actual arguments and pass them to the invoke method. Handling and propagating exceptions is discussed in more detail in the Considerations section.

If the receiver has an accept method whose parameter type matches the inherent type of the argument passed, then the above code fragment finds and invokes it. For example, consider the McDonalds class which extends Store:

public class McDonalds extends Store {
  public void accept(Cash cash) {
    // ...

Suppose there is a Customer class whose instance pays a particular store using a particular type of money. Suppose further that the Customer class is part of a framework, and therefore does not know about specific kinds of stores or money:

public class Customer {
  public void pay(Store s, Money m) {
    // some processing

One could create an instance of Customer and invoke the pay method, passing arguments of type McDonalds and Cash:

Customer cust = new Customer(); McDonalds(), new Cash(4.00));

The customer invokes the accept method via the abstract references to its arguments. Without covariant specialization, the accept(Cash) method provided by McDonalds would not be called. Instead, the McDonalds class must override the accept(Money) method and in its implementation, check whether the inherent type of the argument is Cash, and if so, downcast it. It must throw an exception if the inherent type is something other than Cash. The situation worsens for Sears, because the overriding method must use conditional statements to check for each possible type of money it can accept. These problems disappear when covariance is used.

As mentioned earlier, the algorithm as described so far finds only methods whose parameter types exactly match the inherent types of the arguments. Suppose we have a subtype of Cash named Coin, and call pay as follows: McDonalds(), new Coin(0.50));

An exception will be thrown because no exact match is found, even though there is a good candidate, namely accept(Cash). We can avoid this by replacing the line containing the call to getDeclaredMethod with the following code to search for a method that takes the most derived type of Money that is applicable:

Method method = null; // current best method
Method[] methods = inherentReceiverType.getMethods();

for (int i = 0; i < methods.length; i++) {
  if (methods[i].getName().equals("accept")) {
    Class paramTypes[] = methods[i].getParameterTypes();
    // check if method is applicable
    if (paramTypes.length == 1 &&
      // check if current method is more specific
      if (method == null ||
        method = methods[i];

if (method.getParameterTypes()[0] == Money.class) {
  // selected method is current method, so throw exception to prevent recursion

In the above code, when more than one method is applicable, the technique selects the most specific method at run time using the same selection criteria Java uses to select one overloaded method over another at compile time, thus preserving Java's method selection philosophy. With multiple parameters, method selection becomes more complex.

Method Selection with Multiple Parameters

The basic approach to method selection can be extended to support multiple parameters. Since a fundamental goal is to preserve Java's method selection principles as defined in the Java language specification [4], we begin by defining these principles. We follow that with a description of the algorithm.

In Java, a method declaration is applicable to a method invocation if the names of the methods and number of parameters match, and the type of each argument passed can be converted to the type of the corresponding parameter in the declaration signature. A method is accessible if access modifiers allow the method to be invoked.

Given two applicable methods, one is more specific than the other if the type of each parameter of the first method can be converted to the type of the corresponding parameter of the second, and the type of at least one parameter of the first is not identical to the type of the corresponding parameter of the second. A method is maximally specific if it is applicable and accessible and there is no other accessible and applicable method that is more specific. A method is most specific if it is the only maximally specific method.

For example, consider three methods supported by an object x with the following signatures, where Float inherits from Number and DataInputStream inherits from InputStream:

f1: f(Number, InputStream)
f2: f(Number, DataInputStream)
f3: f(Float, InputStream)

All three methods are applicable for the following invocation:

x.f(new Float(), new DataInputStream())

Methods f2 and f3 are each more specific than f1 with respect to this invocation. Neither is more specific than the other, because f2 is a better match with respect to the second parameter, and f3 is a better match with respect to the first parameter. Since there is no method that is more specific, both are maximally specific. There is no most specific method, because there is more than one maximally specific method. If either f2 or f3 did not exist, then the remaining method would be the most specific method.

Java identifies the declaration of the most specific method at compile time by utilizing the declared type of the receiver and the declared types of the arguments passed. Furthermore, it binds to the actual method implementation at run time based on the inherent type of the receiver. In the covariant specialization technique, the most specific method is selected at run time based on the inherent type of the receiver and the inherent types of the arguments passed.

The basic technique may be modified to support multiple parameters with the following steps:

  1. Find the name of the method that is covariantly specialized. Java does not provide a simple way to get the name of the currently executing method. Either hard code the method name, or use the type safe technique described in the Considerations section.
  2. Get all applicable methods supported by the receiver. Call getMethods on the class of the receiver, and discard methods that are not applicable. Check that the method name and number of parameters are the same, and use isAssignableFrom to determine whether the parameter types are convertible.
  3. Find a maximally specific method. Consider the first method in the list of applicable methods to be the best candidate for maximally specific method. Examine each method in the list. If it is more specific than the previous best candidate, then it becomes the new best candidate. To determine whether one method is more specific than another, check whether each parameter type in the first method can be converted to the corresponding parameter type of the second method by calling isAssignableFrom. Although we now have a maximally specific method, there may be others.
  4. Determine whether the maximally specific method found is the most specific method. Examine each method in the list of applicable methods. If any method in that list is neither more nor less specific than the previously identified maximally specific method, then there is no most specific method, so raise an exception. Otherwise, the previously identified maximally specific method is the most specific method.
  5. Invoke the most specific method. Use the invoke method on the Method object for the most specific method.

Essentially, the algorithm implements a search to find a singular solution, if one exists.


When implementing and using the covariance technique presented in this article, there are various considerations to be aware of. The major ones are mentioned here.

Finding the method name. Deducing the method name at run time instead of hard coding it has the advantage of being less error prone and more general. Java does not provide an API to access the call stack or the currently executing method. However, this information is available in any Throwable object. One can construct a Throwable object, print the call stack captured in this object to a PrintStream, and then parse the stream to extract the names of the methods in the call chain.

Handling exceptions. There are exceptions that can occur with covariant specialization that need special attention. First, we must deal with exceptions thrown by the reflection methods executed in the covariance technique implementation. Second, the code that implements covariance must itself throw a java.lang.NoSuchMethodException if there is no implementation to dispatch. Third, when a covariantly specialized method is executed via invoke, and that method throws an exception, invoke throws an InvocationTargetException that contains the original exception. To propagate the original exception, it should be extracted and rethrown. To maintain correct semantics, a covariantly specialized method must throw exceptions that are subtypes of the exceptions thrown by the method that it specializes, and this should be enforced by the covariance code.

Handling primitive types. Previously we ignored the distinction between method invocation conversion and assignment conversion, because they are different only for primitive types [4]. To handle parameters of primitive types, we need to do something special for method selection and method invocation. For method selection, one must extract the Class object associated with a primitive parameter type to pass to isAssignableFrom. This can be done by using the class keyword on the primitive type; for example, int.class returns the Class object for int. For method invocation, one needs to call invoke passing an array of objects that represents the arguments. Since a primitive value is not an object, we must substitute an instance of the corresponding wrapper type in the array.

Accessibility. The covariant specialization technique is activated in the superclass method which is called by the Java runtime. In order for this method to be called, the Java compiler and runtime have already done the appropriate access checks. If the dispatching code is executed, it implies that the invoker of the method has the appropriate access to the package, class, and method. Further checks can be added in the dispatching code when it examines methods with compatible signatures, if desired.

Including only instance methods. Since only instance methods and not class methods may be specialized, the method selection algorithm must eliminate class methods from its list of applicable methods for dispatching in step 2.

Invariant overriding. Without special coding logic, one must not invariantly specialize a method that is intended to be covariantly specialized, because then the original implementation, which provides the dispatching code, is replaced with the new implementation. One way to combine covariance and invariance is to have the subclass's implementation of the method invoke the superclass's implementation, and then handle this case specially in the superclass to prevent a second dispatch to the subclass's method.

Creating a generic dispatcher. It is possible to create a class which implements the dispatcher in a generic manner in order to avoid duplicating the code in every class where you want to use covariance. Such a generic dispatcher could implement finding the name of the method to be executed, method selection, method invocation, primitive parameter type handling, and invariant overriding.

Performance. The performance penalty due to dynamic method lookup and selection can be mitigated by caching previous selection information. Furthermore, the method selection algorithm can be optimized by combining steps 2 and 3 into a single pass.

Declared type of receiver. If the declared type of the receiver is a subclass of the class that implements the dispatching to support covariant specialization, then covariance is not exhibited because the dispatching code, which is in the superclass, is not executed. To circumvent this, dispatching code must be embedded at multiple levels of the class hierarchy.


Earlier we hinted at some uses for covariant specialization. Now that the reader has a grasp of covariant specialization and how it may be implemented in Java, we discuss in more detail where the technique can be applied.

Use to separate state and behavior. The same data object may need to be manipulated in different ways by different applications, especially in large enterprises. It makes sense to separate these unrelated operations from the data object itself. When the data class is part of an inheritance hierarchy, we end up with two parallel hierarchies, one for the data classes and one for the behavior classes. The topmost behavior class may define a generic method that takes the topmost data class as a parameter, and this method could be covariantly specialized in derived behavior classes so that each behavior class takes the corresponding data class as a parameter.

Use to build extensible frameworks. Covariant specialization is often useful in frameworks. A framework cannot know about user-defined classes derived from framework classes, and therefore these derived classes cannot be used as parameter types within the framework. Methods in the framework must specify parameters that have types which are part of the framework.

For example, the Store and Money classes might be part of a framework that is used across many specific applications. The Store has an accept method that takes a parameter of type Money. When an application is built using this framework, it may need new types of money. The framework should be designed to accommodate different subtypes of Money that applications might introduce, without actually knowing them. Sears might add a store-specific credit card SearsCard as a acceptable form of money. In the absence of covariant specialization, the accept method provided by the Store class must be overridden by a subclass, and the overridden method must check whether the argument passed in is of type SearsCard and then downcast it. The problem gets worse as the number of types of money that must be specially dealt with increases, since we need a branch for each type. If the accept method in the Store class is replaced by a covariantly specialized accept method, user-defined store subclasses can implement type-specific accept methods, thus eliminating the need to downcast and the need for branch statements.

Another example of a framework situation where covariant specialization is useful is event notification [5]. An event notification framework knows only about a generic event type. Applications define specific types of events and handlers for them. When an event occurs, the framework code notifies the application, invoking the type-specific handler provided by the application for that event.

Use instead of the Visitor pattern. Covariance is often a better solution in situations where the Visitor pattern might otherwise be used. The Visitor pattern uses the double dispatch technique wherein the method implementation executed depends on two successive dispatches, the first one determined by the inherent type of the element and the second determined by the inherent type of the visitor [3].

If the Visitor pattern were applied to our example, the money hierarchy would be an element hierarchy manipulated by stores acting as visitors. The Visitor pattern is intrusive, requiring elements to implement an accept method to accept visitors. It is also not extensible, since adding a new element requires addition of a new method to the abstract visitor class and implementation of that method by each concrete visitor class. This is especially troublesome when new elements are added frequently. Using covariance is such situations eliminates this problem.

The Visitor pattern cannot be readily generalized to do dispatching based on inherent types of more than two objects. With covariant specialization, an implementation can be dispatched based on inherent types of more than two objects by covariantly specializing a method with multiple parameters.

Use to enhance design patterns. The method selection and dispatching technique described in this article to support covariant specialization can be used to enhance various design patterns. In patterns that use delegation, such as Mediator, State, and Strategy, the delegator can use this technique to select a method on the delegate for invocation.

Used with care, covariant specialization can lead to much more elegant designs. Unfortunately, few mainstream programming languages support this useful feature. The technique presented in this article makes covariant specialization available in a relatively transparent manner, and adds a valuable tool to the Java programmer's arsenal.


  1. Boyland, J. and G. Castagna. "Parasitic Methods: An Implementation of Multi-Methods for Java." Proceedings of OOPSLA '97: Object-Oriented Programming Systems, Languages and Applications, ACM Press, New York, October 1997
  2. Castagna, G. "Covariance and Contravariance: Conflict without a Cause." ACM Transactions on Programming Languages and Systems, Vol. 17 No. 3, ACM Press, New York, 1995.
  3. Gamma, E., R. Helm, R. Johnson, and J. Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, Reading, MA, 1995.
  4. Gosling, J., B. Joy, and G. Steele. The Java Language Specification, The Java Series, Addison-Wesley, Reading, MA, 1996,
  5. Gupta, S., J. Hartkopf, and S. Ramaswamy. "Event Notifier, A Pattern for Event Notification." Java Report, Vol. 3 No. 7, SIGS Publications, New York, July 1998.
  6. Hathaway, B. "Back to Basics: Greater Programming Power, Part 1." Object Currents, Vol. 1 No. 3, SIGS Publications, New York, March 1996.

Author Biographies

Suchitra Gupta received his Ph.D. from the State University of New York at Stony Brook. He worked for the Pennsylvania State University at University Park as an Assistant Professor of Computer Science. He contributed to the design and development of reservoir simulators at Shell Oil Company. At present he works for U S WEST. His recent work has focused on design and development of distributed applications using object-oriented techniques. He can be reached at

Jeffrey M. Hartkopf received his B.S. and M.S. in Computer Science from the University of Colorado at Boulder. He works for U S WEST on a groundbreaking project in computing systems management. He has also participated in the development of an enterprise platform for large scale client-server applications. He can be reached at

Suresh Ramaswamy received his B.S. in Electrical Engineering from the University of Bombay and his M.S. in Electrical Engineering from the University of Southern California. He worked on CAD application development at Mentor Graphics Corporation. Presently, he works for U S WEST on the use of distributed object technologies for building enterprise-class client-server applications. He also teaches introductory and advanced Java programming courses at the University of Colorado at Denver. He can be reached at Lucid Field.