简体   繁体   中英

Adding arbitrary Number class vars

I want to sum a list of Number object and for each to use only the real value (if it's an Integer I want to use only .intValue() method and not .doubleValue ex..) and I don't want to use instanceof .

The return value needs to be of Number type.

How can I do it with double dispatch or strategy pattern or something similar?

I can't extend each implementing class of Number and I can't sum two Number vars.

there are only 6 .xValue() methods in Number and I want to use each of them accordingly.

Since the actually returned type is relevant to the caller and has not much use for the caller when it still is non-obvious due to a declared type of Number , it should be under the caller's control and combined with a generic type signature, which allows the caller to actually use a particular return type. Eg

public static <N extends Number, R extends Number> R sum(
        List<? extends N> input, Function<? super N, ? extends R> cast,
        BinaryOperator<R> addition) {

    return input.stream().<R>map(cast).reduce(addition).orElse(null);
}
public static <N extends Number> N sum(
        List<? extends N> input, BinaryOperator<N> addition) {

    return sum(input, Function.identity(), addition);
}

This allows to request the calculation to be within the input types, eg

List<Integer> list = Arrays.asList(1, 2, 3, 4);
Integer iSum1 = sum(list, Integer::sum);
Integer iSum2 = sum(list, Math::addExact);//throw on overflow

but also widening the type before summing up:

Long lSum = sum(list, Integer::longValue, Long::sum);

Likewise, you can handle Long or Double input types:

List<Long> list = Arrays.asList(1L, 2L, 3L, 4L);
Long lSum1 = sum(list, Long::sum);
Long lSum2 = sum(list, Math::addExact);//throw on overflow
// without precision loss:
BigInteger biSum = sum(list, BigInteger::valueOf, BigInteger::add);
List<Double> list = Arrays.asList(1.0, 2.0, 3.0, 4.0);
Double dSum = sum(list, Double::sum);
// without precision loss:
BigDecimal bdSum = sum(list, BigDecimal::valueOf, BigDecimal::add);

Or deal with mixed types:

List<Number> list = Arrays.asList(1, 2L, 3.0, 4F);
Double dSum = sum(list, Number::doubleValue, Double::sum);
BigDecimal bdSum = sum(list, n -> new BigDecimal(n.toString()), BigDecimal::add);

Note that Java's Number type hierarchy does not reflect the type conversion rules of the primitive types. So while a mixture of int and long values could be handled as long whereas mixing int and double would require using double to prevent loss of precision, there is no difference between mixing Integer and Long vs. mixing Integer and Double , both are just mixtures of different Number subtypes. So in either case, you need a Number::xxxValue conversion in-between and regardless of the actual combination, any Number::xxxValue conversion would compile without a warning, even when it implies loss of precision.

Since large long values could lose precision when being converted to double , the last example use an intermediate String value, to ensure that, in the presence of long and double input value, all conversions to BigDecimal are lossless.

Just to summarize what was also discussed in the comments/chat.

Disclaimer(s): do not use this code in production or just be sure to understand what it really does. By no means is this solution the way to go or best practice. Returning a Number whose type is adapted and a caller doesn't realize will give you troubles sooner or later. Please have a look at Holgers answer if you want to solve it in a caller-friendly way. This answer here just solves the issue of the OP the way he requested it. It hasn't any real benefit. It's basically here to just show what a bad idea it could be to solve it the way it was requested ;-). That having said, lets begin...

One way to define a strategy:

class Strategy {
    Predicate<Number> predicate;
    UnaryOperator<Number> transformation;
    Strategy(Predicate<Number> predicate, UnaryOperator<Number> transformation) {
        this.predicate = predicate;
        this.transformation = transformation;
    }
    boolean applies(Number number) {
        return predicate.test(number);
    }

    Number transformNumber(Number number) {
        return transformation.apply(number);
    }
}

A list of possible strategies could then look like

List<Strategy> strategies = Arrays.asList(
        new Strategy(n -> n.byteValue() == n.doubleValue(), Number::byteValue),
        new Strategy(n -> n.shortValue() == n.doubleValue(), Number::shortValue),
        new Strategy(n -> n.intValue() == n.doubleValue(), Number::intValue),
        new Strategy(n -> n.longValue() == n.doubleValue(), Number::longValue),                                                // please read the disclaimer again...  
        new Strategy(n -> n.floatValue() == n.doubleValue(), Number::floatValue),                                              // please spare your comments :-)
        new Strategy(n -> true, Number::doubleValue)                                                                           // ... lets continue!
);

A simple sum and the application of the strategies:

Optional<Number> sum(Number... numbers) {
    return Arrays.stream(numbers)
            .reduce(this::sumBasedOnStrategy);
}
Number sumBasedOnStrategy(Number one, Number two) {
    Number result = one.doubleValue() + two.doubleValue();
    return strategies.stream()
            .filter(s -> s.applies(result))
            .map(s -> s.transformNumber(result))
            .findFirst()
            .orElseThrow(() -> new IllegalArgumentException("No known strategy for the given number"));
}

Now testing the sum strategies:

Stream.of(1, 256, 66000, 3000000000L, 1.1f, 3.4f, Double.MAX_VALUE)
            .map(n -> sum(n, 1))
            .map(Optional::get)
            .map(Object::getClass)
            .forEach(System.out::println);

What would you expect?

class java.lang.Byte
class java.lang.Short
class java.lang.Integer
class java.lang.Long
class java.lang.Double // really?
class java.lang.Float
class java.lang.Double

And here are the corresponding sum results...

2
257
66001
3000000001
2.100000023841858 // thank you for your attention ;-)
4.4
1.7976931348623157E308

Note that there are also other constellations that lead to incorrect results. And again: what does it help to get a Integer after summing two double values? As Holger also showed in his comment (quoted):

A result value based selection would only work with a declared return type of Number , so the caller would not even notice the changing type, which would cause problems without a benefit. Think of Number n1 = 0.5 + 0.5, n2 = sum(0.5, 0.5); where n1.equals(n2) would yield false as 1 ( Integer ) is not equal to 1.0 ( Double ).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM