JDK-8286139 : Add methods Math::to*Exact([long|double]) for safely checked primitive casts
  • Type: CSR
  • Component: core-libs
  • Sub-Component: java.lang
  • Priority: P4
  • Status: Provisional
  • Resolution: Unresolved
  • Fix Versions: tbd
  • Submitted: 2022-05-04
  • Updated: 2024-01-22
Related Reports
CSR :  
Relates :  
Description
Summary
-------

Add a family of Math::to*Exact([long|double]) methods, as described in [JDK-8279986: methods Math::asXExact for safely checked primitive casts][1]

This also applies to StrictMath.


  [1]: https://bugs.openjdk.java.net/browse/JDK-8279986

Specification
-------------

Both `java.lang.Math` and `java.lang.StrictMath` are extended with the new API points below.

Note that the spec for `toIntExact(long)` has *not* changed, except for minor typographical corrections.

```
    /**
     * Returns the value of the {@code long} argument,
     * throwing an exception if the value overflows an {@code int}.
     *
     * @param value the {@code long} value
     * @return the argument as an {@code int}
     * @throws ArithmeticException if the argument overflows an {@code int}
     * @since 1.8
     */
    public static int toIntExact(long value) {

    /**
     * Returns the value of the {@code double} argument,
     * throwing an exception if the conversion is inexact.
     * The method returns if and only if the argument and the result
     * are mathematically equal.
     *
     * <p>Special cases:
     * <ul>
     * <li>If the argument is {@link Double#NEGATIVE_INFINITY},
     * {@link Double#POSITIVE_INFINITY} or {@link Double#NaN},
     * the method throws.
     * <li>If the argument is {@code -0.0} or {@code 0.0},
     * the method returns {@code 0}.
     * </ul>
     *
     * @param value the {@code double} value
     * @return the argument as a {@code int}
     * @throws ArithmeticException if the conversion is inexact
     * @see Math#rint(double)
     * @see Math#round(double)
     * @since 19
     */
    public static int toIntExact(double value) {

    /**
     * Returns the value of the {@code long} argument,
     * throwing an exception if the value overflows a {@code short}.
     *
     * @param value the {@code long} value
     * @return the argument as a {@code short}
     * @throws ArithmeticException if the argument overflows a {@code short}
     * @since 19
     */
    public static short toShortExact(long value) {

    /**
     * Returns the value of the {@code long} argument,
     * throwing an exception if the value overflows a {@code byte}.
     *
     * @param value the {@code long} value
     * @return the argument as a {@code byte}
     * @throws ArithmeticException if the argument overflows a {@code byte}
     * @since 19
     */
    public static byte toByteExact(long value) {

    /**
     * Returns the value of the {@code long} argument,
     * throwing an exception if the conversion is inexact.
     * The method returns if and only if the argument and the result
     * are mathematically equal.
     *
     * <p>Special case:
     * <ul>
     * <li>If the argument is {@code 0L}, the method returns {@code 0.0f}.
     * </ul>
     *
     * @param value the {@code long} value
     * @return the argument as a {@code float}
     * @throws ArithmeticException if the conversion is inexact
     * @since 19
     */
    public static float toFloatExact(long value) {

    /**
     * Returns the value of the {@code double} argument,
     * throwing an exception if the conversion is inexact.
     * The method returns if and only if the argument and the result
     * are mathematically equal.
     *
     * <p>Special cases:
     * <ul>
     * <li>If the argument is {@link Double#NEGATIVE_INFINITY},
     * {@link Double#POSITIVE_INFINITY}, {@link Double#NaN},
     * {@code -0.0} or {@code 0.0},
     * the method returns {@link Float#NEGATIVE_INFINITY},
     * {@link Float#POSITIVE_INFINITY}, {@link Float#NaN},
     * {@code -0.0f} or {@code 0.0f}, respectively.
     * </ul>
     *
     * @param value the {@code double} value
     * @return the argument as a {@code float}
     * @throws ArithmeticException if the conversion is inexact
     * @since 19
     */
    public static float toFloatExact(double value) {

    /**
     * Returns the value of the {@code long} argument,
     * throwing an exception if the conversion is inexact.
     * The method returns if and only if the argument and the result
     * are mathematically equal.
     *
     * <p>Special case:
     * <ul>
     * <li>If the argument is {@code 0L}, the method returns {@code 0.0}.
     * </ul>
     *
     * @param value the {@code long} value
     * @return the argument as a {@code double}
     * @throws ArithmeticException if the conversion is inexact
     * @since 19
     */
    public static double toDoubleExact(long value) {

    /**
     * Returns the value of the {@code double} argument,
     * throwing an exception if the conversion is inexact.
     * The method returns if and only if the argument and the result
     * are mathematically equal.
     *
     * <p>Special cases:
     * <ul>
     * <li>If the argument is {@link Double#NEGATIVE_INFINITY},
     * {@link Double#POSITIVE_INFINITY} or {@link Double#NaN},
     * the method throws.
     * <li>If the argument is {@code -0.0} or {@code 0.0},
     * the method returns {@code 0L}.
     * </ul>
     *
     * @param value the {@code double} value
     * @return the argument as a {@code long}
     * @throws ArithmeticException if the conversion is inexact
     * @see Math#rint(double)
     * @see Math#round(double)
     * @since 19
     */
    public static long toLongExact(double value) {

    /**
     * Returns the value of the {@code long} argument,
     * throwing an exception if the value overflows the range
     * [0, 2<sup>{@link Integer#SIZE}</sup>) of an unsigned int.
     *
     * @param value the {@code long} value
     * @return the argument as an unsigned int
     * @throws ArithmeticException if the argument overflows an unsigned int
     * @since 19
     */
    public static long toUnsignedIntRangeExact(long value) {

    /**
     * Returns the value of the {@code long} argument,
     * throwing an exception if the value overflows the range
     * [0, 2<sup>{@link Short#SIZE}</sup>) of an unsigned short.
     *
     * @param value the {@code long} value
     * @return the argument as an unsigned short
     * @throws ArithmeticException if the argument overflows an unsigned short
     * @since 19
     */
    public static int toUnsignedShortRangeExact(long value) {

    /**
     * Returns the value of the {@code long} argument,
     * throwing an exception if the value overflows the range
     * [0, 2<sup>{@link Byte#SIZE}</sup>) of an unsigned byte.
     *
     * @param value the {@code long} value
     * @return the argument as an unsigned byte
     * @throws ArithmeticException if the argument overflows an unsigned byte
     * @since 19
     */
    public static int toUnsignedByteRangeExact(long value) {
```

