8

I have a class with code like the following, where I want it to be trivial to use any class/type which represents a number. I find myself defining a large amount of methods, like the following:

public class Range {
    private BigDecimal inferior = new BigDecimal(0);
    private BigDecimal superior = new BigDecimal(1);

    public Range(BigDecimal inferior, BigDecimal superior) {
        if (inferior.compareTo(superior) == -1) {
            this.inferior = inferior;
            this.superior = superior;
        }
    }
    public Range(int inferior, int superior) {
        this(new BigDecimal(inferior), new BigDecimal(superior));
    }
    public Range(Integer inferior, Integer superior) {
        this(new BigDecimal(inferior), new BigDecimal(superior));
    }
    public Range(float inferior, float superior) {
        this(new BigDecimal(inferior), new BigDecimal(superior));
    }
    public Range(double inferior, double superior) {
        this(new BigDecimal(inferior), new BigDecimal(superior));
    }
}

I haven't even written every combination possible! For instance, one which takes a float and a double, or an int and a BigDecimal.

How could this be achieved in a clean way, so that there are parameters valid for multiple classes/data types which are already predefined, or even primitives? I've considered adapters and proxies, but I regularly find myself not understanding the explanations and I can't figure out if they fit my use case and if so how - this question may have already been answered on SO, but if so at least I would like to see if anyone can explain it to me according to this particular example.

