RNG-176: Enhance the UniformRandomProvider interface

Add methods for streams and generation of numbers in a range. The
methods match those implementations in JDK 17 RandomGenerator interface.

Add default implementations of the existing interface methods using
nextLong() as the source of randomness.
diff --git a/commons-rng-client-api/pom.xml b/commons-rng-client-api/pom.xml
index 86353b0..8caa901 100644
--- a/commons-rng-client-api/pom.xml
+++ b/commons-rng-client-api/pom.xml
@@ -42,4 +42,31 @@
     <rng.jira.component>client-api</rng.jira.component>
   </properties>
 
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>com.github.siom79.japicmp</groupId>
+        <artifactId>japicmp-maven-plugin</artifactId>
+        <configuration>
+          <parameter>
+            <!-- Adding Java 8 default methods should not break binary compatibility. -->
+            <overrideCompatibilityChangeParameters>
+              <overrideCompatibilityChangeParameter>
+                <compatibilityChange>METHOD_NEW_DEFAULT</compatibilityChange>
+                <binaryCompatible>true</binaryCompatible>
+                <sourceCompatible>true</sourceCompatible>
+                <semanticVersionLevel>PATCH</semanticVersionLevel>
+              </overrideCompatibilityChangeParameter>
+              <overrideCompatibilityChangeParameter>
+                <compatibilityChange>METHOD_ABSTRACT_NOW_DEFAULT</compatibilityChange>
+                <binaryCompatible>true</binaryCompatible>
+                <sourceCompatible>true</sourceCompatible>
+                <semanticVersionLevel>PATCH</semanticVersionLevel>
+              </overrideCompatibilityChangeParameter>
+            </overrideCompatibilityChangeParameters>
+          </parameter>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
 </project>
diff --git a/commons-rng-client-api/src/main/java/org/apache/commons/rng/UniformRandomProvider.java b/commons-rng-client-api/src/main/java/org/apache/commons/rng/UniformRandomProvider.java
index 07d3108..1c48564 100644
--- a/commons-rng-client-api/src/main/java/org/apache/commons/rng/UniformRandomProvider.java
+++ b/commons-rng-client-api/src/main/java/org/apache/commons/rng/UniformRandomProvider.java
@@ -16,6 +16,10 @@
  */
 package org.apache.commons.rng;
 
+import java.util.stream.DoubleStream;
+import java.util.stream.IntStream;
+import java.util.stream.LongStream;
+
 /**
  * Applies to generators of random number sequences that follow a uniform
  * distribution.
@@ -25,24 +29,23 @@
 public interface UniformRandomProvider {
     /**
      * Generates {@code byte} values and places them into a user-supplied array.
-     * <p>
-     * The number of random bytes produced is equal to the length of the
+     *
+     * <p>The number of random bytes produced is equal to the length of the
      * the byte array.
-     * </p>
      *
      * @param bytes Byte array in which to put the random bytes.
      * Cannot be {@code null}.
      */
-    void nextBytes(byte[] bytes);
+    default void nextBytes(byte[] bytes) {
+        UniformRandomProviderSupport.nextBytes(this, bytes, 0, bytes.length);
+    }
 
     /**
      * Generates {@code byte} values and places them into a user-supplied array.
      *
-     * <p>
-     * The array is filled with bytes extracted from random integers.
+     * <p>The array is filled with bytes extracted from random integers.
      * This implies that the number of random bytes generated may be larger than
      * the length of the byte array.
-     * </p>
      *
      * @param bytes Array in which to put the generated bytes.
      * Cannot be {@code null}.
@@ -53,16 +56,19 @@
      * @throws IndexOutOfBoundsException if {@code len < 0} or
      * {@code len > bytes.length - start}.
      */
-    void nextBytes(byte[] bytes,
-                   int start,
-                   int len);
+    default void nextBytes(byte[] bytes, int start, int len) {
+        UniformRandomProviderSupport.validateFromIndexSize(start, len, bytes.length);
+        UniformRandomProviderSupport.nextBytes(this, bytes, start, len);
+    }
 
     /**
      * Generates an {@code int} value.
      *
      * @return the next random value.
      */
-    int nextInt();
+    default int nextInt() {
+        return (int) (nextLong() >>> 32);
+    }
 
     /**
      * Generates an {@code int} value between 0 (inclusive) and the
@@ -71,9 +77,29 @@
      * @param n Bound on the random number to be returned.  Must be positive.
      * @return a random {@code int} value between 0 (inclusive) and {@code n}
      * (exclusive).
-     * @throws IllegalArgumentException if {@code n} is negative.
+     * @throws IllegalArgumentException if {@code n} is not above zero.
      */
-    int nextInt(int n);
+    default int nextInt(int n) {
+        UniformRandomProviderSupport.validateUpperBound(n);
+        return UniformRandomProviderSupport.nextInt(this, n);
+    }
+
+    /**
+     * Generates an {@code int} value between the specified {@code origin} (inclusive) and
+     * the specified {@code bound} (exclusive).
+     *
+     * @param origin Lower bound on the random number to be returned.
+     * @param bound Upper bound (exclusive) on the random number to be returned.
+     * @return a random {@code int} value between {@code origin} (inclusive) and
+     * {@code bound} (exclusive).
+     * @throws IllegalArgumentException if {@code origin} is greater than or equal to
+     * {@code bound}.
+     * @since 1.5
+     */
+    default int nextInt(int origin, int bound) {
+        UniformRandomProviderSupport.validateRange(origin, bound);
+        return UniformRandomProviderSupport.nextInt(this, origin, bound);
+    }
 
     /**
      * Generates a {@code long} value.
@@ -89,28 +115,308 @@
      * @param n Bound on the random number to be returned.  Must be positive.
      * @return a random {@code long} value between 0 (inclusive) and {@code n}
      * (exclusive).
-     * @throws IllegalArgumentException if {@code n} is negative.
+     * @throws IllegalArgumentException if {@code n} is not greater than 0.
      */
-    long nextLong(long n);
+    default long nextLong(long n) {
+        UniformRandomProviderSupport.validateUpperBound(n);
+        return UniformRandomProviderSupport.nextLong(this, n);
+    }
+
+    /**
+     * Generates a {@code long} value between the specified {@code origin} (inclusive) and
+     * the specified {@code bound} (exclusive).
+     *
+     * @param origin Lower bound on the random number to be returned.
+     * @param bound Upper bound (exclusive) on the random number to be returned.
+     * @return a random {@code long} value between {@code origin} (inclusive) and
+     * {@code bound} (exclusive).
+     * @throws IllegalArgumentException if {@code origin} is greater than or equal to
+     * {@code bound}.
+     * @since 1.5
+     */
+    default long nextLong(long origin, long bound) {
+        UniformRandomProviderSupport.validateRange(origin, bound);
+        return UniformRandomProviderSupport.nextLong(this, origin, bound);
+    }
 
     /**
      * Generates a {@code boolean} value.
      *
      * @return the next random value.
      */
-    boolean nextBoolean();
+    default boolean nextBoolean() {
+        return nextInt() < 0;
+    }
 
     /**
-     * Generates a {@code float} value between 0 and 1.
+     * Generates a {@code float} value between 0 (inclusive) and 1 (exclusive).
      *
-     * @return the next random value between 0 and 1.
+     * @return the next random value between 0 (inclusive) and 1 (exclusive).
      */
-    float nextFloat();
+    default float nextFloat() {
+        return (nextInt() >>> 8) * 0x1.0p-24f;
+    }
 
     /**
-     * Generates a {@code double} value between 0 and 1.
+     * Generates a {@code float} value between 0 (inclusive) and the
+     * specified {@code bound} (exclusive).
      *
-     * @return the next random value between 0 and 1.
+     * @param bound Upper bound (exclusive) on the random number to be returned.
+     * @return a random {@code float} value between 0 (inclusive) and {@code bound}
+     * (exclusive).
+     * @throws IllegalArgumentException if {@code bound} is not both finite and greater than 0.
+     * @since 1.5
      */
