In this lesson you will learn about the mathematical capabilities of Java. Java is designed as a multipurpose language. In addition to standard, general-purpose arithmetic support, Java supports a small number of more-advanced math concepts such as infinity. Support is provided through a mixture of Java language features and library classes.
Java supplies a wide range of floating point and integral numerics in both fixed and arbitrary precision form. The representation of numerics and related functions in Java generally adheres to IEEE standards. In some cases, you can choose to use either algorithms designed for speed or algorithms whose results strictly adhere to published standards.
Topics:
• BigDecimal
• additional integral types and operations
• numeric casting
• expression evaluation order
• NaN
(Not a Number)
• infinity
• numeric overflow
• bit manipulation
• java.lang.Math
• static imports
The class java.math.BigDecimal allows you to do arbitrary-precision decimal (base 10) floating-point arithmetic. This means that arithmetic with BigDecimal works like you learned in grade school. The BigDecimal class provides a comprehensive set of arithmetic methods and extensive control over rounding. A BigDecimal object represents a decimal number of arbitrary precision, meaning that you decide the number of significant digits it will contain. It is immutable—you cannot change the number that a BigDecimal stores.
The most frequent use of BigDecimal is in financial applications. Financial applications often require you to accurately account for money down to the last hundredth of a cent or even at more granular levels.
Floating-point numbers, as implemented by the Java primitive types float
and double
, do not allow every number to be represented exactly as specified. This is because it is not mathematically possible to represent certain decimal numbers (such as 0.1) using binary.1 BigDecimal can exactly represent every number.
1 See http://www.alphaworks.ibm.com/aw.nsf/FAQs/bigdecimal.
There are two major downsides to using BigDecimal. First, hardware normally optimizes binary floating-point operations, but BigDecimal's decimal floating point is implemented in software. You may experience poor performance when doing extremely heavy math calculations with BigDecimal.
Second, there is no mathematical operator support for BigDecimal. You must execute common operations such as addition and multiplication via method calls. The requisite code is wordy and thus tedious to write and read.
The student information system must maintain an account of charges and credits for each student. The first test demonstrates the ability of an Account to track a balance based on applied charges and credits.
The preferred way to construct a new BigDecimal is to pass a String to its constructor. The String contains the value you want the BigDecimal to represent. You could also pass a double
, but it may not precisely represent the number you expect, due to the nature of floating-point representation in Java.2 As a result, the BigDecimal may not precisely represent the number you expect.
2 See the Java Glossary entry at http://mindprod.com/jgloss/floatingpoint.html for a discussion of floating-point representation.
You may also construct BigDecimal objects from other BigDecimal objects. This allows you to directly use an expression returning a BigDecimal as the constructor parameter.
The Account implementation:
package sis.studentinfo;
import java.math.BigDecimal;
public class Account {
private BigDecimal balance = new BigDecimal("0.00");
public void credit(BigDecimal amount) {
balance = balance.add(amount);
}
public BigDecimal getBalance() {
return balance;
}
}
BigDecimal objects are immutable. Sending an add
message to a BigDecimal does not alter it. Instead, the add
method takes the argument's value and adds it to the value the BigDecimal stores. The add
method then creates and returns a new BigDecimal object with this sum. In the Account implementation, the result of sending the add
message to balance
is a new BigDecimal object. You must reassign this new object to balance
in order to maintain the total.
BigDecimal provides a full complement of methods to represent arithmetic operations. They include abs
, add
, divide
, max
, min
, multiply
, negate
, and subtract
. Additional methods help you to manage scaling and to extract values as different types from a BigDecimal.
In testTransactions
, the assertion expects the result to be a BigDecimal with value "11.10"
—with two places after the decimal point. In contrast, you would expect the result of an equivalent Java float
or double
operation to be "11.1"
. Adding such an assertion:
assertEquals(new BigDecimal("11.1"), account.getBalance());
fails the test.
The number of digits in the fractional part represents the scale of the number. When one does arithmetic operations on BigDecimal numbers, the result is a new BigDecimal whose scale is the larger between the receiving BigDecimal and the parameter BigDecimal. For example:
assertEquals(new BigDecimal("5.300"),
new BigDecimal("5.000").add(new BigDecimal("0.3")));
That is, the scale as the result of adding a 1-scale number to a 3-scale number is 3.
When dividing numbers, the result often contains a larger number of fractional digits than either the divisor or the dividend. By default, BigDecimal does not expand the scale of the result to this larger amount; instead, it restricts it to the larger of the dividend and divisor scales. You can also explicitly define a new scale for the result.
Constraining the scale means that Java may need to round the result of a division operation. Java provides eight different rounding modes. You are probably most familiar with the rounding mode that corresponds to BigDecimal.ROUND_HALF_UP. This mode tells BigDecimal to round the result up if the distance toward the nearest neighbor is greater than or equal to 0.5, and to round it down otherwise. For example, 5.935 would round up to the 2-scale number 5.94, and 5.934 would round down to 5.93.
For an account, you must be able to capture the average transaction amount. A test demonstrates this need:
public void testTransactionAverage() {
Account account = new Account();
account.credit(new BigDecimal("0.10"));
account.credit(new BigDecimal("11.00"));
account.credit(new BigDecimal("2.99"));
assertEquals(new BigDecimal("4.70"), account.transactionAverage());
}
Review the Java API documentation for details on the other seven rounding modes.
To this point, you have learned about the numeric primitive types int
, double
, and float
. You have learned about the basic arithmetic operators, the increment/decrement operators for int
, and compound assignment.
You have been using the int
type for integer numerics. There are other integer types available, each representing different sizes available in which to store numbers of that type. Table 10.1 lists all integral types, their size, and the range of values that they support.
Table 10.1. Integer Types
The char
type is also a numeric type. Refer to Lesson 3 for more information on char
.
You usually express numeric literals as decimal, or base 10, numbers. Java also allows you to represent them in hexadecimal (base 16) or octal (base 8).
You prefix hexadecimal literals with 0x
.
You prefix octal literals with 0
:
assertEquals(10, 012);
Integral literals are by default of int
type. You can force an integral literal to be a long
by suffixing it with the letter L
. The lowercase letter l
can also be used, but prefer use of an uppercase L
, since the lowercase letter is difficult to discern from the number 1
.
Let's revisit the code in the class Performance (from Lesson 7) that calculates the average for a series of tests.
public double average() {
double total = 0.0;
for (int score: tests)
total += score;
return total / tests.length;
}
Suppose the total
local variable had been declared as an int
instead of a double
, which is reasonable since each test score is an int
. Adding an int
to an int
returns another int
.
public double average() {
int total = 0;
for (int score: tests)
total += score;
return total / tests.length;
}
This seemingly innocuous change will break several tests in PerformanceTest. The problem is, dividing an int
by an int
also returns an int
. Integer division always produces integer results; any remainders are discarded. The following two assertions demonstrate correct integer division:
assertEquals(2, 13 / 5);
assertEquals(0, 2 / 4);
If you need the remainder while doing integer division, use the modulus operator (%
).
assertEquals(0, 40 % 8);
assertEquals(3, 13 % 5);
Java supports numeric types of different ranges in value. The float
type uses 32 bits for its implementation and expresses a smaller range of numbers than does double
, which uses 64 bits. The integral primitive types—char
, byte
, short
, int
, and long
—each support a range that is different from the rest of the integral primitive types.
You can always assign a variable of a smaller primitive type to a larger primitive type. For example, you can always assign a float
reference to a double
reference:
float x = 3.1415f;
double y = x;
Behind the scenes, this assignment implicitly causes a type conversion from float
to double
.
Going the other way—from a larger primitive type to a smaller—implies that there might be some loss of information. If a number stored as a double is larger than the largest possible float, assigning it to a float would result in the truncation of information. In this situation, Java recognizes the possible loss of information and forces you to cast so that you explicitly recognize that there might be a problem.
If you attempt to compile this code:
double x = 3.1415d;
float y = x;
you will receive a compiler error:
possible loss of precision
found : double
required: float
float y = x;
^
You must cast the double
to a float
:
double x = 3.1415d;
float y = (float)x;
You may want to go the other way and cast from integral primitives to floating-point numbers in order to force noninteger division. A solution to the above problem of calculating the average for a Performance is to cast the dividend to a double
before applying the divisor:
public double average() {
int total = 0;
for (int score: tests)
total += score;
return (double)total / tests.length;
}
As an added note: When it builds an expression that uses a mix of integral and float values, Java converts the result of the expression to an appropriate float value:
assertEquals(600.0f, 20.0f * 30, 0.05);
assertEquals(0.5, 15.0 / 30, 0.05);
assertEquals(0.5, 15 / 30.0, 0.05);
The order of evaluation in a complex expression is important. The basic rules:
• Java evaluates parenthesized expressions first, from innermost parentheses to outermost parentheses.
• Certain operators have higher precedence than others; for example, multiplication has a higher precedence than addition.
• Otherwise, expressions are evaluated from left to right.
You should use parentheses in particularly complex expressions, since the precedence rules can be difficult to remember. In fact, some of them are counterintuitive. Parenthesizing is worth the effort to ensure that there is no misunderstanding about how an expression will be evaluated. Just don't go overboard with the parentheses—most developers should be familiar with the basic precedence rules.
Use parentheses to simplify understanding of complex expressions.
Here are a few examples of how precedence affects the result of an expression:
assertEquals(7, 3 * 4 - 5); // left to right
assertEquals(-11, 4 - 5 * 3); // multiplication before subtraction
assertEquals(-3, 3 * (4 - 5)); // parentheses evaluate first
When in doubt, write a test!
If no test scores exist, the average for a Performance should be zero. You haven't yet written a test for this scenario. Add the following brief test to PerformanceTest.
public void testAverageForNoScores() {
Performance performance = new Performance();
assertEquals(0.0, performance.average());
}
If you run this, oops! You get a NullPointerException. That can be solved by initializing the integer array of tests defined in Performance to an empty array:
private int[] tests = {};
When rerunning the tests, you get a different exception:
junit.framework.AssertionFailedError: expected:<0.0> but was:<NaN>
NaN
is a constant defined on both the java.lang.Float and java.lang.Double classes that means “Not a Number.”
If there are no scores, tests.length()
returns 0
, meaning you are dividing by zero in this line of code:
return total / tests.length;
When it works with integers, Java throws an ArithmeticException to indicate that you are dividing by zero. With floating-point numbers, you get NaN
, which is a legitimate floating-point number.
Fix the problem by checking the number of test scores as the very first thing in average
.
public double average() {
if (tests.length == 0)
return 0.0;
int total = 0;
for (int score: tests)
total += score;
return (double)total / tests.length;
}
NaN
has some interesting characteristics. Any boolean comparisons against NaN
always result in false
, as these language tests demonstrate:
assertFalse(Double.NaN > 0.0);
assertFalse(Double.NaN < 1.0);
assertFalse(Double.NaN == 1.0);
You might want the average method to return NaN
. But how would you write the test, since you cannot compare NaN
to any other floating-point number? Java supplies the isNaN
static method, defined on both Float and Double, for this purpose:
public void testAverageForNoScores() {
Performance performance = new Performance();
assertTrue(Double.isNaN(performance.average()));
}
The java.lang.Float class provides two constants for infinity: Float.NEGATIVE_INFINITY
and Float.POSITIVE_INFINITY
. The class java.lang.Double provides corresponding constants.
While integral division by zero results in an error condition, double
and float
operations involving zero division result in the mathematically correct infinity value. The following assertions demonstrate use of the infinity constants.
When working with integral variables, you must be careful to avoid overflow, which can create incorrect results.
For each numeric type, Java provides constants representing its range. For example, the constants Integer.MAX_VALUE
and Integer.MIN_VALUE
represent the largest (231 – 1) and smallest (–231) values an int
can have, respectively.
Java will internally assign the results of an expression to an appropriately large primitive type. The example test shows how you can add one to a byte
at its maximum value (127). Java stores the result of the expression as a larger primitive value.
byte b = Byte.MAX_VALUE;
assertEquals(Byte.MAX_VALUE + 1, b + 1);
But if you attempt to assign the result of an expression that is too large back to the byte
, the results are probably not what you expected. The following test passes, showing that adding 1 to a byte at maximum value results in the smallest possible byte:
byte b = Byte.MAX_VALUE;
assertEquals(Byte.MAX_VALUE + 1, b + 1);
b += 1;
assertEquals(Byte.MIN_VALUE, b);
The result is due to the way Java stores the numbers in binary. Any additional significant bits are lost in an overflow condition.
Floating-point numbers overflow to infinity.
assertTrue(Double.isInfinite(Double.MAX_VALUE * Double.MAX_VALUE));
Floating-point numbers can also underflow—that is, have a value too close to zero to represent. Java makes these numbers zero.
Java supports bit-level operations on integers. Among myriad other uses, you might use bit manipulation for mathematical calculations, for performance (some mathematical operations may be faster using bit shifting), in encryption, or for working with a compact collection of flags.
Your computer ultimately stores all numbers internally as a series of bits. A bit is represented by a binary, or base 2, number. A binary number is either 0 or 1. The following table on top of page 381 counts in binary from 0 through 15.
The table also shows the hexadecimal (“hex”) representation of each number. Java does not allow you to use binary literals, so the easiest way to understand bit operations is to represent numbers with hexadecimal (base 16) literals. Each hex digit in a hex literal represents four binary digits.
Java represents an int
as 32 binary digits. The leading bit indicates whether the int
is positive (0
) or negative (1
). Java stores positive integers as pure binary—each binary digit adds into a positive sum. Negative integers are stored in two's complement form. To obtain the two's complement representation of a number, take its positive binary representation, invert all binary digits, and add 1
.
For example, the number 17
is represented in two's complement as:
0000_0000_0000_0000_0000_0000_0001_00013
3 I have added underscores for readability.
The number -17
is represented in two's complement as:
1111_1111_1111_1111_1111_1111_1110_1111
Java contains four logical operators that allow you to manipulate bits: bit-and, bit-or, negation, and bit-xor (exclusive-or). A truth table defines how each logical bit operator works, by specifying the results for all possible bit pairings.
The bit-and, bit-or, and bit-xor logical operators operate on two integers; they are therefore referred to as binary operators. The negation operator operates on a single integer and is thus a unary operator.
To execute a binary logical operation, Java compares all corresponding bits from each of the two numbers involved. When doing a bitwise-and operation on two 32-bit integers, then, this means that 32 individual bit-ands take place. To execute a unary logical operation against an integer, Java negates each bit in the integer individually.
You can perform logical bit operations against two integers of type int
or smaller. However, Java internally translates integers declared as smaller than int
—byte
, short
, and char
—to type int
before the bit operation takes place.
The bit-and operator (&
) is also known as bitwise multiplication. If both bits are 1
, bitwise multiplication returns a 1
; otherwise it returns a 0
. The following TDTT demonstrates bit-and.
assertEquals(0, 0 & 0);
assertEquals(0, 0 & 1);
assertEquals(0, 1 & 0);
assertEquals(1, 1 & 1);
The bit-or operator (|
) is also known as bitwise addition. If both bits are 0
, bitwise addition returns a 0
; otherwise it returns a 1
. The following TDTT demonstrates bit-or.
assertEquals(0, 0 | 0);
assertEquals(1, 0 | 1);
assertEquals(1, 1 | 0);
assertEquals(1, 1 | 1);
The xor (exclusive-or) operator (^
) is also known as bitwise difference. If both bits are the same, bitwise difference returns 0
; otherwise it returns a 1
. The following TDTT demonstrates xor.
assertEquals(0, 0 ^ 0);
assertEquals(1, 0 ^ 1);
assertEquals(1, 1 ^ 0);
assertEquals(0, 1 ^ 1);
The logical negation operator (~
) flips all bits in the integer so that 0
s become 1
s and 1
s become 0
s.
Java supports the use of compound assignment with logical bit operators. Thus, x &= 1
is equivalent to x = x & 1
.
If you have a series of related flags (boolean values) to represent, you can use an integer variable to compactly represent them. A less-efficient alternative would be to define each as a separate boolean
variable or as an array of boolean
s. Each bit in the integer represents a different flag. Thus, you could represent eight flags in a single byte. For example, the binary value 00000001
would mean that the first flag is on and all other flags are off. The binary value 00000101
would mean that the first and third flags are on and all other flags are off.
To set the values of each flag, you first define a mask for each binary position. The mask for the first position is 00000001
(decimal 1), the mask for the second position is 00000010
(decimal 2), and so on. Setting a value is done using bit-or, and extracting a value is done using bit-and.
You need to be able to set four yes-no flags on each student: Does the student reside on campus, is the student tax-exempt, is the student a minor, and is the student a troublemaker? You have 200,000 students and memory is at a premium.
You should normally prefer explicit query methods for each flag (isOnCampus
, isTroublemaker
, etc.). The above approach may be preferable if you have a large number of flags and more dynamic needs.
The relevant code in Student follows.
Each flag is an enum
constant, defined by the Flag enum
located in Student. Each enum
instantiation passes an integer representing a mask to the constructor of Flag. You store the flags in the int
instance variable settings
. You explicitly initialize this variable to all 0
s to demonstrate intent.
The set
method takes a variable number of Flag enum
objects. It loops through the array and applies the Flag's mask to the settings
variable by using a bit-or operator.
The unset
method loops through Flag enum
objects and bit-ands the negation of the Flag's mask to the settings
variable.
A demonstration of how the bit-or operation sets the correct bit:
The isOn
method bit-ands a Flag's mask with the settings
variable.
Finally, a demonstration of the use of negation in the unset
method:
Using bit operations to store multiple flags is a classic technique that comes from a need to squeeze the most possible information into memory. It is not normally recommended or needed for most Java development, since an array or other collection of boolean
values provides a clearer, simpler representation.
The xor operator has the unique capability of being reversible.
int x = 5; // 101
int y = 7; // 111
int xPrime = x ^ y; // 010
assertEquals(2, xPrime);
assertEquals(x, xPrime ^ y);
You can also use xor as the basis for parity checking. Transmitting data introduces the possibility of individual bits getting corrupted. A parity check involves transmitting additional information that acts like a checksum. You calculate this checksum by applying an xor against all data sent. The receiver executes the same algorithm against the data and checksum. If checksums do not match, the sender knows to retransmit the data.
The parity check is binary: A stream of data has either even parity or odd parity. If the number of 1s in the data is even, parity is even. If the number of 1s in the data is odd, the parity is odd. You can use xor to calculate this parity. Here is a simple stand-alone test:
public void testParity() {
assertEquals(0, xorAll(0, 1, 0, 1));
assertEquals(1, xorAll(0, 1, 1, 1));
}
private int xorAll(int first, int... rest) {
int parity = first;
for (int num: rest)
parity ^= num;
return parity;
}
Xoring an even number of 1s will always result in an even parity (0). Xoring an odd number of 1s will always result in an odd parity (1).
The reason this works: Xoring is the same as adding two numbers, then taking modulus of dividing by 2. Here is an extension of the truth table that demonstrates this:
Dividing mod 2 tells you whether a number is odd (1) or even (0). When you add a string of binary digits together, only the 1 digits will contribute to a sum. Thus, taking this sum modulus 2 will tell you whether the number of 1 digits is odd or even.
Take this one level further to calculate a checksum for any integer. The job of the ParityChecker class is to calculate a checksum for a byte array of data. The test shows how corrupting a single byte within a datum results in a different checksum.
ParityChecker loops through all bytes of data, xoring each byte with a cumulative checksum. In the test, I lined up the binary translation of each decimal number (source1
, source2
, and source3
). This allows you to see how the checksum
of 5 is a result of xoring the bits in each column.
A simple xor parity check will not catch all possible errors. More-complex schemes involve adding a parity bit to each byte transmitted in addition to the a parity byte at the end of all bytes transmitted. This provides a matrix scheme that can also pinpoint the bytes in error.
Java provides three operators for shifting bits either left or right.
To shift bits left or right, and maintain the sign bit, use the bit shift left (<<
) or bit shift right (>>
) operators.
A bit shift left moves each bit one position to the left. Leftmost bits are lost. Rightmost bits are filled with zeroes.
For example, shifting the bit pattern 1011
one position to the left in a 4-bit unsigned number would result in 0110
. Some annotated Java examples:
You'll note that bit shifting left is the equivalent of multiplying by powers of 2. Bit shifting right is the equivalent of dividing by powers of 2.
The unsigned bit shift right shifts all bits irrespective of the sign bit. Rightmost bits are lost, and the leftmost bits (including the sign bit) are filled with 0
.
Note that an unsigned bit shift right always results in a positive number.
Bit shifting has uses in cryptography and in graphical image manipulation. You can also use bit shifting for some mathematical operations, such as dividing or multiplying by powers of 2. Only do so if you need extremely fast performance, and then only if your performance tests show you that the math is the performance problem.
The Java API library includes the class java.util.BitSet. It encapsulates a vector of bits and grows as needed. BitSet objects are mutable—its individual bits may be set or unset. You can bit-and, bit-or, or bit-xor one BitSet object to another. You can also negate a BitSet. One of its few benefits is that it supports bit operations for numbers larger than the capacity of an int
.
The class java.lang.Math provides a utility library of mathematical functions. It also provides constant definitions Math.E
, for the base of the natural log, and Math.PI
, to represent pi. These constants are double quantities that provide fifteen decimal places.
The Java API library also contains a class named java.lang.StrictMath, which provides bit-for-bit adherence to accepted standard results for the functions. For most purposes, Math is acceptable and also results in better performance.
I summarize the functions provided by the Math class in the following table. Refer to the Java API documentation for complete details on each function.
A method to calculate the hypotenuse of a right triangle provides a simple example. (The hypotenuse is the side opposite the right angle.)
The pow
function is used to square the sides a
and b
. The hypotenuse
method then sums these squares and returns the square root of the sum using sqrt
.
Pervasive use of the Math class gives you a justifiable reason for using the static import facility introduced in Lesson 4. Code with a lot of Math method calls otherwise becomes a nuisance to code and read.
I've named the utility class Math, like the java.lang.Math class. While this is a questionable practice (it may introduce unnecessary confusion), it demonstrates that Java can disambiguate the two.
As you saw in Lesson 7, Java supplies a corresponding wrapper class for each primitive type: Integer, Double, Float, Byte, Boolean, and so on. The primary use of the wrapper class is to convert primitives to object form so that they can be stored in collections.
The wrapper classes for the numeric types have additional uses. You learned earlier in this lesson that each numeric wrapper class provides a MIN_VALUE
and MAX_VALUE
constant. You also saw how the Float and Double classes provide constants for Not a Number (NaN
) and infinity.
The class Integer provides static utility methods to produce printable representations of an int
in hex, octal, and binary form.
assertEquals("101", Integer.toBinaryString(5));
assertEquals("32", Integer.toHexString(50));
assertEquals("21", Integer.toOctalString(17));
The Java library includes general-purpose methods to produce a proper String for any radix (base). The following assertion shows how to get the trinary (base 3) String for an int
.
assertEquals("1022", Integer.toString(35, 3));
In Lesson 8, you learned that the method Integer.parseInt
takes a String and parses it to produce the corresponding int
value. You typically use the parseInt
method to convert user interface input into numeric form. All characters in the input String must be decimal digits, except for the first character, which may optionally be a negative sign. If the String does not contain a parseable integer, parseInt
throws a NumberFormatException.
An additional form of parseInt
takes a radix, representing the base of the input string, as the second parameter. The method valueOf
operates the same as parseInt
, except that it returns a new Integer object instead of an int
.
Each of the corresponding numeric wrapper classes has a parsing method. For example, you may convert a String to a double
using Double.parseDouble
, or to a float
using Float.parseFloat
.
The parseInt
method can accept only decimal input. The decode
method can parse an input string that might be in hex or octal, as these assertions show:
assertEquals(253, Integer.decode("0xFD"));
assertEquals(253, Integer.decode("0XFD"));
assertEquals(253, Integer.decode("#FD"));
assertEquals(15, Integer.decode("017"));
assertEquals(10, Integer.decode("10"));
assertEquals(-253, Integer.decode("-0xFD"));
assertEquals(-253, Integer.decode("-0XFD"));
assertEquals(-253, Integer.decode("-#FD"));
assertEquals(-15, Integer.decode("-017"));
assertEquals(-10, Integer.decode("-10"));
The class Math provides the method random
to return a pseudorandom double
between 0.0
and 1.0
. A number in this range may be all you need. The generated number is not truly random—the elements of a sequence of pseudorandom numbers are only approximately independent from each other.4 For most applications, this is sufficient.
4 http://en.wikipedia.org/wiki/Pseudo-random_number_generator.
The class java.util.Random is a more comprehensive solution for generating pseudorandom numbers. It produces pseudorandom sequences of boolean
s, byte
s, int
s, long
s, float
s, Gaussians, or double
s. A pseudorandom sequence generator is not purely random; it is instead based upon an arithmetical algorithm.
You can create a Random instance with or without a seed. A seed is essentially a unique identifier for a random sequence. Two Random objects created with the same seed will produce the same sequence of values. If you create a Random object without explicitly specifying a seed, the class uses the system clock as the basis for the seed.5
5 Which means that if you construct two Random objects and the construction happens to execute in the same nanosecond, you get the same sequence in both. In earlier versions of Java, the Random constructor used the current system time in milliseconds. This increased granularity significantly increased the likelihood of two Random objects having the same sequence.
You could create a simulation of coin flips through repeated use of the nextBoolean
method on Random. A true
value would indicate heads and a false
value would indicate tails. The below test shows use of the seed value to produce two identical pseudorandom coin-flip sequences.
The methods nextInt
, nextDouble
, nextLong
, and nextFloat
work similarly. An additional version of nextInt
takes as a parameter a maximum value, such that the method always returns a number from 0
through the maximum value.
There are a few ways to write unit tests against code that must use random sequences. One way is to provide a subclass implementation of the Random class and substitute that for the Random class used in the code. The substitute Random class will provide a known sequence of values. This technique is known as mocking the Random class. I will cover mocking in considerable depth later.
In Lesson 8, you provided your own logging Handler class solely for purposes of testing. This was also a form of mocking.
You must assign passwords to students so they can access their accounts online. Passwords are eight characters long. Each character of the password must fall in a certain character range.
The test method testGeneratePassword
sets the random
variable into the PasswordGenerator class to point to an instance of MockRandom. The MockRandom class extends from the Random class. The Java API documentation explains how to properly extend the Random class by overriding the method next(int bits)
to return a random number based on a bit sequence. All other methods in the Random class are based on this method.
The mock implementation takes an int
representing a starting character value as a parameter to its constructor. It uses this to calculate an initial value for the random sequence and stores it as i
. The initial value is a number relative to the lowest valid character in the password. Its next(int bits)
method simply returns and then increments the value of i
.
MockRandom is defined completely within PasswordGeneratorTest. It is a nested class of PasswordGenerator. You can directly instantiate Mock-Random from within PasswordGeneratorTest, but not from any other class. There are additional kinds of nested classes, each kind with its own nuances. In Lesson 11, you will learn about nested classes in depth. Until then, use them with caution.
The implementation of PasswordGenerator follows.
Note that if the package-level setRandom
method is not called, the random instance variable keeps its initialized value of a legitimate java.util.Random instance. Since MockRandom meets the contract of java.util.Random, you can substitute an instance of MockRandom for Random for purposes of testing.
The goal of testGeneratePassword
is to prove that the PasswordGenerator class can generate and return a random password. The test does not need to reprove the functionality inherent in the Random class, but the test does need to prove that a PasswordGenerator object interacts with a Random instance (or subclass instance) through its published interface, nextInt(int max)
. The test must also prove that PasswordGenerator uses the nextInt
return value properly. In this example, PasswordGenerator uses the nextInt
return value to help construct an 8-character String of random values.
Java supplies an additional random class, java.util.SecureRandom, as a standards-based, cryptographically strong pseudorandom number generator.
Figure 10.1. Mocking the Random Class
"10.00"
and another using the value "1"
. Show they are not equal. Use multiplication and scale to create a new BigDecimal from the second so that it equals the first. Now reverse the transformation, starting from 10.00, and get back to 1.0.9
and 0.005 * 2.0
are not equal with floats. To what precision are they equal? With doubles?public class CompilerError {
float x = 0.01;
}
0xDEAD
. How could you recode the literal in octal?int
values. Code it to return only numbers in the list that are divisible by 3. Code it twice: The first time, use only the modulus (%)
operator. The second time, use the division (/)
operator and the multiplication operator if necessary, but not the %
operator. Test against the sequence of integers from 1 through 10.toChar
method on the Board class.float x = 1;
float x = 1.0;
float x = (int)1.0;
(int)1.9
?Math.rint(1.9)
?Math.rint
, how is rounding performed? Is 1.5 equal to 1 or 0? How about 2.5?x
is 5 and y
is 10 and x
and y
are both ints
? Flag the lines that wouldn't compile, as well. What are the values of x
and y
afterward? (Consider each expression to be discrete—i.e., each expression starts with the values of x
and y
at 5 and 10.)
<<
operator, convert 17
into 34
.~1
?>>
and >>>
. Is there a difference between the two for positive numbers? For negative numbers?Math.random
to generate a random integer from 1 through 50. How will you go about testing your function? Can you test it perfectly? How confident can you get that your code works?double
is not the same between a Random seeded with 1 and a Random with no seed. Can you make a test that absolutely proves that this is true?char
, byte
, short
, int
, long
). Hint: Use shift operators. Remember that signed types require an extra bit to store the sign.18.191.186.219