Numeric Operators
In the last section, we created and initialized some variables. Now let's look at Java's facilities for numeric operations. They will look very familiar to C programmers, but there are some differences. Operators that perform arithmetic or numeric comparison are shown in Table 3.2. The precedence gives the order in which the compiler performs operations, with 1 being the first. You can always use parentheses to control the order in which operations are performed.
Table 3.2 Numeric Operators in Java
Precedence |
Operator |
Description |
1 |
++ |
Increment by 1 (or 1.0) |
1 |
-- |
Decrement by 1 (or 1.0) |
1 |
+ |
Unary plus |
1 |
- |
Unary minus |
2 |
* |
Multiplication |
2 |
/ |
Division |
2 |
% |
Modulo |
3 |
+ |
Addition |
3 |
- |
Subtraction |
5 |
< |
Less than test |
5 |
> |
Greater than test |
5 |
<= |
Less than/equal test |
5 |
>= |
Greater than/equal test |
6 |
== |
Equals test (identical values) |
6 |
!= |
Not equals to test |
13 |
op= |
op with assignment (+=, -=, *=, and so on) |
Order of Evaluation of Operands
When evaluating an expression, Java always evaluates the operand on the left first. This rule can be important if the left operand is a method call or an expression that modifies a variable that appears on the right.
Increment and Decrement
Java follows the C convention with the increment and decrement operators, which directly modify the value in a primitive variable by adding or subtracting 1. When this operator appears in a larger expression, the order in which the modification occurs depends on the position of the operator, as shown in the following code fragment:
1. int x = 5 ; 2. int y = x++ ; // y gets the value 5, before incrementing x 3. int y2 = ++x ; // y2 gets the value 7, after incrementing
When evaluating expressions that involve increment and decrement, keep in mind that expression evaluation is always "left first." For example, consider the following sequence:
1. int a = 2 ; 2. a += ++a ; 3. System.out.println( "value of a= " + a );
This code prints value of a= 5 because the Java first evaluates the left side of += as 2, and then evaluates ++a as 3, and finally carries out the addition and stores the result in a, replacing the value created by ++a. Remember that ++ or -- before the variable indicates "pre" evaluation of the variable and when the operator is after the variable, it indicates "post" evaluation.
CAUTION
It would not be at all unusual for you to have one or more questions in which the order of increment or decrement operations is critical.
Unary + and - Operators
Distinct from the arithmetic add and subtract operators, the unary + and - operators affect a single operand. Unary - changes the sign of a numeric expression to the right of the operator. Unary + has no effect on an expression; it is included for completeness and because some programmers like to use it to emphasize that a number is positive.
Arithmetic Operators
In general, the arithmetic operators +, -, /, and * work as you would expect, but you will need to know the conventions that the compiler uses to convert various primitives before performing operations. As with C, the operator appears between its two operands.
Arithmetic Operators with Assignment
The operators that combine an arithmetic operator with the = assignment operator perform an operation on the contents of the variable on the left side and store the results in the variable. For example, in the following code, line 2 is equivalent to line 3:
1. int x = 5 ; 2. x += 10 ; // x gets 5 + 10 3. x = x + 10 ;
The compiler makes some assumptions when it sees an operator with assignment. For instance, in the following sequence of statements, the compiler does not object to the fact that line 2 adds an int value to a byte because it performs an explicit cast, the equivalent of line 4; however, in line 3, which is the logical equivalent of line 2, it raises an objection:
1. byte b = 0 ; 2. b += 27 ; 3. b = b + 27 ; 4. b = (byte)(b + 27) ;
Widening Conversions
Widening conversions of a number are those that don't lose information on the overall magnitude. For instance, the integer primitives byte, char, and short can all be converted to an int primitive, and an int primitive can be converted to a long integer without loss of information. You may see this sort of widening conversion referred to as numeric promotion.
An int can be converted to a float primitive, but there may be some loss of precision in the least significant bits. This conversion is carried out according to the Institute of Electrical and Electronics Engineers (IEEE) standard.
When evaluating an arithmetic expression with two operands, the compiler converts primitives by widening according to these rules:
If either is of type double, the other is converted to double.
Otherwise, if either is a float, the other is converted to float.
Otherwise, if either is of type long, the other is converted to long.
Otherwise, both operands are converted to int.
These automatic conversions can have significant consequences, particularly when you are trying to store the results of an expression in a primitive variable that has a smaller capacity than one of the operands. Consider the following code:
1. int a = 2 ; 2. float x = 1.5f ; 3. a = x * a ;
By rule 2, both sides of the expression in line 3 are converted to float. However, the compiler knows that float variables have a much wider range of magnitude than int variables. Therefore, if you try to compile the code, you get an error message. To avoid this error, you have to use a cast.
Conversion with Casting
You can always direct the order and direction of number conversions with specific casts. As an example, consider the following code fragment:
1. float x = 123 ; 2. byte b = 23 ; 3. float y = x + b ; 4. b = (byte) y ;
In line 3, the compiler converts b to a float before performing the addition. You have to include the specific cast operation to get the compiler to accept line 4 because converting a float to an 8-bit byte involves potential loss of magnitude and precision.
The Modulo Operator
You can think of the % (modulo) operator as yielding the remainder from an implied division of the left operand (dividend) by the right operand (divisor). The result is negative only when the dividend is negative. Note that if the operands are integers, the ArithmeticException can be thrown if the divisor is zero, just as in integer division.
Using % with floating-point primitives produces results similar to the integer operation, but note that the special floating-point values, such as NaN and POSITIVE_INFINITY, can result.
Numeric Comparisons
The numeric comparisons <, >, <=, >=, !=, and == work pretty much as expected with Java primitives. If the operands are of two different types, the compiler promotes one or both according to the rules for arithmetic operators. Remember that the result of a numeric comparison is a boolean primitive.
The <, >, <=, and >= operators are meaningless for objects, but the == and != operators can be used. When used with object references, == results in true only if the references are identical. We return to this subject later in this chapter in the "Testing Object Equality" section because it is very important.
CAUTION
Be sure you master the differences between the == comparison with primitives and with objects. In our experience, this difference has been one of the most frequent sources of errors (on the exam and in programming).
Arithmetic Errors
In general, Java lets you make a variety of arithmetic errors without warning you. If your code conducts operations that overflow the bounds of 32-bit or 64-bit integer arithmetic, that is your problem. Division by zero in integer arithmetic is the only error that produces a runtime exception, namely, an ArithmeticException.
On the other hand, floating-point operations meet the requirements of the IEEE standard for representing values that are out of the normal range. These special values are defined for float primitives as constants in the Float class, as shown in Table 3.3. The string representation is what you get from the Float.toString method. The Double class defines similar constants for double primitive values.
Table 3.3 Special Floating-Point Values
Constant |
Interpretation |
Corresponding String |
Float.MAX_VALUE |
The largest number representable |
3.4028235E38 |
Float.MIN_VALUE |
The smallest number representable |
1.4E-45 |
Float.NEGATIVE_INFINITY |
Negative divided by zero |
-Infinity |
Float.POSITIVE_INFINITY |
Positive divided by zero |
Infinity |
Float.NaN |
Not a number |
NaN |
Not a Number
The special NaN value is particularly tricky to handle. NaN can result from mathematical functions that are undefined, such as taking the square root of a negative number.
You cannot directly compare the NaN value with anything. You must detect it with the special Float.isNaN or Double.isNaN methods, as in the following example:
1. float x = (float) Math.sqrt( y ) ; // where y may be neg 2. if( x == Float.NaN ) x = 0.0 ; // WRONG, always false 3. if( Float.isNaN( x ) ) x = 0.0 ; // the right way to detect NaN
This example shows the right way (line 3) and one of the many wrong ways (line 2) to detect the NaN value.
Floating-Point Math and strictfp
The strictfp modifier is related to the way floating-point calculations are carried out, as affected by specialized math coprocessors. Recall that float and double primitives use 32 and 64 bits, respectively, to store values. However, some floating-point coprocessors can use internal representations of numbers that use more bits for the intermediate results of calculation. These processors produce results that are more accurate but differ slightly from what you would get if every intermediate calculation result were forced back to a 32- or 64-bit representation.
Normally, you would want to use the most accurate results possible, but this means that a calculation on one Java Virtual Machine (JVM) could produce a result that is slightly different from the same calculation on another JVM. Of course, this is contrary to the spirit of Java. Starting in Java 1.2, the strictfp modifier has been available so you can force floating-point math to reduce all intermediate results to the standard 32- or 64-bit representation, ensuring that calculations produce the same results on all JVMs.
When used as a method modifier, strictfp ensures that all calculations in the method follow the strict calculation rules. When used as a class modifier, strictfp forces all methods in a class to follow strict calculation rules.