Outbound Parameters

Though it is not always discussed in introductory programming courses, parameters can be used to pass information to a method (i.e., inbound parameters), to pass information from a method (i.e., outbound parameters), or to do both (i.e., in-out parameters). While some programming languages make this explicit, Java does not.

Motivation

In Java, a method can only return a single value or reference, and this is sometimes inconvenient. Suppose, for example, you want to write a method that is passed a double[] and returns both the maximum value and the minimum value. One way to achieve the desired result is to construct and return a double[] that contains two elements, the maximum and the minimum. Another way to achieve the desired result is to create a class called Range that contains two attributes, the minimum and maximum, and construct and return an instance of it. This chapter considers a third approach — outbound parameters.

Review

In order to understand the use of outbound parameters in Java, it is critical to understand parameter passing. In particular, it is critical to understand that Java passes all parameters by value. This means that the formal parameter (sometimes called the parameter) is, in fact, a copy of the actual parameter (sometimes called the argument). This is important because it means that, though a method can change the formal parameter, it can’t change the actual parameter.

Thinking About The Problem

At first glance, this might make you think that it is impossible to have outbound parameters in Java. However, when a parameter is a reference type, even though the formal parameter is a copy of the actual parameter, the formal and actual parameters refer to the same object. That is, they are aliases. Hence, if the object is mutable, a method can change the attributes of that object.

With this in mind, there are three different situations to consider, corresponding to the need to pass each of the following:

  1. A mutable reference type;
  2. A value type; or
  3. An immutable reference type.

Each situation must be handled slightly differently.

The first situation is the easiest to handle. In this case, the method simply changes the attributes of the outbound formal parameter (which is an alias for the actual parameter).

The second situation is slightly more complicated. In this case, changing the formal parameter has no impact on the actual parameter. What you’d like to do is, somehow “convert” the value type to a reference type. While this isn’t possible, you can, instead, create a wrapper class, that serves the same purpose. For example, if you want to have an outbound int parameter then you write an IntWrapper class like the following:

public class IntWrapper {
    private int   wrapped;
    
    public IntWrapper() {
        set(0);
    }
    
    public IntWrapper(int i) {
        set(i);
    }

    public int get() {
        return wrapped;
    }

    public void set(int i) {
        wrapped = i;
    }
}

The third situation is more like the second than the first. Since the parameter is immutable, even though the formal parameter is an alias, there is no way to change the attributes of the object being referred to. Hence, you must again create a wrapper class. For example, if you want to have an outbound Color parameter (which is immutable) then you write a ColorWrapper class like the following:

import java.awt.Color;

public class ColorWrapper {
    private Color wrapped;

    public ColorWrapper() {
        set(null);
    }

    public ColorWrapper(Color c)
    {
        set(c);
    }

    public Color get() {
        return wrapped;
    }

    public void set(Color c) {
        wrapped = c;
    }
}

The Pattern

What all of this means is that to make use of this pattern you must complete several steps.

1. Write a wrapper class if necessary;

2. Declare a method with an appropriate signature;

3. (See below);

4. Perform the necessary operations in the body of the method; and

5. Modify the attributes of the outbound parameter.

To use the pattern in this form, the invoker of the method must then construct an “empty” instance of the outbound parameter and pass it to the method. When the method returns, the values of the outbound parameter will have been set, and the invoker can then make use of it.

The solution can be improved by giving the invoker the flexibility to either use an outbound parameter for the result or to return the result. The invoker can signal its preference by passing either an empty/uninitialized outbound object or null. In the latter case, the method will construct an instance of the outbound parameter, modify it, and return it. In the former case, the method will modify the given outbound parameter, and return it (for consistency).

This leads to the following additional steps (which you may have noticed are missing above):

3. At the top of the method, check to see if the outbound parameter is null and, if it is, construct an instance of the outbound parameter;

6. Return the outbound parameter.

Examples

Some examples will help to clear up any confusion you may have.

Outbound Arrays

Returning to the motivating example, if you want to simultaneously calculate the minimum and maximum elements of a double[], you can use the pattern to create an extremes() method like the following:

    public static double[] extremes(double[] data, double[] range) {
        if (range == null) range = new double[2];

        range[0] = Double.POSITIVE_INFINITY;
        range[1] = Double.NEGATIVE_INFINITY;

        for (int i = 0; i < data.length; i++) {
           if (data[i] < range[0]) range[0] = data[i];
           if (data[i] > range[1]) range[1] = data[i];
        }

        return range;
}

You can then invoke this method in either of two ways.