Xerz
  • 314
  • 1
  • 4
  • 15
  • 2
    You might want to look into [using generics](https://docs.oracle.com/javase/tutorial/java/generics/types.html). Generics will not work with primitives though, so you'd have to use the respective wrapper classes for the primitive types. This could potentially allow you to extend Ranges beyond purely numeric classes as well. – Mihir Kekkar Sep 23 '19 at 22:42
  • Make use of `BigDecimal(String)`. See my answer for more details. – Vince Sep 25 '19 at 01:07

3 Answers3

5

Use the Builder Pattern. Create a nested static class that accepts each of the distinguishing datatypes for each of the two numbers. Primitive types from byte through long will be widened to long, and float to double. BigIntegers can be converted to BigDecimals, and BigDecimal references will be copied.

public static class Builder {
    BigDecimal first;
    BigDecimal second;

    public void setFirst(long value) { first = new BigDecimal(value); }
    public void setFirst(double value) { first = new BigDecimal(value); }
    public void setFirst(BigInteger value) { first = new BigDecimal(value); }
    public void setFirst(BigDecimal value) { first = value; }
    public void setSecond(long value) { second = new BigDecimal(value); }
    public void setSecond(double value) { second = new BigDecimal(value); }
    public void setSecond(BigInteger value) { second = new BigDecimal(value); }
    public void setSecond(BigDecimal value) { second = value; }
    public Range build() {
        if (first == null || second == null) {
            throw new IllegalArgumentException("Must supply both values.");
        }
        return new Range(first, second);
    }
}

The Builder pattern allows validation before building the desired object, and it bypasses the "constructor explosion" that would occur attempting to cover every possible combination. With n possible types, you have 2 * n builder setter methods instead of n2 constructors.

I included long, even though it can be widened to double legally, for precision reasons, because there are very high values of type long that can't be precisely represented as doubles.

Then, your constructor becomes:

public Range(BigDecimal first, BigDecimal second) {
    if (first.compareTo(second) < 0) {
        this.inferior = first;
        this.superior = second;
    }
    else {
        this.inferior = second;
        this.superior = first;
    }
}

I changed the == -1 to < 0 to match the compareTo contract, and added the else case that switches them if needed.

rgettman
  • 176,041
  • 30
  • 275
  • 357
  • A lot of boilerplate. – mentallurg Sep 24 '19 at 09:57
  • A builder object is for *optional* configuration. Using it for *required* configuration can result in requirements not being configured, which can lead to bugged code. – Vince Sep 24 '19 at 23:23
  • @VinceEmigh I thought the `IllegalArgumentException` was thrown for that – Xerz Sep 30 '19 at 15:47
  • @Xerz Yes, but it's turning what should be a compiler error into a runtime error. Builders were designed to allow clients to pass in optional values. People tend to default to it as it's an "easy" solution - check out [this answer](https://stackoverflow.com/a/34465569/2398375) I wrote expressing the same thing: builders are easily abused, resulting in overlooking the real solution. Especially in this case, any new types would require you to modify the builder, violating O/C. It's not scalable. You need a proper interface: `Number`, which values can subtype. – Vince Dec 28 '19 at 16:39
2

Use class Number:

public Range(Number inferior, Number superior)

Integer, Long, Double - all of them are subclasses of Number.

Alternatively, use generics:

public class Range<T> {
    private T inferior;
    private T superior;

    public Range(T inferior, T superior) {
        this.inferior = inferior;
        this.superior = superior;
    }
}

Usage:

Range<Long> rangeLong = new Range<>(0L, 1000000000L);
Range<Double> rangeDouble= new Range<>(0d, 457.129d);
mentallurg
  • 4,967
  • 5
  • 28
  • 36
  • `BigDecimal` doesn't have a constructor that takes a `Number`. – Johannes Kuhn Sep 23 '19 at 22:49
  • Why do you want to have inferior and superior as BigDecimal? – mentallurg Sep 23 '19 at 22:50
  • Ohh, I know, let's make a subclass for every number type. That's much less boilerplate. – Johannes Kuhn Sep 23 '19 at 22:51
  • Subclasses still mean boilerplate. Use generics. See example above. – mentallurg Sep 23 '19 at 22:58
  • I'm probably not going to accept this answer since I don't feel it answers the overall issue of multiple type parameters, BUT `Number` works really well and allows taking primitive `int`s, `float`s, `double`s and `long`s! – Xerz Sep 30 '19 at 15:43
  • 1
    Also, see [@VinceEmigh's answer](https://stackoverflow.com/questions/58070970/multiple-type-parameters-in-java-methods-including-existing-classes-and-primiti#58086624) – Xerz Sep 30 '19 at 15:57
  • Using one type parameter wouldn't allow him to do what he needs: "*I haven't even written every combination possible! For instance, one which takes a float and a double, or an int and a BigDecimal.*". You would need 2, but even then, the values would not be `BigDecimal` by the time they're assigned to the field, which is what the OP desires - **this would not work**. Also, there's no explanation as to *how* you would use `Range(Number, Number)`. – Vince Oct 04 '19 at 00:50
-1

From the docs of BigDecimal(String):

This is generally the preferred way to convert a float or double into a BigDecimal, as it doesn't suffer from the unpredictability of the BigDecimal(double) constructor.


Accept Number, use String.valueOf to convert to String, then pass into BigDecimal:

public class Range {
    private BigDecimal inferior;
    private BigDecimal superior;

    public Range(Number inferior, Number superior) {
        this.inferior = new BigDecimal(String.valueOf(inferior));
        this.superior = new BigDecimal(String.valueOf(superior));
    }
}

Any classes which extend Number, including new types you introduce, will automatically be supported.

Vince
  • 14,470
  • 7
  • 39
  • 84
  • Oh, I'll take a look into this as soon as I can, seems promising! If anything I'm surprised that you need to convert to `String`s in order to avoid unpredictability, seems like a needless step – Xerz Sep 29 '19 at 09:33
  • When the real types you use are Integer or Long, then you get much overhead because of inefficient memory and CPU usage because of BigDecimal. If this is used in a home work in the school, then it doesn't matter. But if it is used in some reall application, this can be problem. – mentallurg Sep 30 '19 at 20:09
  • @mentallurg In what ways? Do you have any benchmarks to prove that? There may be a performance drop due to potential boxing & parsing to a `String`, but `BigDecimal(long)` parses the value to a `BigInteger` if the value exceeds 32 bits, so the performance drop may not be as significant as a scalable interface. – Vince Oct 02 '19 at 02:38
  • Regarding memory: What kind of benchmark you need? Look at the internal structure of BigDecimal and compare it to the other types. Regarding performance: write a test and you will see. Example: http://java-performance.info/bigdecimal-vs-double-in-financial-calculations/. One of tests shows that the same logic implemented with BigDecimal is 8 times slower, for other operation it is **200 times slower**, 4.1s vs 0.018s. It makes difference if your application needs 1 hour or 200 hours (more than a week) for the same operation. – mentallurg Oct 02 '19 at 06:02
  • @mentallurg That's comparing operations on a `BigDecimal` to operations on a primitive `long`... He's using `BigDecimal`, not `long`. Those statistics don't apply here. Show a benchmark proving how the use of `BigDecimal(long)` is x times faster than `BigDecimal(String)` – Vince Oct 02 '19 at 14:39
  • We don't care about BigDecimal(long) vs BigDecimal(String) or any other BigDecimal. My first comment is about simple types like Long (or Double, or Float) vs BigDecimal. That is is the problem of your approach. – mentallurg Oct 02 '19 at 18:22
  • @mentallurg He's not using `Long`, he's using `BigDecimal`. He needs `BigDecimal`, which is why your article doesn't apply here. He's not using primitives, which is what was used for the 0.018. The article does not show a 200x slow down with my approach. – Vince Oct 04 '19 at 00:22
  • @Xerz Did this fit your needs? – Vince Oct 04 '19 at 00:38
  • @VinceEmigh Hey, sorry for my late answer! It was for a small homework example so for that it did fine, however I'm unsure which option works best for a more professional codebase. – Xerz Dec 25 '19 at 17:38
  • @Xerz Using `BigDecimal(String)` is a must for desirable results from floating point values. As for how you design the interface is up to you. A builder wouldn't be scalable, as supporting new types require you to modify the builder, violating the Open/Close principle. As long as any new types extend `Number`, the `Range` will support that type. – Vince Dec 25 '19 at 21:00