-    double nextDouble();
+    default float nextFloat(float bound) {
+        UniformRandomProviderSupport.validateUpperBound(bound);
+        return UniformRandomProviderSupport.nextFloat(this, bound);
+    }
+
+    /**
+     * Generates a {@code float} value between the specified {@code origin} (inclusive)
+     * and the specified {@code bound} (exclusive).
+     *
+     * @param origin Lower bound on the random number to be returned.
+     * @param bound Upper bound (exclusive) on the random number to be returned.
+     * @return a random {@code float} value between {@code origin} (inclusive) and
+     * {@code bound} (exclusive).
+     * @throws IllegalArgumentException if {@code origin} is not finite, or {@code bound}
+     * is not finite, or {@code origin} is greater than or equal to {@code bound}.
+     * @since 1.5
+     */
+    default float nextFloat(float origin, float bound) {
+        UniformRandomProviderSupport.validateRange(origin, bound);
+        return UniformRandomProviderSupport.nextFloat(this, origin, bound);
+    }
+
+    /**
+     * Generates a {@code double} value between 0 (inclusive) and 1 (exclusive).
+     *
+     * @return the next random value between 0 (inclusive) and 1 (exclusive).
+     */
+    default double nextDouble() {
+        return (nextLong() >>> 11) * 0x1.0p-53;
+    }
+
+    /**
+     * Generates a {@code double} value between 0 (inclusive) and the
+     * specified {@code bound} (exclusive).
+     *
+     * @param bound Upper bound (exclusive) on the random number to be returned.
+     * @return a random {@code double} value between 0 (inclusive) and {@code bound}
+     * (exclusive).
+     * @throws IllegalArgumentException if {@code bound} is not both finite and greater than 0.
+     * @since 1.5
+     */
+    default double nextDouble(double bound) {
+        UniformRandomProviderSupport.validateUpperBound(bound);
+        return UniformRandomProviderSupport.nextDouble(this, bound);
+    }
+
+    /**
+     * Generates a {@code double} value between the specified {@code origin} (inclusive)
+     * and the specified {@code bound} (exclusive).
+     *
+     * @param origin Lower bound on the random number to be returned.
+     * @param bound Upper bound (exclusive) on the random number to be returned.
+     * @return a random {@code double} value between {@code origin} (inclusive) and
+     * {@code bound} (exclusive).
+     * @throws IllegalArgumentException if {@code origin} is not finite, or {@code bound}
+     * is not finite, or {@code origin} is greater than or equal to {@code bound}.
+     * @since 1.5
+     */
+    default double nextDouble(double origin, double bound) {
+        UniformRandomProviderSupport.validateRange(origin, bound);
+        return UniformRandomProviderSupport.nextDouble(this, origin, bound);
+    }
+
+    /**
+     * Returns an effectively unlimited stream of {@code int} values.
+     *
+     * @return a stream of random {@code int} values.
+     * @since 1.5
+     */
+    default IntStream ints() {
+        return IntStream.generate(this::nextInt).sequential();
+    }
+
+    /**
+     * Returns an effectively unlimited stream of {@code int} values between the specified
+     * {@code origin} (inclusive) and the specified {@code bound} (exclusive).
+     *
+     * @param origin Lower bound on the random number to be returned.
+     * @param bound Upper bound (exclusive) on the random number to be returned.
+     * @return a stream of random values between the specified {@code origin} (inclusive)
+     * and the specified {@code bound} (exclusive).
+     * @throws IllegalArgumentException if {@code origin} is greater than or equal to
+     * {@code bound}.
+     * @since 1.5
+     */
+    default IntStream ints(int origin, int bound) {
+        UniformRandomProviderSupport.validateRange(origin, bound);
+        return IntStream.generate(() -> nextInt(origin, bound)).sequential();
+    }
+
+    /**
+     * Returns a stream producing the given {@code streamSize} number of {@code int}
+     * values.
+     *
+     * @param streamSize Number of values to generate.
+     * @return a stream of random {@code int} values. the stream is limited to the given
+     * {@code streamSize}.
+     * @throws IllegalArgumentException if {@code streamSize} is negative.
+     * @since 1.5
+     */
+    default IntStream ints(long streamSize) {
+        UniformRandomProviderSupport.validateStreamSize(streamSize);
+        return ints().limit(streamSize);
+    }
+
+    /**
+     * Returns a stream producing the given {@code streamSize} number of {@code int}
+     * values between the specified {@code origin} (inclusive) and the specified
+     * {@code bound} (exclusive).
+     *
+     * @param streamSize Number of values to generate.
+     * @param origin Lower bound on the random number to be returned.
+     * @param bound Upper bound (exclusive) on the random number to be returned.
+     * @return a stream of random values between the specified {@code origin} (inclusive)
+     * and the specified {@code bound} (exclusive); the stream is limited to the given
+     * {@code streamSize}.
+     * @throws IllegalArgumentException if {@code streamSize} is negative, or if
+     * {@code origin} is greater than or equal to {@code bound}.
+     * @since 1.5
+     */
+    default IntStream ints(long streamSize, int origin, int bound) {
+        UniformRandomProviderSupport.validateStreamSize(streamSize);
+        UniformRandomProviderSupport.validateRange(origin, bound);
+        return ints(origin, bound).limit(streamSize);
+    }
+
+    /**
+     * Returns an effectively unlimited stream of {@code long} values.
+     *
+     * @return a stream of random {@code long} values.
+     * @since 1.5
+     */
+    default LongStream longs() {
+        return LongStream.generate(this::nextLong).sequential();
+    }
+
+    /**
+     * Returns an effectively unlimited stream of {@code long} values between the
+     * specified {@code origin} (inclusive) and the specified {@code bound} (exclusive).
+     *
+     * @param origin Lower bound on the random number to be returned.
+     * @param bound Upper bound (exclusive) on the random number to be returned.
+     * @return a stream of random values between the specified {@code origin} (inclusive)
+     * and the specified {@code bound} (exclusive).
+     * @throws IllegalArgumentException if {@code origin} is greater than or equal to
+     * {@code bound}.
+     * @since 1.5
+     */
+    default LongStream longs(long origin, long bound) {
+        UniformRandomProviderSupport.validateRange(origin, bound);
+        return LongStream.generate(() -> nextLong(origin, bound)).sequential();
+    }
+
+    /**
+     * Returns a stream producing the given {@code streamSize} number of {@code long}
+     * values.
+     *
+     * @param streamSize Number of values to generate.
+     * @return a stream of random {@code long} values. the stream is limited to the given
+     * {@code streamSize}.
+     * @throws IllegalArgumentException if {@code streamSize} is negative.
+     * @since 1.5
+     */
+    default LongStream longs(long streamSize) {
+        UniformRandomProviderSupport.validateStreamSize(streamSize);
+        return longs().limit(streamSize);
+    }
+
+    /**
+     * Returns a stream producing the given {@code streamSize} number of {@code long}
+     * values between the specified {@code origin} (inclusive) and the specified
+     * {@code bound} (exclusive).
+     *
+     * @param streamSize Number of values to generate.
+     * @param origin Lower bound on the random number to be returned.
+     * @param bound Upper bound (exclusive) on the random number to be returned.
+     * @return a stream of random values between the specified {@code origin} (inclusive)
+     * and the specified {@code bound} (exclusive); the stream is limited to the given
+     * {@code streamSize}.
+     * @throws IllegalArgumentException if {@code streamSize} is negative, or if
+     * {@code origin} is greater than or equal to {@code bound}.
+     * @since 1.5
+     */
+    default LongStream longs(long streamSize, long origin, long bound) {
+        UniformRandomProviderSupport.validateStreamSize(streamSize);
+        UniformRandomProviderSupport.validateRange(origin, bound);
+        return longs(origin, bound).limit(streamSize);
+    }
+
+    /**
+     * Returns an effectively unlimited stream of {@code double} values between 0
+     * (inclusive) and 1 (exclusive).
+     *
+     * @return a stream of random values between 0 (inclusive) and 1 (exclusive).
+     * @since 1.5
+     */
+    default DoubleStream doubles() {
+        return DoubleStream.generate(this::nextDouble).sequential();
+    }
+
+    /**
+     * Returns an effectively unlimited stream of {@code double} values between the
+     * specified {@code origin} (inclusive) and the specified {@code bound} (exclusive).
+     *
+     * @param origin Lower bound on the random number to be returned.
+     * @param bound Upper bound (exclusive) on the random number to be returned.
+     * @return a stream of random values between the specified {@code origin} (inclusive)
+     * and the specified {@code bound} (exclusive).
+     * @throws IllegalArgumentException if {@code origin} is not finite, or {@code bound}
+     * is not finite, or {@code origin} is greater than or equal to {@code bound}.
+     * @since 1.5
+     */
+    default DoubleStream doubles(double origin, double bound) {
+        UniformRandomProviderSupport.validateRange(origin, bound);
+        return DoubleStream.generate(() -> nextDouble(origin, bound)).sequential();
+    }
+
+    /**
+     * Returns a stream producing the given {@code streamSize} number of {@code double}
+     * values between 0 (inclusive) and 1 (exclusive).
+     *
+     * @param streamSize Number of values to generate.
+     * @return a stream of random values between 0 (inclusive) and 1 (exclusive).
+     * @throws IllegalArgumentException if {@code streamSize} is negative.
+     * @since 1.5
+     */
+    default DoubleStream doubles(long streamSize) {
+        UniformRandomProviderSupport.validateStreamSize(streamSize);
+        return doubles().limit(streamSize);
+    }
+
+    /**
+     * Returns a stream producing the given {@code streamSize} number of {@code double}
+     * values between the specified {@code origin} (inclusive) and the specified
+     * {@code bound} (exclusive).
+     *
+     * @param streamSize Number of values to generate.
+     * @param origin Lower bound on the random number to be returned.
+     * @param bound Upper bound (exclusive) on the random number to be returned.
+     * @return a stream of random values between the specified {@code origin} (inclusive)
+     * and the specified {@code bound} (exclusive); the stream is limited to the given
+     * {@code streamSize}.
+     * @throws IllegalArgumentException if {@code streamSize} is negative, or if
+     * {@code origin} is not finite, or {@code bound} is not finite, or {@code origin} is
+     * greater than or equal to {@code bound}.
+     * @since 1.5
+     */
+    default DoubleStream doubles(long streamSize, double origin, double bound) {
+        UniformRandomProviderSupport.validateStreamSize(streamSize);
+        UniformRandomProviderSupport.validateRange(origin, bound);
+        return doubles(origin, bound).limit(streamSize);
+    }
 }