On the one hand, you can construct the array to hold the outbound parameter as follows:

        double[] temperatures = {75.3, 81.9, 68.2, 67.9};
        double[] lowhigh = new double[2];
        
        extremes(temperatures, lowhigh);

The variable that was constructed to contain the outbound parameters can then be used normally. In this case, lowhigh[0] will contain the minimum, and lowhigh[1] will contain the maximum.

On the other hand, you can pass null as the outbound parameter and allow the method to construct and return it, as follows:

        double[] temperatures = {75.3, 81.9, 68.2, 67.9};
        double[] lowhigh;
        
        lowhigh = extremes(temperatures, null);

After the return, lowhigh can be used exactly as in the previous example. The difference is that the memory for the array was allocated in the method named extremes() rather than in the invoker.

Note that it is not necessary to pass null explicitly. One can, instead, pass a variable that has been assigned the reference null, as in the following example:

        double[] temperatures = {75.3, 81.9, 68.2, 67.9};
        double[] lowhigh = null;
        
        lowhigh = extremes(temperatures, lowhigh);

The difference is purely stylistic, though some people prefer the explicit approach for clarity reasons.

Outbound Mutable Objects

Continuing with the same example, instead of using an array for the outbound parameter, you can create a class of mutable objects named Range to accomplish the same thing, as follows:

public class Range {
    
    private   double max, min;
    
    public Range() {
        set(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
    }
    
    public Range(double min, double max) {
        set(min, max);
    }

    public double getMax() {
        return max;
    }

    public double getMin() {
        return min;
    }

    public void set(double min, double max) { 
        this.min = min;
        this.max = max;
    }
}

The method for finding the minimum and maximum (now named extrema() rather than extremes() to avoid any confusion) can then be implemented as follows:

    public static Range extrema(double[] data, Range range) {
        if (range == null) range = new Range();
        double  max, min;

        min = Double.POSITIVE_INFINITY;
        max = Double.NEGATIVE_INFINITY;

        for (int i = 0; i < data.length; i++) { 
            if (data[i] < min) min = data[i];
            if (data[i] > max) max = data[i];
        }
        range.set(min, max);
        return range;
     }

It can then be invoked with a second parameter that is explicitly null or a Range variable that has been assigned the value null as in the earlier example.

Should you want to include both versions (i.e., the one that is passed/returns a double[] and the one that is passed/returns a Range) and want to be able to explicitly pass null as the second parameter, then the two methods must have different names. Otherwise, the invocation will be ambiguous (i.e., the compiler will not be able to determine which version you want to invoke because null does not have the type of the second parameter in either version).

Outbound Value Types

Now suppose that you want to write a method that is passed an int[] and calculates the number of positive elements, the number of negative elements, and the number of zeroes. You could return an array containing these values, but this approach is prone to error because you must remember which index corresponds to which value. So, you decide to use outbound parameters.

However, as discussed above, you can’t use int values directly; instead you must use a wrapper. This leads to the following implementation:

    public static void summarize(int[] data, 
                                 IntWrapper positives, 
                                 IntWrapper negatives,
                                 IntWrapper zeroes) {
        int neg = 0, pos = 0, zer = 0;

        for (int i = 0; i < data.length; i++) {
            if (data[i] < 0) neg++;
            else if (data[i] > 0) pos++;
            else zer++;
        }
        positives.set(pos);
        negatives.set(neg);
        zeroes.set(zer);
    }

Note that, in this example, the method doesn’t return anything. Hence, the invoker must construct the outbound parameters.

Outbound Immutable Objects

Finally, suppose that you are obsessed with your University’s color palette (e.g., purple and gold), and that you want to write a method that converts any Color to the main color in that palette (e.g., purple). Since Color objects are immutable, you must wrap the parameter as discussed above. You can then implement the purpleOut() method as follows:

    public static ColorWrapper purpleOut(ColorWrapper wrapper) {
        if (wrapper == null) wrapper = new ColorWrapper();

        wrapper.set(new Color(69, 0, 132));
        return wrapper;
    }

It can then be invoked as follows:

        ColorWrapper color = new ColorWrapper(Color.RED);

        purpleOut(color);

A Warning

You might be wondering why you had to write an IntWrapper class when the Java API includes the Integer, Double, Boolean, etc. classes. While those classes are also wrappers, they were designed for a different purpose. Specifically, they were created so that wrapped value types could be added to collections (which hold references). As it turns out, the objects in those classes are immutable and, hence, can not be used for the purposes of this chapter.



Licenses and Attributions


Speak Your Mind

-->