Books / Patterns for Beginning Programmers / Chapter 32
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:
- A mutable reference type;
- A value type; or
- 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.