diff --git a/commons-rng-client-api/src/main/java/org/apache/commons/rng/UniformRandomProviderSupport.java b/commons-rng-client-api/src/main/java/org/apache/commons/rng/UniformRandomProviderSupport.java
new file mode 100644
index 0000000..b798103
--- /dev/null
+++ b/commons-rng-client-api/src/main/java/org/apache/commons/rng/UniformRandomProviderSupport.java
@@ -0,0 +1,423 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.rng;
+
+/**
+ * Support for {@link UniformRandomProvider} default methods.
+ *
+ * @since 1.5
+ */
+final class UniformRandomProviderSupport {
+    /** Message for an invalid stream size. */
+    private static final String INVALID_STREAM_SIZE = "Invalid stream size: ";
+    /** Message for an invalid upper bound (must be positive, finite and above zero). */
+    private static final String INVALID_UPPER_BOUND = "Upper bound must be above zero: ";
+    /** Message format for an invalid range for lower inclusive and upper exclusive. */
+    private static final String INVALID_RANGE = "Invalid range: [%s, %s)";
+    /** 2^32. */
+    private static final long POW_32 = 1L << 32;
+
+    /** No instances. */
+    private UniformRandomProviderSupport() {}
+
+    /**
+     * Validate the stream size.
+     *
+     * @param size Stream size.
+     * @throws IllegalArgumentException if {@code size} is negative.
+     */
+    static void validateStreamSize(long size) {
+        if (size < 0) {
+            throw new IllegalArgumentException(INVALID_STREAM_SIZE + size);
+        }
+    }
+
+    /**
+     * Validate the upper bound.
+     *
+     * @param bound Upper bound (exclusive) on the random number to be returned.
+     * @throws IllegalArgumentException if {@code bound} is equal to or less than zero.
+     */
+    static void validateUpperBound(int bound) {
+        if (bound <= 0) {
+            throw new IllegalArgumentException(INVALID_UPPER_BOUND + bound);
+        }
+    }
+
+    /**
+     * Validate the upper bound.
+     *
+     * @param bound Upper bound (exclusive) on the random number to be returned.
+     * @throws IllegalArgumentException if {@code bound} is equal to or less than zero.
+     */
+    static void validateUpperBound(long bound) {
+        if (bound <= 0) {
+            throw new IllegalArgumentException(INVALID_UPPER_BOUND + bound);
+        }
+    }
+
+    /**
+     * Validate the upper bound.
+     *
+     * @param bound Upper bound (exclusive) on the random number to be returned.
+     * @throws IllegalArgumentException if {@code bound} is equal to or less than zero, or
+     * is not finite
+     */
+    static void validateUpperBound(float bound) {
+        // Negation of logic will detect NaN
+        if (!(bound > 0 && bound <= Float.MAX_VALUE)) {
+            throw new IllegalArgumentException(INVALID_UPPER_BOUND + bound);
+        }
+    }
+
+    /**
+     * Validate the upper bound.
+     *
+     * @param bound Upper bound (exclusive) on the random number to be returned.
+     * @throws IllegalArgumentException if {@code bound} is equal to or less than zero, or
+     * is not finite
+     */
+    static void validateUpperBound(double bound) {
+        // Negation of logic will detect NaN
+        if (!(bound > 0 && bound <= Double.MAX_VALUE)) {
+            throw new IllegalArgumentException(INVALID_UPPER_BOUND + bound);
+        }
+    }
+
+    /**
+     * Validate the range between the specified {@code origin} (inclusive) and the
+     * specified {@code bound} (exclusive).
+     *
+     * @param origin Lower bound on the random number to be returned.
+     * @param bound Upper bound (exclusive) on the random number to be returned.
+     * @throws IllegalArgumentException if {@code origin} is greater than or equal to
+     * {@code bound}.
+     */
+    static void validateRange(int origin, int bound) {
+        if (origin >= bound) {
+            throw new IllegalArgumentException(String.format(INVALID_RANGE, origin, bound));
+        }
+    }
+
+    /**
+     * Validate the range between the specified {@code origin} (inclusive) and the
+     * specified {@code bound} (exclusive).
+     *
+     * @param origin Lower bound on the random number to be returned.
+     * @param bound Upper bound (exclusive) on the random number to be returned.
+     * @throws IllegalArgumentException if {@code origin} is greater than or equal to
+     * {@code bound}.
+     */
+    static void validateRange(long origin, long bound) {
+        if (origin >= bound) {
+            throw new IllegalArgumentException(String.format(INVALID_RANGE, origin, bound));
+        }
+    }
+
+    /**
+     * Validate the range between the specified {@code origin} (inclusive) and the
+     * specified {@code bound} (exclusive).
+     *
+     * @param origin Lower bound on the random number to be returned.
+     * @param bound Upper bound (exclusive) on the random number to be returned.
+     * @throws IllegalArgumentException if {@code origin} is not finite, or {@code bound}
+     * is not finite, or {@code origin} is greater than or equal to {@code bound}.
+     */
+    static void validateRange(double origin, double bound) {
+        if (origin >= bound || !Double.isFinite(origin) || !Double.isFinite(bound)) {
+            throw new IllegalArgumentException(String.format(INVALID_RANGE, origin, bound));
+        }
+    }
+
+    /**
+     * Checks if the sub-range from fromIndex (inclusive) to fromIndex + size (exclusive) is
+     * within the bounds of range from 0 (inclusive) to length (exclusive).
+     *
+     * <p>This function provides the functionality of
+     * {@code java.utils.Objects.checkFromIndexSize} introduced in JDK 9. The
+     * <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Objects.html#checkFromIndexSize(int,int,int)">Objects</a>
+     * javadoc has been reproduced for reference.
+     *
+     * <p>The sub-range is defined to be out of bounds if any of the following inequalities
+     * is true:
+     * <ul>
+     * <li>{@code fromIndex < 0}
+     * <li>{@code size < 0}
+     * <li>{@code fromIndex + size > length}, taking into account integer overflow
+     * <li>{@code length < 0}, which is implied from the former inequalities
+     * </ul>
+     *
+     * <p>Note: This is not an exact implementation of the functionality of
+     * {@code Objects.checkFromIndexSize}. The following changes have been made:
+     * <ul>
+     * <li>The method signature has been changed to avoid the return of {@code fromIndex};
+     * this value is not used within this package.
+     * <li>No checks are made for {@code length < 0} as this is assumed to be derived from
+     * an array length.
+     * </ul>
+     *
+     * @param fromIndex the lower-bound (inclusive) of the sub-interval
+     * @param size the size of the sub-range
+     * @param length the upper-bound (exclusive) of the range
+     * @throws IndexOutOfBoundsException if the sub-range is out of bounds
+     */
+    static void validateFromIndexSize(int fromIndex, int size, int length) {
+        // check for any negatives (assume 'length' is positive array length),
+        // or overflow safe length check given the values are all positive
+        // remaining = length - fromIndex
+        if ((fromIndex | size) < 0 || size > length - fromIndex) {
+            throw new IndexOutOfBoundsException(
+                // Note: %<d is 'relative indexing' to re-use the last argument
+                String.format("Range [%d, %<d + %d) out of bounds for length %d",
+                    fromIndex, size, length));
+        }
+    }
+
+    /**
+     * Generates random bytes and places them into a user-supplied array.
+     *
+     * <p>The array is filled with bytes extracted from random {@code long} values. This
+     * implies that the number of random bytes generated may be larger than the length of
+     * the byte array.
+     *
+     * @param source Source of randomness.
+     * @param bytes Array in which to put the generated bytes. Cannot be null.
+     * @param start Index at which to start inserting the generated bytes.
+     * @param len Number of bytes to insert.
+     */
+    static void nextBytes(UniformRandomProvider source,
+                          byte[] bytes, int start, int len) {
+        // Index of first insertion plus multiple of 8 part of length
+        // (i.e. length with 3 least significant bits unset).
+        final int indexLoopLimit = start + (len & 0x7ffffff8);
+
+        // Start filling in the byte array, 8 bytes at a time.
+        int index = start;
+        while (index < indexLoopLimit) {
+            final long random = source.nextLong();
+            bytes[index++] = (byte) random;
+            bytes[index++] = (byte) (random >>> 8);
+            bytes[index++] = (byte) (random >>> 16);
+            bytes[index++] = (byte) (random >>> 24);
+            bytes[index++] = (byte) (random >>> 32);
+            bytes[index++] = (byte) (random >>> 40);
+            bytes[index++] = (byte) (random >>> 48);
+            bytes[index++] = (byte) (random >>> 56);
+        }
+
+        // Index of last insertion + 1
+        final int indexLimit = start + len;
+
+        // Fill in the remaining bytes.
+        if (index < indexLimit) {
+            long random = source.nextLong();
+            for (;;) {
+                bytes[index++] = (byte) random;
+                if (index == indexLimit) {
+                    break;
+                }
+                random >>>= 8;
+            }
+        }
+    }
+
+    /**
+     * Generates an {@code int} value between 0 (inclusive) and the specified value
+     * (exclusive).
+     *
+     * @param source Source of randomness.
+     * @param n Bound on the random number to be returned. Must be strictly positive.
+     * @return a random {@code int} value between 0 (inclusive) and {@code n} (exclusive).
+     */
+    static int nextInt(UniformRandomProvider source,
+                       int n) {
+        // Lemire (2019): Fast Random Integer Generation in an Interval
+        // https://arxiv.org/abs/1805.10941
+        long m = (source.nextInt() & 0xffffffffL) * n;
+        long l = m & 0xffffffffL;
+        if (l < n) {
+            // 2^32 % n
+            final long t = POW_32 % n;
+            while (l < t) {
+                m = (source.nextInt() & 0xffffffffL) * n;
+                l = m & 0xffffffffL;
+            }
+        }
+        return (int) (m >>> 32);
+    }
+
+    /**
+     * Generates an {@code int} value between the specified {@code origin} (inclusive) and
+     * the specified {@code bound} (exclusive).
+     *
+     * @param source Source of randomness.
+     * @param origin Lower bound on the random number to be returned.
+     * @param bound Upper bound (exclusive) on the random number to be returned. Must be
+     * above {@code origin}.
+     * @return a random {@code int} value between {@code origin} (inclusive) and
+     * {@code bound} (exclusive).
+     */
+    static int nextInt(UniformRandomProvider source,
+                       int origin, int bound) {
+        final int n = bound - origin;
+        if (n > 0) {
+            return nextInt(source, n) + origin;
+        }
+        // Range too large to fit in a positive integer.
+        // Use simple rejection.
+        int v = source.nextInt();
+        while (v < origin || v >= bound) {
+            v = source.nextInt();
+        }
+        return v;
+    }
+
+    /**
+     * Generates an {@code long} value between 0 (inclusive) and the specified value
+     * (exclusive).
+     *
+     * @param source Source of randomness.
+     * @param n Bound on the random number to be returned. Must be strictly positive.
+     * @return a random {@code long} value between 0 (inclusive) and {@code n}
+     * (exclusive).
+     */
+    static long nextLong(UniformRandomProvider source,
+                         long n) {
+        long bits;
+        long val;
+        do {
+            bits = source.nextLong() >>> 1;
+            val  = bits % n;
+        } while (bits - val + (n - 1) < 0);
+
+        return val;
+    }
+
+    /**
+     * Generates a {@code long} value between the specified {@code origin} (inclusive) and
+     * the specified {@code bound} (exclusive).
+     *
+     * @param source Source of randomness.
+     * @param origin Lower bound on the random number to be returned.
+     * @param bound Upper bound (exclusive) on the random number to be returned. Must be
+     * above {@code origin}.
+     * @return a random {@code long} value between {@code origin} (inclusive) and
+     * {@code bound} (exclusive).
+     */
+    static long nextLong(UniformRandomProvider source,
+                         long origin, long bound) {
+        final long n = bound - origin;
+        if (n > 0) {
+            return nextLong(source, n) + origin;
+        }
+        // Range too large to fit in a positive integer.
+        // Use simple rejection.
+        long v = source.nextLong();
+        while (v < origin || v >= bound) {
+            v = source.nextLong();
+        }
+        return v;
+    }
+
+    /**
+     * Generates a {@code float} value between 0 (inclusive) and the specified value
+     * (exclusive).
+     *
+     * @param source Source of randomness.
+     * @param bound Bound on the random number to be returned. Must be strictly positive.
+     * @return a random {@code float} value between 0 (inclusive) and {@code bound}
+     * (exclusive).
+     */
+    static float nextFloat(UniformRandomProvider source,
+                           float bound) {
+        float v = source.nextFloat() * bound;
+        if (v >= bound) {
+            // Correct rounding
+            v = Math.nextDown(bound);
+        }
+        return v;
+    }
+
+    /**
+     * Generates a {@code float} value between the specified {@code origin} (inclusive)
+     * and the specified {@code bound} (exclusive).
+     *
+     * @param source Source of randomness.
+     * @param origin Lower bound on the random number to be returned. Must be finite.
+     * @param bound Upper bound (exclusive) on the random number to be returned. Must be
+     * above {@code origin} and finite.
+     * @return a random {@code float} value between {@code origin} (inclusive) and
+     * {@code bound} (exclusive).
+     */
+    static float nextFloat(UniformRandomProvider source,
+                           float origin, float bound) {
+        float v = source.nextFloat();
+        // This expression allows (bound - origin) to be infinite
+        // origin + (bound - origin) * v
+        // == origin - origin * v + bound * v
+        v = (1f - v) * origin + v * bound;
+        if (v >= bound) {
+            // Correct rounding
+            v = Math.nextDown(bound);
+        }
+        return v;
+    }
+
+    /**
+     * Generates a {@code double} value between 0 (inclusive) and the specified value
+     * (exclusive).
+     *
+     * @param source Source of randomness.
+     * @param bound Bound on the random number to be returned. Must be strictly positive.
+     * @return a random {@code double} value between 0 (inclusive) and {@code bound}
+     * (exclusive).
+     */
+    static double nextDouble(UniformRandomProvider source,
+                             double bound) {
+        double v = source.nextDouble() * bound;
+        if (v >= bound) {
+            // Correct rounding
+            v = Math.nextDown(bound);
+        }
+        return v;
+    }
+
+    /**
+     * Generates a {@code double} value between the specified {@code origin} (inclusive)
+     * and the specified {@code bound} (exclusive).
+     *
+     * @param source Source of randomness.
+     * @param origin Lower bound on the random number to be returned. Must be finite.
+     * @param bound Upper bound (exclusive) on the random number to be returned. Must be
+     * above {@code origin} and finite.
+     * @return a random {@code double} value between {@code origin} (inclusive) and
+     * {@code bound} (exclusive).
+     */
+    static double nextDouble(UniformRandomProvider source,
+                             double origin, double bound) {
+        double v = source.nextDouble();
+        // This expression allows (bound - origin) to be infinite
+        // origin + (bound - origin) * v
+        // == origin - origin * v + bound * v
+        v = (1f - v) * origin + v * bound;
+        if (v >= bound) {
+            // Correct rounding
+            v = Math.nextDown(bound);
+        }
+        return v;
+    }
+}
diff --git a/commons-rng-client-api/src/test/java/org/apache/commons/rng/UniformRandomProviderTest.java b/commons-rng-client-api/src/test/java/org/apache/commons/rng/UniformRandomProviderTest.java
new file mode 100644
index 0000000..0cd7076
--- /dev/null
+++ b/commons-rng-client-api/src/test/java/org/apache/commons/rng/UniformRandomProviderTest.java
@@ -0,0 +1,1114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.rng;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.SplittableRandom;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.function.LongSupplier;
+import java.util.stream.Collectors;
+import java.util.stream.DoubleStream;
+import java.util.stream.IntStream;
+import java.util.stream.LongStream;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * Tests for default method implementations in {@link UniformRandomProvider}.
+ *
+ * <p>This class verifies all exception conditions for the range methods and
+ * the range arguments to the stream methods. Streams methods are asserted
+ * to call the corresponding single value generation method in the interface.
+ * Single value generation methods are asserted using a test of uniformity
+ * from multiple samples.
+ */
+class UniformRandomProviderTest {
+    private static final long STREAM_SIZE_ONE = 1;
+    /** Sample size for statistical uniformity tests. */
+    private static final int SAMPLE_SIZE = 1000;
+    /** Sample size for statistical uniformity tests as a BigDecimal. */
+    private static final BigDecimal SAMPLE_SIZE_BD = BigDecimal.valueOf(SAMPLE_SIZE);
+    /** Relative error used to verify the sum of expected frequencies matches the sample size. */
+    private static final double RELATIVE_ERROR = Math.ulp(1.0) * 2;
+
+    /**
+     * Dummy class for checking the behavior of the UniformRandomProvider.
+     */
+    private static class DummyGenerator implements UniformRandomProvider {
+        /** An instance. */
+        static final DummyGenerator INSTANCE = new DummyGenerator();
+
+        @Override
+        public long nextLong() {
+            throw new UnsupportedOperationException("The nextLong method should not be invoked");
+        }
+    }
+
+    static int[] invalidNextIntBound() {
+        return new int[] {0, -1, Integer.MIN_VALUE};
+    }
+
+    static Stream<Arguments> invalidNextIntOriginBound() {
+        return Stream.of(
+            Arguments.of(1, 1),
+            Arguments.of(2, 1),
+            Arguments.of(-1, -1),
+            Arguments.of(-1, -2)
+        );
+    }
+
+    static long[] invalidNextLongBound() {
+        return new long[] {0, -1, Long.MIN_VALUE};
+    }
+
+    static Stream<Arguments> invalidNextLongOriginBound() {
+        return Stream.of(
+            Arguments.of(1L, 1L),
+            Arguments.of(2L, 1L),
+            Arguments.of(-1L, -1L),
+            Arguments.of(-1L, -2L)
+        );
+    }
+
+    static float[] invalidNextFloatBound() {
+        return new float[] {0, -1, -Float.MAX_VALUE, Float.NaN, Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY};
+    }
+
+    static Stream<Arguments> invalidNextFloatOriginBound() {
+        return Stream.of(
+            Arguments.of(1f, 1f),
+            Arguments.of(2f, 1f),
+            Arguments.of(-1f, -1f),
+            Arguments.of(-1f, -2f),
+            Arguments.of(Float.NEGATIVE_INFINITY, 0f),
+            Arguments.of(0f, Float.POSITIVE_INFINITY),
+            Arguments.of(0f, Float.NaN),
+            Arguments.of(Float.NaN, 1f)
+        );
+    }
+
+    static double[] invalidNextDoubleBound() {
+        return new double[] {0, -1, -Double.MAX_VALUE, Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY};
+    }
+
+    static Stream<Arguments> invalidNextDoubleOriginBound() {
+        return Stream.of(
+            Arguments.of(1, 1),
+            Arguments.of(2, 1),
+            Arguments.of(-1, -1),
+            Arguments.of(-1, -2),
+            Arguments.of(Double.NEGATIVE_INFINITY, 0),
+            Arguments.of(0, Double.POSITIVE_INFINITY),
+            Arguments.of(0, Double.NaN),
+            Arguments.of(Double.NaN, 1)
+        );
+    }
+
+    static long[] streamSizes() {
+        return new long[] {0, 1, 13};
+    }
+
+    /**
+     * Creates a functional random generator by implementing the
+     * {@link UniformRandomProvider#nextLong} method with a high quality source of randomness.
+     *
+     * @param seed Seed for the generator.
+     * @return the random generator
+     */
+    private static UniformRandomProvider createRNG(long seed) {
+        // The algorithm for SplittableRandom with the default increment passes:
+        // - Test U01 BigCrush
+        // - PractRand with at least 2^42 bytes (4 TiB) of output
+        return new SplittableRandom(seed)::nextLong;
+    }
+
+    @Test
+    void testNextBytesThrows() {
+        final UniformRandomProvider rng = DummyGenerator.INSTANCE;
+        Assertions.assertThrows(NullPointerException.class, () -> rng.nextBytes(null));
+        Assertions.assertThrows(NullPointerException.class, () -> rng.nextBytes(null, 0, 1));
+        // Invalid range
+        final int length = 10;
+        final byte[] bytes = new byte[length];
+        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> rng.nextBytes(bytes, -1, 1), "start < 0");
+        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> rng.nextBytes(bytes, length, 1), "start >= length");
+        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> rng.nextBytes(bytes, 0, -1), "len < 0");
+        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> rng.nextBytes(bytes, 5, 10), "start + len > length");
+        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> rng.nextBytes(bytes, 5, Integer.MAX_VALUE), "start + len > length, taking into account integer overflow");
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"invalidNextIntBound"})
+    void testNextIntBoundThrows(int bound) {
+        final UniformRandomProvider rng = DummyGenerator.INSTANCE;
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.nextInt(bound));
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"invalidNextIntOriginBound"})
+    void testNextIntOriginBoundThrows(int origin, int bound) {
+        final UniformRandomProvider rng = DummyGenerator.INSTANCE;
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.nextInt(origin, bound));
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"invalidNextLongBound"})
+    void testNextLongBoundThrows(long bound) {
+        final UniformRandomProvider rng = DummyGenerator.INSTANCE;
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.nextLong(bound));
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"invalidNextLongOriginBound"})
+    void testNextLongOriginBoundThrows(long origin, long bound) {
+        final UniformRandomProvider rng = DummyGenerator.INSTANCE;
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.nextLong(origin, bound));
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"invalidNextFloatBound"})
+    void testNextFloatBoundThrows(float bound) {
+        final UniformRandomProvider rng = DummyGenerator.INSTANCE;
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.nextFloat(bound));
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"invalidNextFloatOriginBound"})
+    void testNextFloatOriginBoundThrows(float origin, float bound) {
+        final UniformRandomProvider rng = DummyGenerator.INSTANCE;
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.nextFloat(origin, bound));
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"invalidNextDoubleBound"})
+    void testNextDoubleBoundThrows(double bound) {
+        final UniformRandomProvider rng = DummyGenerator.INSTANCE;
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.nextDouble(bound));
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"invalidNextDoubleOriginBound"})
+    void testNextDoubleOriginBoundThrows(double origin, double bound) {
+        final UniformRandomProvider rng = DummyGenerator.INSTANCE;
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.nextDouble(origin, bound));
+    }
+
+    @Test
+    void testNextFloatExtremes() {
+        final UniformRandomProvider rng = new DummyGenerator() {
+            private int i;
+            private int[] values = {0, -1, 1 << 8};
+            @Override
+            public int nextInt() {
+                return values[i++];
+            }
+        };
+        Assertions.assertEquals(0, rng.nextFloat(), "Expected zero bits to return 0");
+        Assertions.assertEquals(Math.nextDown(1f), rng.nextFloat(), "Expected all bits to return ~1");
+        // Assumes the value is created from the high 24 bits of nextInt
+        Assertions.assertEquals(1f - Math.nextDown(1f), rng.nextFloat(), "Expected 1 bit (shifted) to return ~0");
+    }
+
+    @ParameterizedTest
+    @ValueSource(floats = {Float.MIN_NORMAL, Float.MIN_NORMAL / 2})
+    void testNextFloatBoundRounding(float bound) {
+        // This method will be used
+        final UniformRandomProvider rng = new DummyGenerator() {
+            @Override
+            public float nextFloat() {
+                return Math.nextDown(1.0f);
+            }
+        };
+        Assertions.assertEquals(bound, rng.nextFloat() * bound, "Expected result to be rounded up");
+        Assertions.assertEquals(Math.nextDown(bound), rng.nextFloat(bound), "Result was not correctly rounded down");
+    }
+
+    @Test
+    void testNextFloatOriginBoundInfiniteRange() {
+        // This method will be used
+        final UniformRandomProvider rng = new DummyGenerator() {
+            private int i;
+            private float[] values = {0, 0.25f, 0.5f, 0.75f, 1};
+            @Override
+            public float nextFloat() {
+                return values[i++];
+            }
+        };
+        final float x = Float.MAX_VALUE;
+        Assertions.assertEquals(-x, rng.nextFloat(-x, x));
+        Assertions.assertEquals(-x / 2, rng.nextFloat(-x, x), Math.ulp(x / 2));
+        Assertions.assertEquals(0, rng.nextFloat(-x, x));
+        Assertions.assertEquals(x / 2, rng.nextFloat(-x, x), Math.ulp(x / 2));
+        Assertions.assertEquals(Math.nextDown(x), rng.nextFloat(-x, x));
+    }
+
+    @Test
+    void testNextFloatOriginBoundRounding() {
+        // This method will be used
+        final float v = Math.nextDown(1.0f);
+        final UniformRandomProvider rng = new DummyGenerator() {
+            @Override
+            public float nextFloat() {
+                return v;
+            }
+        };
+        float origin;
+        float bound;
+
+        // Simple case
+        origin = 3.5f;
+        bound = 4.5f;
+        Assertions.assertEquals(bound, origin + v * (bound - origin), "Expected result to be rounded up");
+        Assertions.assertEquals(Math.nextDown(bound), rng.nextFloat(origin, bound), "Result was not correctly rounded down");
+
+        // Alternate formula:
+        // requires sub-normal result for 'origin * v' to force rounding
+        origin = -Float.MIN_NORMAL / 2;
+        bound = Float.MIN_NORMAL / 2;
+        Assertions.assertEquals(bound, origin * (1 - v) + v * bound, "Expected result to be rounded up");
+        Assertions.assertEquals(Math.nextDown(bound), rng.nextFloat(origin, bound), "Result was not correctly rounded down");
+    }
+
+    @Test
+    void testNextDoubleExtremes() {
+        final UniformRandomProvider rng = new DummyGenerator() {
+            private int i;
+            private long[] values = {0, -1, 1L << 11};
+            @Override
+            public long nextLong() {
+                return values[i++];
+            }
+        };
+        Assertions.assertEquals(0, rng.nextDouble(), "Expected zero bits to return 0");
+        Assertions.assertEquals(Math.nextDown(1.0), rng.nextDouble(), "Expected all bits to return ~1");
+        // Assumes the value is created from the high 53 bits of nextInt
+        Assertions.assertEquals(1.0 - Math.nextDown(1.0), rng.nextDouble(), "Expected 1 bit (shifted) to return ~0");
+    }
+
+    @ParameterizedTest
+    @ValueSource(doubles = {Double.MIN_NORMAL, Double.MIN_NORMAL / 2})
+    void testNextDoubleBoundRounding(double bound) {
+        // This method will be used
+        final UniformRandomProvider rng = new DummyGenerator() {
+            @Override
+            public double nextDouble() {
+                return Math.nextDown(1.0);
+            }
+        };
+        Assertions.assertEquals(bound, rng.nextDouble() * bound, "Expected result to be rounded up");
+        Assertions.assertEquals(Math.nextDown(bound), rng.nextDouble(bound), "Result was not correctly rounded down");
+    }
+
+    @Test
+    void testNextDoubleOriginBoundInfiniteRange() {
+        // This method will be used
+        final UniformRandomProvider rng = new DummyGenerator() {
+            private int i;
+            private double[] values = {0, 0.25, 0.5, 0.75, 1};
+            @Override
+            public double nextDouble() {
+                return values[i++];
+            }
+        };
+        final double x = Double.MAX_VALUE;
+        Assertions.assertEquals(-x, rng.nextDouble(-x, x));
+        Assertions.assertEquals(-x / 2, rng.nextDouble(-x, x), Math.ulp(x / 2));
+        Assertions.assertEquals(0, rng.nextDouble(-x, x));
+        Assertions.assertEquals(x / 2, rng.nextDouble(-x, x), Math.ulp(x / 2));
+        Assertions.assertEquals(Math.nextDown(x), rng.nextDouble(-x, x));
+    }
+
+    @Test
+    void testNextDoubleOriginBoundRounding() {
+        // This method will be used
+        final double v = Math.nextDown(1.0);
+        final UniformRandomProvider rng = new DummyGenerator() {
+            @Override
+            public double nextDouble() {
+                return v;
+            }
+        };
+        double origin;
+        double bound;
+
+        // Simple case
+        origin = 3.5;
+        bound = 4.5;
+        Assertions.assertEquals(bound, origin + v * (bound - origin), "Expected result to be rounded up");
+        Assertions.assertEquals(Math.nextDown(bound), rng.nextDouble(origin, bound), "Result was not correctly rounded down");
+
+        // Alternate formula:
+        // requires sub-normal result for 'origin * v' to force rounding
+        origin = -Double.MIN_NORMAL / 2;
+        bound = Double.MIN_NORMAL / 2;
+        Assertions.assertEquals(bound, origin * (1 - v) + v * bound, "Expected result to be rounded up");
+        Assertions.assertEquals(Math.nextDown(bound), rng.nextDouble(origin, bound), "Result was not correctly rounded down");
+    }
+
+    @Test
+    void testNextBooleanIsIntSignBit() {
+        final int size = 10;
+        final int[] values = ThreadLocalRandom.current().ints(size).toArray();
+        final UniformRandomProvider rng = new DummyGenerator() {
+            private int i;
+            @Override
+            public int nextInt() {
+                return values[i++];
+            }
+        };
+        for (int i = 0; i < size; i++) {
+            Assertions.assertEquals(values[i] < 0, rng.nextBoolean());
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(longs = {-1, -2, Long.MIN_VALUE})
+    void testInvalidStreamSizeThrows(long size) {
+        final UniformRandomProvider rng = DummyGenerator.INSTANCE;
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.ints(size), "ints");
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.ints(size, 1, 42), "ints(lower, upper)");
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.longs(size), "longs");
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.longs(size, 3L, 33L), "longs(lower, upper)");
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.doubles(size), "doubles");
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.doubles(size, 1.5, 2.75), "doubles(lower, upper)");
+    }
+
+    @Test
+    void testUnlimitedStreamSize() {
+        final UniformRandomProvider rng = DummyGenerator.INSTANCE;
+        Assertions.assertEquals(Long.MAX_VALUE, rng.ints().spliterator().estimateSize(), "ints");
+        Assertions.assertEquals(Long.MAX_VALUE, rng.ints(1, 42).spliterator().estimateSize(), "ints(lower, upper)");
+        Assertions.assertEquals(Long.MAX_VALUE, rng.longs().spliterator().estimateSize(), "longs");
+        Assertions.assertEquals(Long.MAX_VALUE, rng.longs(3L, 33L).spliterator().estimateSize(), "longs(lower, upper)");
+        Assertions.assertEquals(Long.MAX_VALUE, rng.doubles().spliterator().estimateSize(), "doubles");
+        Assertions.assertEquals(Long.MAX_VALUE, rng.doubles(1.5, 2.75).spliterator().estimateSize(), "doubles(lower, upper)");
+    }
+
+    // Test stream methods throw immediately for invalid range arguments.
+
+    @ParameterizedTest
+    @MethodSource(value = {"invalidNextIntOriginBound"})
+    void testIntsOriginBoundThrows(int origin, int bound) {
+        final UniformRandomProvider rng = DummyGenerator.INSTANCE;
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.ints(origin, bound));
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.ints(STREAM_SIZE_ONE, origin, bound));
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"invalidNextLongOriginBound"})
+    void testLongsOriginBoundThrows(long origin, long bound) {
+        final UniformRandomProvider rng = DummyGenerator.INSTANCE;
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.longs(origin, bound));
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.longs(STREAM_SIZE_ONE, origin, bound));
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"invalidNextDoubleOriginBound"})
+    void testDoublesOriginBoundThrows(double origin, double bound) {
+        final UniformRandomProvider rng = DummyGenerator.INSTANCE;
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.doubles(origin, bound));
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.doubles(STREAM_SIZE_ONE, origin, bound));
+    }
+
+    // Test stream methods call the correct generation method in the UniformRandomProvider.
+    // If range arguments are supplied they are asserted to be passed through.
+
+    @ParameterizedTest
+    @MethodSource(value = {"streamSizes"})
+    void testInts(long streamSize) {
+        final int[] values = ThreadLocalRandom.current().ints(streamSize).toArray();
+        final UniformRandomProvider rng = new DummyGenerator() {
+            private int i;
+            @Override
+            public int nextInt() {
+                return values[i++];
+            }
+        };
+
+        final IntStream stream = rng.ints();
+        Assertions.assertFalse(stream.isParallel());
+        Assertions.assertArrayEquals(values, stream.limit(streamSize).toArray());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"streamSizes"})
+    void testIntsOriginBound(long streamSize) {
+        final int origin = 13;
+        final int bound = 42;
+        final int[] values = ThreadLocalRandom.current().ints(streamSize, origin, bound).toArray();
+        final UniformRandomProvider rng = new DummyGenerator() {
+            private int i;
+            @Override
+            public int nextInt(int o, int b) {
+                Assertions.assertEquals(origin, o, "origin");
+                Assertions.assertEquals(bound, b, "bound");
+                return values[i++];
+            }
+        };
+
+        final IntStream stream = rng.ints(origin, bound);
+        Assertions.assertFalse(stream.isParallel());
+        Assertions.assertArrayEquals(values, stream.limit(streamSize).toArray());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"streamSizes"})
+    void testIntsWithSize(long streamSize) {
+        final int[] values = ThreadLocalRandom.current().ints(streamSize).toArray();
+        final UniformRandomProvider rng = new DummyGenerator() {
+            private int i;
+            @Override
+            public int nextInt() {
+                return values[i++];
+            }
+        };
+
+        final IntStream stream = rng.ints(streamSize);
+        Assertions.assertFalse(stream.isParallel());
+        Assertions.assertArrayEquals(values, stream.toArray());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"streamSizes"})
+    void testIntsOriginBoundWithSize(long streamSize) {
+        final int origin = 13;
+        final int bound = 42;
+        final int[] values = ThreadLocalRandom.current().ints(streamSize, origin, bound).toArray();
+        final UniformRandomProvider rng = new DummyGenerator() {
+            private int i;
+            @Override
+            public int nextInt(int o, int b) {
+                Assertions.assertEquals(origin, o, "origin");
+                Assertions.assertEquals(bound, b, "bound");
+                return values[i++];
+            }
+        };
+
+        final IntStream stream = rng.ints(streamSize, origin, bound);
+        Assertions.assertFalse(stream.isParallel());
+        Assertions.assertArrayEquals(values, stream.toArray());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"streamSizes"})
+    void testLongs(long streamSize) {
+        final long[] values = ThreadLocalRandom.current().longs(streamSize).toArray();
+        final UniformRandomProvider rng = new DummyGenerator() {
+            private int i;
+            @Override
+            public long nextLong() {
+                return values[i++];
+            }
+        };
+
+        final LongStream stream = rng.longs();
+        Assertions.assertFalse(stream.isParallel());
+        Assertions.assertArrayEquals(values, stream.limit(streamSize).toArray());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"streamSizes"})
+    void testLongsOriginBound(long streamSize) {
+        final long origin = 13;
+        final long bound = 42;
+        final long[] values = ThreadLocalRandom.current().longs(streamSize, origin, bound).toArray();
+        final UniformRandomProvider rng = new DummyGenerator() {
+            private int i;
+            @Override
+            public long nextLong(long o, long b) {
+                Assertions.assertEquals(origin, o, "origin");
+                Assertions.assertEquals(bound, b, "bound");
+                return values[i++];
+            }
+        };
+
+        final LongStream stream = rng.longs(origin, bound);
+        Assertions.assertFalse(stream.isParallel());
+        Assertions.assertArrayEquals(values, stream.limit(streamSize).toArray());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"streamSizes"})
+    void testLongsWithSize(long streamSize) {
+        final long[] values = ThreadLocalRandom.current().longs(streamSize).toArray();
+        final UniformRandomProvider rng = new DummyGenerator() {
+            private int i;
+            @Override
+            public long nextLong() {
+                return values[i++];
+            }
+        };
+
+        final LongStream stream = rng.longs(streamSize);
+        Assertions.assertFalse(stream.isParallel());
+        Assertions.assertArrayEquals(values, stream.toArray());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"streamSizes"})
+    void testLongsOriginBoundWithSize(long streamSize) {
+        final long origin = 13;
+        final long bound = 42;
+        final long[] values = ThreadLocalRandom.current().longs(streamSize, origin, bound).toArray();
+        final UniformRandomProvider rng = new DummyGenerator() {
+            private int i;
+            @Override
+            public long nextLong(long o, long b) {
+                Assertions.assertEquals(origin, o, "origin");
+                Assertions.assertEquals(bound, b, "bound");
+                return values[i++];
+            }
+        };
+
+        final LongStream stream = rng.longs(streamSize, origin, bound);
+        Assertions.assertFalse(stream.isParallel());
+        Assertions.assertArrayEquals(values, stream.toArray());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"streamSizes"})
+    void testDoubles(long streamSize) {
+        final double[] values = ThreadLocalRandom.current().doubles(streamSize).toArray();
+        final UniformRandomProvider rng = new DummyGenerator() {
+            private int i;
+            @Override
+            public double nextDouble() {
+                return values[i++];
+            }
+        };
+
+        final DoubleStream stream = rng.doubles();
+        Assertions.assertFalse(stream.isParallel());
+        Assertions.assertArrayEquals(values, stream.limit(streamSize).toArray());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"streamSizes"})
+    void testDoublesOriginBound(long streamSize) {
+        final double origin = 13;
+        final double bound = 42;
+        final double[] values = ThreadLocalRandom.current().doubles(streamSize, origin, bound).toArray();
+        final UniformRandomProvider rng = new DummyGenerator() {
+            private int i;
+            @Override
+            public double nextDouble(double o, double b) {
+                Assertions.assertEquals(origin, o, "origin");
+                Assertions.assertEquals(bound, b, "bound");
+                return values[i++];
+            }
+        };
+
+        final DoubleStream stream = rng.doubles(origin, bound);
+        Assertions.assertFalse(stream.isParallel());
+        Assertions.assertArrayEquals(values, stream.limit(streamSize).toArray());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"streamSizes"})
+    void testDoublesWithSize(long streamSize) {
+        final double[] values = ThreadLocalRandom.current().doubles(streamSize).toArray();
+        final UniformRandomProvider rng = new DummyGenerator() {
+            private int i;
+            @Override
+            public double nextDouble() {
+                return values[i++];
+            }
+        };
+
+        final DoubleStream stream = rng.doubles(streamSize);
+        Assertions.assertFalse(stream.isParallel());
+        Assertions.assertArrayEquals(values, stream.toArray());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"streamSizes"})
+    void testDoublesOriginBoundWithSize(long streamSize) {
+        final double origin = 13;
+        final double bound = 42;
+        final double[] values = ThreadLocalRandom.current().doubles(streamSize, origin, bound).toArray();
+        final UniformRandomProvider rng = new DummyGenerator() {
+            private int i;
+            @Override
+            public double nextDouble(double o, double b) {
+                Assertions.assertEquals(origin, o, "origin");
+                Assertions.assertEquals(bound, b, "bound");
+                return values[i++];
+            }
+        };
+
+        final DoubleStream stream = rng.doubles(streamSize, origin, bound);
+        Assertions.assertFalse(stream.isParallel());
+        Assertions.assertArrayEquals(values, stream.toArray());
+    }
+
+    // Statistical tests for uniform distribution
+
+    // Monobit tests
+
+    @ParameterizedTest
+    @ValueSource(longs = {263784628482L, -2563472, -2367482842368L})
+    void testNextIntMonobit(long seed) {
+        final UniformRandomProvider rng = createRNG(seed);
+        int bitCount = 0;
+        final int n = 100;
+        for (int i = 0; i < n; i++) {
+            bitCount += Integer.bitCount(rng.nextInt());
+        }
+        final int numberOfBits = n * Integer.SIZE;
+        assertMonobit(bitCount, numberOfBits);
+    }
+
+    @ParameterizedTest
+    @ValueSource(longs = {263784628L, 253674, -23568426834L})
+    void testNextDoubleMonobit(long seed) {
+        final UniformRandomProvider rng = createRNG(seed);
+        int bitCount = 0;
+        final int n = 100;
+        for (int i = 0; i < n; i++) {
+            bitCount += Long.bitCount((long) (rng.nextDouble() * (1L << 53)));
+        }
+        final int numberOfBits = n * 53;
+        assertMonobit(bitCount, numberOfBits);
+    }
+
+    @ParameterizedTest
+    @ValueSource(longs = {265342L, -234232, -672384648284L})
+    void testNextBooleanMonobit(long seed) {
+        final UniformRandomProvider rng = createRNG(seed);
+        int bitCount = 0;
+        final int n = 1000;
+        for (int i = 0; i < n; i++) {
+            if (rng.nextBoolean()) {
+                bitCount++;
+            }
+        }
+        final int numberOfBits = n;
+        assertMonobit(bitCount, numberOfBits);
+    }
+
+    @ParameterizedTest
+    @ValueSource(longs = {-1526731, 263846, 4545})
+    void testNextFloatMonobit(long seed) {
+        final UniformRandomProvider rng = createRNG(seed);
+        int bitCount = 0;
+        final int n = 100;
+        for (int i = 0; i < n; i++) {
+            bitCount += Integer.bitCount((int) (rng.nextFloat() * (1 << 24)));
+        }
+        final int numberOfBits = n * 24;
+        assertMonobit(bitCount, numberOfBits);
+    }
+
+    @ParameterizedTest
+    @CsvSource({
+        "-2638468223894, 16",
+        "235647674, 17",
+        "-928738475, 23",
+    })
+    void testNextBytesMonobit(long seed, int range) {
+        final UniformRandomProvider rng = createRNG(seed);
+        final byte[] bytes = new byte[range];
+        int bitCount = 0;
+        final int n = 100;
+        for (int i = 0; i < n; i++) {
+            rng.nextBytes(bytes);
+            for (final byte b1 : bytes) {
+                bitCount += Integer.bitCount(b1 & 0xff);
+            }
+        }
+        final int numberOfBits = n * Byte.SIZE * range;
+        assertMonobit(bitCount, numberOfBits);
+    }
+
+    /**
+     * Assert that the number of 1 bits is approximately 50%. This is based upon a
+     * fixed-step "random walk" of +1/-1 from zero.
+     *
+     * <p>The test is equivalent to the NIST Monobit test with a fixed p-value of
+     * 0.01. The number of bits is recommended to be above 100.</p>
+     *
+     * @see <A
+     * href="https://csrc.nist.gov/publications/detail/sp/800-22/rev-1a/final">Bassham,
+     * et al (2010) NIST SP 800-22: A Statistical Test Suite for Random and
+     * Pseudorandom Number Generators for Cryptographic Applications. Section
+     * 2.1.</a>
+     *
+     * @param bitCount The bit count.
+     * @param numberOfBits Number of bits.
+     */
+    private static void assertMonobit(int bitCount, int numberOfBits) {
+        // Convert the bit count into a number of +1/-1 steps.
+        final double sum = 2.0 * bitCount - numberOfBits;
+        // The reference distribution is Normal with a standard deviation of sqrt(n).
+        // Check the absolute position is not too far from the mean of 0 with a fixed
+        // p-value of 0.01 taken from a 2-tailed Normal distribution. Computation of
+        // the p-value requires the complimentary error function.
+        final double absSum = Math.abs(sum);
+        final double max = Math.sqrt(numberOfBits) * 2.5758293035489004;
+        Assertions.assertTrue(absSum <= max,
+            () -> "Walked too far astray: " + absSum + " > " + max +
+                  " (test will fail randomly about 1 in 100 times)");
+    }
+
+    // Uniformity tests
+
+    @ParameterizedTest
+    @CsvSource({
+        "263746283, 23, 0, 23",
+        "-126536861889, 16, 0, 16",
+        "617868181124, 1234, 567, 89",
+        "-56788, 512, 0, 233",
+        "6787535424, 512, 233, 279",
+    })
+    void testNextBytesUniform(long seed,
+                              int length, int start, int size) {
+        final UniformRandomProvider rng = createRNG(seed);
+        final byte[] buffer = new byte[length];
+
+        final Runnable nextMethod = start == 0 && size == length ?
+                () -> rng.nextBytes(buffer) :
+                () -> rng.nextBytes(buffer, start, size);
+
+        final int last = start + size;
+        Assertions.assertTrue(isUniformNextBytes(buffer, start, last, nextMethod),
+                              "Expected uniform bytes");
+
+        // The parts of the buffer where no values are put should be zero.
+        for (int i = 0; i < start; i++) {
+            Assertions.assertEquals(0, buffer[i], () -> "Filled < start: " + start);
+        }
+        for (int i = last; i < length; i++) {
+            Assertions.assertEquals(0, buffer[i], () -> "Filled >= last: " + last);
+        }
+    }
+
+    /**
+     * Checks that the generator values can be placed into 256 bins with
+     * approximately equal number of counts.
+     * Test allows to select only part of the buffer for performing the
+     * statistics.
+     *
+     * @param buffer Buffer to be filled.
+     * @param first First element (included) of {@code buffer} range for
+     * which statistics must be taken into account.
+     * @param last Last element (excluded) of {@code buffer} range for
+     * which statistics must be taken into account.
+     * @param nextMethod Method that fills the given {@code buffer}.
+     * @return {@code true} if the distribution is uniform.
+     */
+    private static boolean isUniformNextBytes(byte[] buffer,
+                                              int first,
+                                              int last,
+                                              Runnable nextMethod) {
+        final int sampleSize = 10000;
+
+        // Number of possible values (do not change).
+        final int byteRange = 256;
+        // Chi-square critical value with 255 degrees of freedom
+        // and 1% significance level.
+        final double chi2CriticalValue = 310.45738821990585;
+
+        // Bins.
+        final long[] observed = new long[byteRange];
+        final double[] expected = new double[byteRange];
+
+        Arrays.fill(expected, sampleSize * (last - first) / (double) byteRange);
+
+        for (int k = 0; k < sampleSize; k++) {
+            nextMethod.run();
+
+            for (int i = first; i < last; i++) {
+                // Convert byte to an index in [0, 255]
+                ++observed[buffer[i] & 0xff];
+            }
+        }
+
+        // Compute chi-square.
+        double chi2 = 0;
+        for (int k = 0; k < byteRange; k++) {
+            final double diff = observed[k] - expected[k];
+            chi2 += diff * diff / expected[k];
+        }
+
+        // Statistics check.
+        return chi2 <= chi2CriticalValue;
+    }
+
+    // Add range tests
+
+    @ParameterizedTest
+    @CsvSource({
+        // No lower bound
+        "2673846826, 0, 10",
+        "-23658268, 0, 12",
+        "263478624, 0, 31",
+        "1278332, 0, 32",
+        "99734765, 0, 2016128993",
+        "-63485384, 0, 1834691456",
+        "3876457638, 0, 869657561",
+        "-126784782, 0, 1570504788",
+        "2637846, 0, 2147483647",
+        // Small range
+        "2634682, 567576, 567586",
+        "-56757798989, -1000, -100",
+        "-97324785, -54656, 12",
+        "23423235, -526783468, 257",
+        // Large range
+        "-2634682, -1073741824, 1073741824",
+        "6786868132, -1263842626, 1237846372",
+        "-263846723, -368268, 2147483647",
+        "7352352, -2147483648, 61523457",
+    })
+    void testNextIntUniform(long seed, int origin, int bound) {
+        final UniformRandomProvider rng = createRNG(seed);
+        final LongSupplier nextMethod = origin == 0 ?
+                () -> rng.nextInt(bound) :
+                () -> rng.nextInt(origin, bound);
+        checkNextInRange("nextInt", origin, bound, nextMethod);
+    }
+
+    @ParameterizedTest
+    @CsvSource({
+        // No lower bound
+        "2673846826, 0, 11",
+        "-23658268, 0, 19",
+        "263478624, 0, 31",
+        "1278332, 0, 32",
+        "99734765, 0, 2326378468282368421",
+        "-63485384, 0, 4872347624242243222",
+        "3876457638, 0, 6263784682638866843",
+        "-126784782, 0, 7256684297832668332",
+        "2637846, 0, 9223372036854775807",
+        // Small range
+        "2634682, 567576, 567586",
+        "-56757798989, -1000, -100",
+        "-97324785, -54656, 12",
+        "23423235, -526783468, 257",
+        // Large range
+        "-2634682, -4611686018427387904, 4611686018427387904",
+        "6786868132, -4962836478223688590, 6723648246224929947",
+        "-263846723, -368268, 9223372036854775807",
+        "7352352, -9223372036854775808, 61523457",
+    })
+    void testNextLongUniform(long seed, long origin, long bound) {
+        final UniformRandomProvider rng = createRNG(seed);
+        final LongSupplier nextMethod = origin == 0 ?
+                () -> rng.nextLong(bound) :
+                () -> rng.nextLong(origin, bound);
+        checkNextInRange("nextLong", origin, bound, nextMethod);
+    }
+
+    @ParameterizedTest
+    @CsvSource({
+        // Note: If the range limits are integers above 2^24 (16777216) it is not possible
+        // to represent all the values with a float. This has no effect on sampling into bins
+        // but should be avoided when generating integers for use in production code.
+
+        // No lower bound.
+        "2673846826, 0, 11",
+        "-23658268, 0, 19",
+        "263478624, 0, 31",
+        "1278332, 0, 32",
+        "99734765, 0, 1234",
+        "-63485384, 0, 578",
+        "3876457638, 0, 10000",
+        "-126784782, 0, 2983423",
+        "2637846, 0, 16777216",
+        // Range
+        "2634682, 567576, 567586",
+        "-56757798989, -1000, -100",
+        "-97324785, -54656, 12",
+        "23423235, -526783468, 257",
+        "-2634682, -688689797, -516827",
+        "6786868132, -67, 67",
+        "-263846723, -5678, 42",
+        "7352352, 678687, 61523457",
+    })
+    void testNextFloatUniform(long seed, float origin, float bound) {
+        Assertions.assertEquals((long) origin, origin, "origin");
+        Assertions.assertEquals((long) bound, bound, "bound");
+        final UniformRandomProvider rng = createRNG(seed);
+        // Note casting as long will round towards zero.
+        // If the upper bound is negative then this can create a domain error so use floor.
+        final LongSupplier nextMethod = origin == 0 ?
+                () -> (long) rng.nextFloat(bound) :
+                () -> (long) Math.floor(rng.nextFloat(origin, bound));
+        checkNextInRange("nextFloat", (long) origin, (long) bound, nextMethod);
+    }
+
+
+    @ParameterizedTest
+    @CsvSource({
+        // Note: If the range limits are integers above 2^53 (9007199254740992) it is not possible
+        // to represent all the values with a double. This has no effect on sampling into bins
+        // but should be avoided when generating integers for use in production code.
+
+        // No lower bound.
+        "2673846826, 0, 11",
+        "-23658268, 0, 19",
+        "263478624, 0, 31",
+        "1278332, 0, 32",
+        "99734765, 0, 1234",
+        "-63485384, 0, 578",
+        "3876457638, 0, 10000",
+        "-126784782, 0, 2983423",
+        "2637846, 0, 9007199254740992",
+        // Range
+        "2634682, 567576, 567586",
+        "-56757798989, -1000, -100",
+        "-97324785, -54656, 12",
+        "23423235, -526783468, 257",
+        "-2634682, -688689797, -516827",
+        "6786868132, -67, 67",
+        "-263846723, -5678, 42",
+        "7352352, 678687, 61523457",
+    })
+    void testNextDoubleUniform(long seed, double origin, double bound) {
+        Assertions.assertEquals((long) origin, origin, "origin");
+        Assertions.assertEquals((long) bound, bound, "bound");
+        final UniformRandomProvider rng = createRNG(seed);
+        // Note casting as long will round towards zero.
+        // If the upper bound is negative then this can create a domain error so use floor.
+        final LongSupplier nextMethod = origin == 0 ?
+                () -> (long) rng.nextDouble(bound) :
+                () -> (long) Math.floor(rng.nextDouble(origin, bound));
+        checkNextInRange("nextDouble", (long) origin, (long) bound, nextMethod);
+    }
+
+    /**
+     * Tests uniformity of the distribution produced by the given
+     * {@code nextMethod}.
+     * It performs a chi-square test of homogeneity of the observed
+     * distribution with the expected uniform distribution.
+     * Repeat tests are performed at the 1% level and the total number of failed
+     * tests is tested at the 0.5% significance level.
+     *
+     * @param method Generator method.
+     * @param origin Lower bound (inclusive).
+     * @param bound Upper bound (exclusive).
+     * @param nextMethod method to call.
+     */
+    private static void checkNextInRange(String method,
+                                         long origin,
+                                         long bound,
+                                         LongSupplier nextMethod) {
+        // Do not change
+        // (statistical test assumes that 500 repeats are made with dof = 9).
+        final int numTests = 500;
+        final int numBins = 10; // dof = numBins - 1
+
+        // Set up bins.
+        final long[] binUpperBounds = new long[numBins];
+        // Range may be above a positive long: step = (bound - origin) / bins
+        final BigDecimal range = BigDecimal.valueOf(bound)
+                .subtract(BigDecimal.valueOf(origin));
+        final double step = range.divide(BigDecimal.TEN).doubleValue();
+        for (int k = 1; k < numBins; k++) {
+            binUpperBounds[k - 1] = origin + (long) (k * step);
+        }
+        // Final bound
+        binUpperBounds[numBins - 1] = bound;
+
+        // Create expected frequencies
+        final double[] expected = new double[numBins];
+        long previousUpperBound = origin;
+        final double scale = SAMPLE_SIZE_BD.divide(range, MathContext.DECIMAL128).doubleValue();
+        double sum = 0;
+        for (int k = 0; k < numBins; k++) {
+            final long binWidth = binUpperBounds[k] - previousUpperBound;
+            expected[k] = scale * binWidth;
+            sum += expected[k];
+            previousUpperBound = binUpperBounds[k];
+        }
+        Assertions.assertEquals(SAMPLE_SIZE, sum, SAMPLE_SIZE * RELATIVE_ERROR, "Invalid expected frequencies");
+
+        final int[] observed = new int[numBins];
+        // Chi-square critical value with 9 degrees of freedom
+        // and 1% significance level.
+        final double chi2CriticalValue = 21.665994333461924;
+
+        // For storing chi2 larger than the critical value.
+        final List<Double> failedStat = new ArrayList<>();
+        try {
+            final int lastDecileIndex = numBins - 1;
+            for (int i = 0; i < numTests; i++) {
+                Arrays.fill(observed, 0);
+                SAMPLE: for (int j = 0; j < SAMPLE_SIZE; j++) {
+                    final long value = nextMethod.getAsLong();
+                    if (value < origin) {
+                        Assertions.fail(String.format("Sample %d not within bound [%d, %d)",
+                                                      value, origin, bound));
+                    }
+
+                    for (int k = 0; k < lastDecileIndex; k++) {
+                        if (value < binUpperBounds[k]) {
+                            ++observed[k];
+                            continue SAMPLE;
+                        }
+                    }
+                    if (value >= bound) {
+                        Assertions.fail(String.format("Sample %d not within bound [%d, %d)",
+                                                      value, origin, bound));
+                    }
+                    ++observed[lastDecileIndex];
+                }
+
+                // Compute chi-square.
+                double chi2 = 0;
+                for (int k = 0; k < numBins; k++) {
+                    final double diff = observed[k] - expected[k];
+                    chi2 += diff * diff / expected[k];
+                }
+
+                // Statistics check.
+                if (chi2 > chi2CriticalValue) {
+                    failedStat.add(chi2);
+                }
+            }
+        } catch (Exception e) {
+            // Should never happen.
+            throw new RuntimeException("Unexpected", e);
+        }
+
+        // The expected number of failed tests can be modelled as a Binomial distribution
+        // B(n, p) with n=500, p=0.01 (500 tests with a 1% significance level).
+        // The cumulative probability of the number of failed tests (X) is:
+        // x     P(X>x)
+        // 10    0.0132
+        // 11    0.00521
+        // 12    0.00190
+
+        if (failedStat.size() > 11) { // Test will fail with 0.5% probability
+            Assertions.fail(String.format(
+                "%s: Too many failures for n = %d, sample size = %d " +
+                "(%d out of %d tests failed, chi2 > %.3f=%s)",
+                method, bound, SAMPLE_SIZE, failedStat.size(), numTests, chi2CriticalValue,
+                failedStat.stream().map(d -> String.format("%.3f", d))
+                          .collect(Collectors.joining(", ", "[", "]"))));
+        }
+    }
+}
diff --git a/src/main/resources/pmd/pmd-ruleset.xml b/src/main/resources/pmd/pmd-ruleset.xml
index af457bb..ae95f65 100644
--- a/src/main/resources/pmd/pmd-ruleset.xml
+++ b/src/main/resources/pmd/pmd-ruleset.xml
@@ -120,7 +120,8 @@
         value="//ClassOrInterfaceDeclaration[@SimpleName='ListSampler' or @SimpleName='ProviderBuilder'
           or @SimpleName='ThreadLocalRandomSource' or @SimpleName='SeedFactory'
           or @SimpleName='Coordinates' or @SimpleName='Hex' or @SimpleName='SpecialMath'
-          or @SimpleName='Conversions' or @SimpleName='MixFunctions' or @SimpleName='LXMSupport']"/>
+          or @SimpleName='Conversions' or @SimpleName='MixFunctions' or @SimpleName='LXMSupport'
+          or @SimpleName='UniformRandomProviderSupport']"/>
       <!-- Allow samplers to have only factory constructors -->
       <property name="utilityClassPattern" value="[A-Z][a-zA-Z0-9]+(Utils?|Helper|Sampler)" />
     </properties>