Comments
As a point of comparison, see the API proposed in JDK-8304487.
22-01-2024

I don’t object to the name changes for the unsigned guys. I do agree that the special case of -0.0 is different from the other special cases, because mathematical values are not corrupted when the sign of -0.0 “rubs off” and leaves 0.0. If we wanted to give an API for control for -0.0 (that is, to throw on -0.0) it should be factored out as a hypothetical method such as `toMathematicalValueExact(double)` which throws on -0.0, NaN, and infinity. That can be chained with the methods in this RFE to control for -0.0, when users care about that. Should such a method be added to this RFE? I think it is not as useful or necessary as the other, although I would not object.
24-05-2022

Moving to Provisional, not Approved for JDK 20 (not 19). [~jrose], please review the CSR. Given concerns recently raised on the PR, I think it is prudent to consider these methods for addition in JDK 20 rather than 19 to allow more time to adjust the specification, if needed, in particular the handling of -0.0.
24-05-2022

Pre-review initial comments: Are are these methods needed? A conversion from double to byte is presumably rare. Initially I would say should a case should be handled by a composition like toByteExact(toIntExact(double)), of a double -> int and a int -> byte method. The methods taking floating-point arguments should explicitly describe handling of: - signed zero (I recommend against throwing on signed zero, but it does imply an exact mapping is not invertible) - infinity - NaN The integral type -> floating-point type mapping should state an integer 0 is mapped to +0.0.
05-05-2022

[~jrose], right; I don't think the -0.0 -> 0 case is a problem, just that it should be explicitly enumerated in the spec. [~rgiulietti], while the cases in question are what a reasonable programmer should expect given the general role of the methods, I think it is helpful to some users (including conformance testers) to have handling of the the special floating-point value explicitly discussed and also in keeping with the convention on specifications in the math library classes. Also consider adding @-see tags on the floating-point -> integral type methods to Math.rint and Math.round, two different ways to convert floating-point to integral values.
05-05-2022

Good point, Joe. We can get rid of at least five readily. Here is a larger matrix that shows one way we could go here: http://cr.openjdk.java.net/~jrose/draft/value-preserving-conv.pdf The idea is to support int/long/float/double fully but to give some of the other types a two-call factorization, such as when going from double to byte. Viewing these as "safer casts" is a good approach. The destruction of the sign on -0 is a wart, but it's not the fault of wanting "safer casts", it's just an intrinsic oddity of floating point. Regarding use cases, one reason for wanting exact conversions from floating point types when a numeric result must be asserted to be integral. For example, a math function might do something different on an integral input like pow. Or, a mesh algorithm might map fractionally addressed points to integral points in a Java array (indexed by int). In such cases, any of three bugs might happen when the floating point value is cast to an integral type: 1. precision might be lost (for a medium-sized integer), 2. overflow might happen (for a large integer), 3. a fractional value might appear. Just pasting a Java cast over the floating-point value hides all three bugs, while using an exact conversion defends against all three. Also, a coder might try to defend against these bugs "manually" with open-coded logic, but it is error prone, since in fact there are three different things that could go wrong.
04-05-2022

Indeed, I can only imagine that, out in the wild, double->[byte|short] are rarely seen conversions. I think most readers would consider all these methods as "safer" casts. Since casts have a JLS defined semantics, the readers would presumably already assume -0.0->0, 0->0.0, NaN->NaN, etc. Thus, while being explicit never hurts, restating "obvious" things might sound pedantic. My 2 cents.
04-05-2022