This final example will create truly generic code using the adapter approach described in the previous section. The example began as an attempt to create a sum over a sequence of elements (of any type that can be summed), but evolved into performing general operations using a functional style of programming.
If you just look at the process of trying to add objects, you can see that this is a case where we have common operations across classes, but the operations are not represented in any base class that we can specify—sometimes you can even use a'+' operator, and other times there may be some kind of "add"
method. This is generally the situation that you encounter when trying to write generic code, because you want the code to apply across multiple classes—especially, as in this case, multiple classes that already exist and that we have no ability to "fix." Even if you were to narrow this case to subclasses of Number, that superclass doesn't include anything about "addability."
The solution is to use the Strategy design pattern, which produces more elegant code because it completely isolates "the thing that changes" inside of a function object(You will sometimes see these called functors. I will use the term function object rather than functor, as the term "functor" has a specific and different meaning in mathematics).A function object is an object that in some way behaves like a function—typically, there's one method of interest (in languages that support operator overloading, you can make the call to this method look like an ordinary method call). The value of function objects is that, unlike an ordinary method, they can be passed around, and they can also have state that persists across calls. Of course, you can accomplish something like this with any method in a class, but (as with any design pattern) the function object is primarily distinguished by its intent. Here the intent is to create something that behaves like a single method that you can pass around; thus it is closely coupled with—and sometimes indistinguishable from—the Strategy design pattern.
As I've found with a number of design patterns, the lines get kind of blurry here: We are creating function objects which perform adaptation, and they are being passed into methods to be used as strategies.
Taking this approach, I added the various kinds of generic methods that I had originally set out to create, and more. Here is the result:
//: generics/Functional.java
import java.math.*;
import java.util.concurrent.atomic.*;
import java.util.*;
import static net.mindview.util.Print.*;
// Different types of function objects:
interface Combiner<T> { T combine(T x, T y); }
interface UnaryFunction<R,T> { R function(T x); }
interface Collector<T> extends UnaryFunction<T,T> {
T result(); // Extract result of collecting parameter
}
interface UnaryPredicate<T> { boolean test(T x); }
public class Functional {
// Calls the Combiner object on each element to combine
// it with a running result, which is finally returned:
public static <T> T
reduce(Iterable<T> seq, Combiner<T> combiner) {
Iterator<T> it = seq.iterator();
if(it.hasNext()) {
T result = it.next();
while(it.hasNext())
result = combiner.combine(result, it.next());
return result;
}
// If seq is the empty list:
return null; // Or throw exception
}
// Take a function object and call it on each object in
// the list, ignoring the return value. The function
// object may act as a collecting parameter, so it is
// returned at the end.
public static <T> Collector<T>
forEach(Iterable<T> seq, Collector<T> func) {
for(T t : seq)
func.function(t);
return func;
}
// Creates a list of results by calling a
// function object for each object in the list:
public static <R,T> List<R>
transform(Iterable<T> seq, UnaryFunction<R,T> func) {
List<R> result = new ArrayList<R>();
for(T t : seq)
result.add(func.function(t));
return result;
}
// Applies a unary predicate to each item in a sequence,
// and returns a list of items that produced "true":
public static <T> List<T>
filter(Iterable<T> seq, UnaryPredicate<T> pred) {
List<T> result = new ArrayList<T>();
for(T t : seq)
if(pred.test(t))
result.add(t);
return result;
}
// To use the above generic methods, we need to create
// function objects to adapt to our particular needs:
static class IntegerAdder implements Combiner<Integer> {
public Integer combine(Integer x, Integer y) {
return x + y;
}
}
static class
IntegerSubtracter implements Combiner<Integer> {
public Integer combine(Integer x, Integer y) {
return x - y;
}
}
static class
BigDecimalAdder implements Combiner<BigDecimal> {
public BigDecimal combine(BigDecimal x, BigDecimal y) {
return x.add(y);
}
}
static class
BigIntegerAdder implements Combiner<BigInteger> {
public BigInteger combine(BigInteger x, BigInteger y) {
return x.add(y);
}
}
static class
AtomicLongAdder implements Combiner<AtomicLong> {
public AtomicLong combine(AtomicLong x, AtomicLong y) {
// Not clear whether this is meaningful:
return new AtomicLong(x.addAndGet(y.get()));
}
}
// We can even make a UnaryFunction with an "ulp"
// (Units in the last place):
static class BigDecimalUlp
implements UnaryFunction<BigDecimal,BigDecimal> {
public BigDecimal function(BigDecimal x) {
return x.ulp();
}
}
static class GreaterThan<T extends Comparable<T>>
implements UnaryPredicate<T> {
private T bound;
public GreaterThan(T bound) { this.bound = bound; }
public boolean test(T x) {
return x.compareTo(bound) > 0;
}
}
static class MultiplyingIntegerCollector
implements Collector<Integer> {
private Integer val = 1;
public Integer function(Integer x) {
val *= x;
return val;
}
public Integer result() { return val; }
}
public static void main(String[] args) {
// Generics, varargs & boxing working together:
List<Integer> li = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
Integer result = reduce(li, new IntegerAdder());
print(result);
result = reduce(li, new IntegerSubtracter());
print(result);
print(filter(li, new GreaterThan<Integer>(4)));
print(forEach(li,
new MultiplyingIntegerCollector()).result());
print(forEach(filter(li, new GreaterThan<Integer>(4)),
new MultiplyingIntegerCollector()).result());
MathContext mc = new MathContext(7);
List<BigDecimal> lbd = Arrays.asList(
new BigDecimal(1.1, mc), new BigDecimal(2.2, mc),
new BigDecimal(3.3, mc), new BigDecimal(4.4, mc));
BigDecimal rbd = reduce(lbd, new BigDecimalAdder());
print(rbd);
print(filter(lbd,
new GreaterThan<BigDecimal>(new BigDecimal(3))));
// Use the prime-generation facility of BigInteger:
List<BigInteger> lbi = new ArrayList<BigInteger>();
BigInteger bi = BigInteger.valueOf(11);
for(int i = 0; i < 11; i++) {
lbi.add(bi);
bi = bi.nextProbablePrime();
}
print(lbi);
BigInteger rbi = reduce(lbi, new BigIntegerAdder());
print(rbi);
// The sum of this list of primes is also prime:
print(rbi.isProbablePrime(5));
List<AtomicLong> lal = Arrays.asList(
new AtomicLong(11), new AtomicLong(47),
new AtomicLong(74), new AtomicLong(133));
AtomicLong ral = reduce(lal, new AtomicLongAdder());
print(ral);
print(transform(lbd,new BigDecimalUlp()));
}
} /* Output:
28
-26
[5, 6, 7]
5040
210
11.000000
[3.300000, 4.400000]
[11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]
311
true
265
[0.000001, 0.000001, 0.000001, 0.000001]
*///:~
I begin by defining interfaces for different types of function objects. These were created on demand, as I developed the different methods and discovered the need for each. The Combiner class was suggested by an anonymous contributor to one of the articles posted on my Web site. The Combiner abstracts away the specific detail of trying to add two objects, and just says that they are being combined somehow. As a result, you can see that IntegerAdder and IntegerSubtracter can be types of Combiner.
A UnaryFunction takes a single argument and produces a result; the argument and result need not be of the same type. A Collector is used as a "collecting parameter," and you can extract the result when you're finished. A UnaryPredicate produces a boolean result. There are other types of function objects that can be defined, but these are enough to make the point.
The Functional class contains a number of generic methods that apply function objects to sequences. reduce( ) applies the function in a Combiner to each element of a sequence in order to produce a single result.
forEach( ) takes a Collector and applies its function to each element, ignoring the result of each function call. This can be called just for the side effect (which wouldn't be a "functional" style of programming but can still be useful), or the Collector can maintain internal state to become a collecting parameter, as is the case in this example.
transform( ) produces a list by calling a UnaryFunction on each object in the sequence and capturing the result.
Finally, filter( ) applies a UnaryPredicate to each object in a sequence and stores the ones that produce true in a List, which it returns.
You can define additional generic functions. The C++ STL, for example, has lots of them. The problem has also been solved in some open-source libraries, such as the JGA (Generic Algorithms for Java).
In C++, latent typing takes care of matching up operations when you call functions, but in Java we need to write the function objects to adapt the generic methods to our particular needs. So the next part of the class shows various different implementations of the function objects. Note, for example,that IntegerAdder and BigDecimalAdder solve the same problem-adding two objects—by calling the appropriate operations for their particular type. So that's the Adapter pattern and Strategy pattern combined.
In main( ), you can see that in each method call, a sequence is passed along with the appropriate function object. Also, a number of the expressions can get fairly complex, such as:
forEach(filter(li, new GreaterThan<Integer>(4)),new MultiplyingIntegerCollector()).result()
This produces a list by selecting all elements in li that are greater than 4, and then applies the MultiplyingIntegerCollector( ) to the resulting list and extracts the result( ). I won't explain the details of the rest of the code other than to say that you can probably figure it out by walking through it.