Evaluate Java expressions with operators

how-to
11 Jun 202436 mins
JavaProgramming LanguagesSoftware Development

Here's everything you need to know about Java operators and operator types, and how to use them to write expressions for your Java programs.

Three building blocks with question marks.
Credit: Ka Iki/Shutterstock

In this tutorial, you will learn how to write expressions for your Java programs. In many cases, you’ll use operators to write your Java expressions, and there are many operator types to know how to use. I’ll briefly introduce Java’s operator types, including the additive, bitwise, logical, conditional, shift, and equality types and their operands. You’ll also learn about operator overloading and operator precedence, and you’ll see a demonstration of primitive-type conversion. I’ll conclude with a small Java program that you can use to practice primitive-type conversions on your own.

What you’ll learn in this Java tutorial

  • What is a Java expression?
  • How to write simple expressions
  • How to write compound expressions
  • About Java operators and operands
  • All the operator types in Java, with examples
  • About operator precedence and associativity
  • How to work with primitive-type conversions
download
Download the source code for example applications in this tutorial. Created by Jeff Friesen.

What is a Java expression?

Expressions are combinations of literals, method calls, variable names, and operators. Java applications evaluate expressions. Evaluating an expression produces a new value that can be stored in a variable, used to make a decision, and more.

How to write simple expressions

A simple expression is a literal, variable name, or method call. No operators are involved. Here are some examples of simple expressions:


52                         // integer literal
age                        // variable name
System.out.println("ABC"); // method call
"Java"                     // string literal
98.6D                      // double precision floating-point literal
89L                        // long integer literal

A simple expression has a type, which is either a primitive type or a reference type. In these examples, 52 is a 32-bit integer (int); System.out.println("ABC"); is void (void) because it returns no value; "Java" is a string (String); 98.6D is a 64-bit double-precision floating-point value (double); and 89L is a 64-bit long integer (long). We don’t know age‘s type.

Use jshell to experiment

You can easily try out these and other simple expressions using jshell. For example, enter 52 at the jshell> prompt and you’ll receive something like the following output:

$1 ==> 52

$1 is the name of a scratch variable that jshell creates to store 52. (Scratch variables are created whenever literals are entered.) Execute System.out.println($1) and you’ll see 52 as the output.

You can run jshell with the -v command-line argument (jshell -v) to generate verbose feedback. In this case, entering 52 would result in the following message, revealing that scratch variable $1 has int (32-bit integer) type:

|  created scratch variable $1 : int

Next, try entering age. In this case, you’ll probably receive an error message that the symbol was not found. The Java Shell assumes that age is a variable, but it doesn’t know its type. You would have to include a type; for example, see what happens if you enter int age.

How to write compound expressions

A compound expression consists of one or more simple expressions integrated into a larger expression via an operator, which is a sequence of instructions symbolically represented in source code. The operator transforms its expression operand(s) into another value. For example, in 6 * 5, the multiplication operator (*) transforms operands 6 and 5 into 30.

Compound expressions can be combined into larger expressions. For example, 6 * 5 + 10 presents compound expression 6 * 5 and a compound expression consisting of their product, addition operator +, and the number 10. The order of evaluation (multiply first and then add) is dictated by Java’s rule of precedence, which we’ll get to shortly.

About Java operators and operands

Java’s operators are classified by their number of operands:

  • A unary operator has one operand, for example, unary minus (e.g., -5).
  • A binary operator has two operands, examples are multiplication and addition.
  • A ternary operator has three operands; an example is the conditional operator (?:).

Java’s operators are also classified by position:

  • A prefix operator is a unary operator that precedes its operand (e.g., -5).
  • A postfix operator is a unary operator that follows its operand (e.g., age++; — add 1 to age‘s numeric value).
  • An infix operator is a binary or ternary operator between the operator’s operands (e.g., age + 5).

Another jshell example

I’ll introduce more operators in the following sections, where I present examples in the form of applications. You could also try out these operators with jshell, like so:

jshell> 6 + 2
$1 ==> 8

jshell> 7 * $1
$2 ==> 56

In this case, we first enter the expression 6 + 2, which jshell evaluates, assigning the resulting 8 to scratch variable $1. Next, we multiply $1 by 7, which stores 56 in scratch variable $2. This example demonstrates that you can use scratch variables in Java expressions.

Operator types in Java

Next, we’ll tour all the operator types in Java. After introducing each operator type, I’ll present an example that shows you how it’s used in Java expressions.

Additive operators

The additive operators increase or decrease a numeric value through addition and subtraction. Additive operators include addition (+), subtraction (-), postdecrement (--), postincrement (++), predecrement (--), and preincrement (++). String concatenation (+) is also considered to be additive. Here’s a formal definition for each of these operators:

  • Addition: Given operand1 + operand2, where each operand must be of character or numeric type, add operand2 to operand1 and return the sum. Example: 4 + 6.
  • Subtraction: Given operand1 - operand2, where each operand must be of character or numeric type, subtract operand2 from operand1 and return the difference. Example: 4 - 6.
  • Postdecrement: Given variable--, where variable must be of character or numeric type, subtract 1 from variable‘s value (storing the result in variable) and return the original value. Example: x--;.
  • Postincrement: Given variable++, where variable must be of character or numeric type, add 1 to variable‘s value (storing the result in variable) and return the original value. Example: x++;.
  • Predecrement: Given --variable, where variable must be of character or numeric type, subtract 1 from its value, store the result in variable, and return the new decremented value. Example: --x;.
  • Preincrement: Given ++variable, where variable must be of character or numeric type, add 1 to its value, store the result in variable, and return the new incremented value. Example: ++x;.
  • String concatenation: Given operand1 + operand2, where at least one operand is of String type, append operand2‘s string representation to operand1‘s string representation and return the result. Example: "A" + "B".

The addition, subtraction, postdecrement, postincrement, predecrement, and preincrement operators can generate values that overflow the limits of the result type. For example, adding two large positive 64-bit integer values can produce a value that cannot be represented in 64 bits. The resulting overflow is not detected or reported by Java’s additive operators.

Example application: Additive operators

Listing 1 presents a small application for playing with Java’s additive operators.

Listing 1. Additive operators in Java (AddOp.java)

class AddOp
{
   public static void main(String[] args)
   {
      System.out.println(125 + 463);
      System.out.println(2.0 - 6.3);
      int age = 65;
      System.out.println(age);
      System.out.println(age--);
      System.out.println(age++);
      System.out.println(--age);
      System.out.println(++age);
      System.out.println("A" + "B");
   }
}

See Elementary Java language features for an introduction to using the JDK’s javac tool to compile Java source code and the java tool to run the resulting application. Execute the following command to compile Listing 1:

javac AddOp.java

Assuming successful compilation, you should observe an AddOp.class file in the current directory. Execute the following command to run it:

java AddOp

AddOp responds by producing the following output:

588
-4.3
65
65
64
64
65
AB

Studying this output offers insight into the postincrement, postdecrement, preincrement, and predecrement operators. For postincrement/postdecrement, age‘s current value is output before the increment/decrement operation. For preincrement/predecrement, the operation is performed and its result is stored in age, and then age‘s new value is output.

Array index operator

The array index operator ([]) accesses an array element by providing the element’s index (position). This operator is placed after the array variable’s name, as in grades[0] (access the first element in the array assigned to grades; the first element is stored at index 0). Here’s a formal definition:

Given variable[index], where index must be of integer (int) type, read a value from or store a value into variable‘s storage element at location index. Example: temperatures[1]

The value passed to index is a 32-bit integer that is either 0 or a positive value ranging to one less than the array’s length, which is indicated by appending .length to the name of the array. For example, grades.length returns the number of elements in the array assigned to grades.

Example application: Array index operator

Listing 2 presents the source code to an example application that lets you play with the array index operator.

Listing 2. Array index operator in Java (ArrayIndexOp.java)

class ArrayIndexOp
{
   public static void main(String[] args)
   {
      int[] grades = { 89, 90, 68, 73, 79 };
      System.out.println(grades[1]);
      grades[1] = 91;
      System.out.println(grades[1]);
      int index = 4;
      System.out.println(grades[index]);
      System.out.println(grades['C' - 'A']);
//      System.out.println(grades[1D]);
   }
}

Listing 2 is somewhat more interesting than Listing 1. After creating a five-element, one-dimensional array of integers (via an array initializer) and assigning the array’s reference to grades, main() proceeds to access various elements. Two items are of special interest:

  • The array index operator’s index must ultimately be a 32-bit integer (0 or a positive value). You can specify the name of an integer variable (e.g., index), which contains the index value, as the index.
  • You can specify a calculation involving character literals. (Later in this tutorial I’ll introduce type conversions, and you’ll discover why 'C' - 'A' produces an integer (2), which serves as a valid index.)

The final example, which passes 1D as an index to the array index operator, is commented out because it will not compile. If you uncomment the line and attempt to compile Listing 2, you will receive an error message about incompatible types: “possible lossy conversion from double to int..”

Compile Listing 2 (javac ArrayIndexOp.java) and run the application (java ArrayIndexOp). You should observe the following output:

90
91
79
68

Assignment operators

The assignment operator (=) assigns an expression’s value to a variable (e.g., i = 6;), including an array element (e.g., x[0] = 15;). The expression and variable must be assignment compatible, meaning their types must agree. For example, you cannot assign a string literal to an integer variable. I’ll explain more about this when we discuss type conversions.

The compound assignment operators (+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=, >>>=) evaluate expressions and assign the results to variables in one step. Each expression and variable must be assignment compatible. Each operator serves as a useful shortcut. For example, instead of specifying x = x + 3;, you can specify the shorter and equivalent x += 3;.

Bitwise operators

The bitwise operators modify the binary values of their operands, which must be of an integer (byte, short, int, or long) or character type. These operators include bitwise AND (&), bitwise complement (~), bitwise exclusive OR (^), and bitwise inclusive OR (|); and are formally defined below:

  • Bitwise AND: Given operand1 & operand2, where each operand must be of character or an integer type, bitwise AND their corresponding bits and return the result. A result bit is set to 1 when each operand’s corresponding bit is 1. Otherwise, the result bit is set to 0. Example: 1 & 0.
  • Bitwise complement: Given ~operand, where operand must be of character or an integer type, flip operand‘s bits (1s to 0s and 0s to 1s) and return the result. Example: ~1.
  • Bitwise exclusive OR: Given operand1 ^ operand2, where each operand must be of character or an integer type, bitwise exclusive OR their corresponding bits and return the result. A result bit is set to 1 when one operand’s corresponding bit is 1 and the other operand’s corresponding bit is 0. Otherwise, the result bit is set to 0. Example: 1 ^ 0.
  • Bitwise inclusive OR: Given operand1 | operand2, which must be of character or an integer type, bitwise inclusive OR their corresponding bits and return the result. A result bit is set to 1 when either (or both) of the operands’ corresponding bits is 1. Otherwise, the result bit is set to 0. Example: 1 | 0.

Example application: Bitwise operators

Listing 3 presents the source code to a BitwiseOp application that lets you play with the bitwise operators.

Listing 3. Bitwise operators in Java (BitwiseOp.java)

class BitwiseOp
{
   public static void main(String[] args)
   {
      short x = 0B0011010101110010;
      short y = 0B0110101011101011;

      System.out.println(x & y);
      System.out.println(~x);
      System.out.println(x ^ y);
      System.out.println(x | y);
   }
}

Listing 3’s main() method initializes a pair of short integer variables and subsequently uses the bitwise operators to produce new values by operating on their bits; these values are then output.

Compile Listing 3 (javac BitwiseOp.java) and run the application (java BitwiseOp). You should observe the following output:

8290
-13683
24473
32763

Because it’s difficult to see each operator’s effect on its operands, consider the binary equivalent of the previous operators below. (You’ll understand why each binary number is 32 bits long instead of 16 when I introduce type conversions.)

00000000000000000010000001100010
11111111111111111100101010001101
00000000000000000101111110011001
00000000000000000111111111111011

Cast operator

The cast operator(type)–attempts to convert the type of its operand to type. You can convert from one primitive type to another primitive type or from one reference type to another reference type, but not from primitive type to reference type or vice versa.

For example, to convert double precision floating-point value 1.0 to its 32-bit integer equivalent, specify (int) 1.0. Also, to convert circle (of type Circle) to its Shape supertype, specify (Shape) circle. There will be more to learn about cast when we get into type conversions later in this tutorial. The cast operator is also important for inheritance in Java.

Conditional operators

The conditional operators conditionally evaluate Boolean expressions, which are expressions that have Boolean type and evaluate to true or false. These operators include conditional (?:), conditional AND (&&), and conditional OR (||); and are formally defined below:

  • Conditional: Given operand1 ? operand2 : operand3, where operand1 must be of Boolean type, return operand2 when operand1 is true or operand3 when operand1 is false. The types of operand2 and operand3 must agree. Example: boolean status = true; int statusInt = (status) ? 1 : 0;.
  • Conditional AND: Given operand1 && operand2, where each operand must be of Boolean type, return true when both operands are true. Otherwise, return false. When operand1 is false, operand2 isn’t evaluated because the entire expression will be false anyway. This is known as short-circuiting. Example: true && false.
  • Conditional OR: Given operand1 || operand2, where each operand must be of Boolean type, return true when at least one operand is true. Otherwise, return false. When operand1 is true, operand2 isn’t evaluated because the entire expression will be true anyway. This is known as short-circuiting. Example: true || false.

Example application: Conditional operators

Listing 4 presents the source code to a CondOp application that lets you play with the conditional operators.

Listing 4. Conditional operators in Java (CondOp.java)

class CondOp
{
   public static void main(String[] args)
   {
      boolean sold_more_than_100_units = true;
      int bonus_dollars = (sold_more_than_100_units) ? 50 : 0;
      System.out.println(bonus_dollars);

      System.out.println(true && true);
      System.out.println(true && false);
      System.out.println(false && true);
      System.out.println(false && false);

      System.out.println(true || true);
      System.out.println(true || false);
      System.out.println(false || true);
      System.out.println(false || false);

      int x = 0;
      boolean status = true && ++x == 0;
      System.out.println(x);
      status = false && ++x == 0;
      System.out.println(x);
      status = true || ++x == 0;
      System.out.println(x);
      status = false || ++x == 0;
      System.out.println(x);
   }
}

Because of short-circuiting, && and || won’t always evaluate their right operands. Although short-circuiting can improve performance somewhat because only one expression is evaluated, it can also be a source of bugs when a side-effect (code that is executed as a byproduct of expression evaluation) is involved.

Listing 4 presents side-effects in which variable x is preincremented. Preincrement occurs for expressions true && ++x == 0 and false || ++x == 0. However, this variable isn’t incremented for expressions false && ++x == 0 and true || ++x == 0 because there’s no need to evaluate the right operands in these contexts.

Compile Listing 4 (javac CondOp.java) and run the application (java CondOp). You should observe the following output:

50
true
false
false
false
true
true
true
false
1
1
1
2

Equality operators

The equality operators compare their operands to determine if they are equal or unequal. These operators include equality (==) and inequality (!=). The former operator returns true when both operands are equal; the latter operator returns true when both operands are unequal. These operators are formally defined below:

  • Equality: Given operand1 == operand2, where both operands must be comparable (you cannot compare an integer with a Boolean value, for example), compare both operands for equality. Return true when these operands are equal. Otherwise, return false. Example: 'A' == 'a'.
  • Inequality: Given operand1 != operand2, where both operands must be comparable (you cannot compare a floating-point value with a string literal, for example), compare both operands for inequality. Return true when these operands are not equal. Otherwise, return false. Example: 'A' != 'a'.

You will need to be careful when comparing floating-point values because not all floating-point values can be represented accurately in memory. For example, 0.1 cannot be represented accurately. For this reason, some expressions involving these operands will return false when you think they should return true.

These operators can be used to compare primitive values or object references. However, you cannot compare a primitive value with an object reference. For example, you might have two Employee objects whose references are stored in e1 and e2. Expression e1 == e2 returns true only when variables e1 and e2 refer to the same Employee object.

String comparison is a little unusual. You can attempt to compare two string literals, as in "A" == "B". However, because string literals are really String objects, you are really comparing references to these objects and not comparing their characters. As a result, true returns only when both operands reference the same String object.

Example application: Equality operators

Listing 5 presents the source code to an EqualityOp application that lets you play with the equality operators.

Listing 5. Equality operators in Java (EqualityOp.java)

class EqualityOp
{
   public static void main(String[] args)
   {
      int x = 0;
      System.out.println(x == 0);
      System.out.println(x != 0);

      double d = 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1;
      System.out.println(d);
      System.out.println(d == 0.9);

      System.out.println("A" == "A");
      System.out.println("A" == "B");
      System.out.println("AB" == "A" + "B");
      String s = "B";
      System.out.println("AB" == "A" + s);
   }
}

Compile Listing 5 (javac EqualityOp.java) and run the application (java EqualityOp). You should observe the following output:

true
false
0.8999999999999999
false
true
false
true
false

The first two output lines are unsurprising given that x contains 0. The next two output lines reveal that 0.1 cannot be stored exactly in memory: summing nine instances of 0.1 doesn’t equal nine. For this reason, it’s unwise to control an iteration’s duration via an equality expression involving floating-point values.

The remaining output lines prove that Java creates exactly one String object for each unique string literal. For example, there is one String object for "A", another String object for "B", and a third String object for "AB". In "A" == "A", both "A" literals refer to the same "A" object, so true is the result. However, in "A" == "B", "A" and "B" refer to the "A" object and to a "B" object. Because of different references, false is the result. Next, in "AB" == "A" + "B", the string concatenation produces a reference to the same "AB" object as is referenced by literal "AB", so true is the result. Finally, String s = "B"; creates a new String object that contains B and whose reference is assigned to s. This object (and reference) is separate from the object (and reference) associated with "B". As a result, in "AB" == "A" + s, we end up with the "AB" String object’s reference being compared with a reference to a different String object containing AB, and this comparison results in false as the result. You’ll learn more about string comparison in a future tutorial.

Logical operators

The logical operators are the Boolean equivalent of the bitwise operators. Instead of working on the bit values of integral operands, they work on their Boolean operands. These operators include logical AND (&), logical complement (!), logical exclusive OR (^), and logical inclusive OR (|); and are formally defined below:

  • Logical AND: Given operand1 & operand2, where each operand must be of Boolean type, return true when both operands are true. Otherwise, return false. In contrast to conditional AND, logical AND doesn’t perform short-circuiting. Example: true & false.
  • Logical complement: Given !operand, where operand must be of Boolean type, flip operand‘s value (true to false or false to true) and return the result. Example: !false.
  • Logical exclusive OR: Given operand1 ^ operand2, where each operand must be of Boolean type, return true when one operand is true and the other operand is false. Otherwise, return false. Example: true ^ false.
  • Logical inclusive OR: Given operand1 | operand2, where each operand must be of Boolean type, return true when at least one operand is true. Otherwise, return false. In contrast to conditional OR, logical inclusive OR doesn’t perform short-circuiting. Example: true | false.

Example application: Logical operators

Listing 6 presents the source code to a LogicalOp application that lets you play with the logical operators.

Listing 6. Logical operators in Java (LogicalOp.java)

class LogicalOp
{
   public static void main(String[] args)
   {
      int x = 0;
      System.out.println(false & ++x == 0);
      System.out.println(x);
      System.out.println(!false);
      System.out.println(true ^ true);
      System.out.println(true ^ false);
      System.out.println(true | ++x == 0);
      System.out.println(x);
   }
}

Compile Listing 6 (javac LogicalOp.java) and run the application (java LogicalOp). You should observe the following output:

false
1
true
false
true
true
2

Member access operator

The member access operator (.) accesses class or object members (e.g., methods). For example, assuming that name is of type String and is initialized to a string, name.length() returns the length of that string. Essentially, this operator is accessing the length() member of the name object.

Java regards arrays as special objects with a single length member whose value (an int) denotes the number of elements in the array. For example, grades.length returns the length of (the number of elements in) the array that grades references. In other words, this operator is accessing the length member of the grades array.

Method call operator

The method call operator() — signifies that a method is being called and identifies the number, order, and types of expressions being passed to the method. For example, in System.out.println("Java");, () signifies that method println, which is a member of the System class’s out member, is being called with one argument: "Java".

Multiplicative operators

The multiplicative operators greatly increase or decrease a numeric value through the equivalent of multiple additions or subtractions (e.g., 4 times 3 is equivalent to adding three 4s, and 12 divided by 3 is equivalent to repeatedly subtracting 3 from 12 until the remainder is less than 3 (0, in this example). These operators include multiplication (*), division (/), and remainder (%); and are formally defined below:

  • Multiplication: Given operand1 * operand2, where each operand must be of character or numeric type, multiply operand1 by operand2 and return the product. Example: 4 * 3.
  • Division: Given operand1 / operand2, where each operand must be of character or numeric type, divide operand1 by operand2 and return the quotient. Example: 12 / 3.
  • Remainder: Given operand1 % operand2, where each operand must be of character or numeric type, divide operand1 by operand2 and return the remainder. Also known as the modulus operator. Example: 12 % 3.

The multiplication operator can generate a product that overflows the limits of the result type, and doesn’t detect and report an overflow. If you need to detect an overflow, you’ll want to work with the Math class’s multiplyExact() methods.

Example application: Multiplicative operators

Listing 7 presents the source code to a MulOp application that lets you play with the multiplicative operators.

Listing 7. Multiplicative operators in Java (MulOp.java)

class MulOp
{
   public static void main(String[] args)
   {
      System.out.println(64.0 * 3.0);
      System.out.println(64 / 3);
      System.out.println(64 % 3);
      System.out.println(10.0 / 0.0);
      System.out.println(-10.0 / 0.0);
      System.out.println(0.0 / 0.0);
      System.out.println(10 / 0);
   }
}

Listing 7 is fairly straightforward until you encounter the division-by-zero expressions. Dividing a numeric value by 0 (via the division or remainder operator) results in interesting behavior:

  • Dividing a floating-point/double precision floating-point value by 0 causes the operator to return one of the following special values: +infinity (the dividend is positive), -infinity (the dividend is negative), or NaN — Not a Number — (the dividend and divisor are both 0).
  • Dividing an integer value by integer 0 causes the operator to throw an ArithmeticException object.

Compile Listing 7 (javac MulOp.java) and run the application (java MulOp). You should observe the following output:

192.0
21
1
Infinity
-Infinity
NaN
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at MulOp.main(MulOp.java:11)

Object creation operator

The object creation operator (new) is used to create an object from a class or to create an array. This operator is formally defined below:

  • Given new identifier(argument list), allocate memory for object and call constructor specified as identifier(argument list). Example: new String("ABC").
  • Given new identifier[integer size], allocate a one-dimensional array of values. Example: new int[5].

To create a two-dimensional array, the syntax changes to identifier[integer size][integer size] (e.g., new double[5][5]). For additional dimensions, append an [integer size] per dimension.

See Classes and objects in Java for an introduction to creating objects and arrays.

Relational operators

The relational operators impose an ordering on their operands by determining which operand is greater, lesser, and so on. These operators include greater than (>), greater than or equal to (>=), less than (<), and less than or equal to (<=). Type checking (instanceof) is also considered to be relational. These operators are formally defined below:

  • Greater than: Given operand1 > operand2, where each operand must be of character or numeric type, return true when operand1 is greater than operand2. Otherwise, return false. Example: 65.3 > 22.5.
  • Greater than or equal to: Given operand1 >= operand2, where each operand must be of character or numeric type, return true when operand1 is greater than or equal to operand2. Otherwise, return false. Example: 0 >= 0.
  • Less than: Given operand1 < operand2, where each operand must be of character or numeric type, return true when operand1 is less than operand2. Otherwise, return false. Example: x < 15.
  • Less than or equal to: Given operand1 <= operand2, where each operand must be of character or numeric type, return true when operand1 is less than or equal to operand2. Otherwise, return false. Example: 0 <= 0.
  • Type checking: Given operand1 instanceof operand2, where operand1 is an object and operand2 is a class (or other user-defined type), return true when operand1 is an instance of operand2. Otherwise, return false.

Example application: Relational operators

Listing 8 presents the source code to a RelOp application that lets you play with the relational operators.

Listing 8. Relational operators in Java (RelOp.java)

class RelOp
{
   public static void main(String[] args)
   {
      int x = 10;
      System.out.println(x > 10);
      System.out.println(x >= 10);
      System.out.println(x < 10);
      System.out.println(x <= 10);
      System.out.println("A" instanceof String);
   }
}

Compile Listing 8 (javac RelOp.java) and run the application (java RelOp). You should observe the following output:

false
true
false
true
true

The final output line is interesting because it proves that a string literal (e.g., "A") is in fact a String object.

Shift operators

The shift operators let you shift an integral value left or right by a specific number of bit positions. These operators include left shift (<<), signed right shift (>>), and unsigned right shift (>>>); and are formally defined below:

  • Left shift: Given operand1 << operand2, where each operand must be of character or integer type, shift operand1‘s binary representation left by the number of bits that operand2 specifies. For each shift, a 0 is shifted into the rightmost bit and the leftmost bit is discarded. Only the five low-order bits of operand2 are used when shifting a 32-bit integer (to prevent shifting more than the number of bits in a 32-bit integer). Only the six low-order bits of operand2 are used when shifting a 64-bit integer (to prevent shifting more than the number of bits in a 64-bit integer). The shift preserves negative values. Furthermore, it’s equivalent to (but faster than) multiplying by a multiple of 2. Example: 3 << 2.
  • Signed right shift: Given operand1 >> operand2, where each operand must be of character or integer type, shift operand1‘s binary representation right by the number of bits that operand2 specifies. For each shift, a copy of the sign bit (the leftmost bit) is shifted to the right and the rightmost bit is discarded. Only the five low-order bits of operand2 are used when shifting a 32-bit integer (to prevent shifting more than the number of bits in a 32-bit integer). Only the six low-order bits of operand2 are used when shifting a 64-bit integer (to prevent shifting more than the number of bits in a 64-bit integer). The shift preserves negative values. Furthermore, it’s equivalent to (but faster than) dividing by a multiple of 2. Example: -5 >> 2.
  • Unsigned right shift: Given operand1 >>> operand2, where each operand must be of character or integer type, shift operand1‘s binary representation right by the number of bits that operand2 specifies. For each shift, a zero is shifted into the leftmost bit and the rightmost bit is discarded. Only the five low-order bits of operand2 are used when shifting a 32-bit integer (to prevent shifting more than the number of bits in a 32-bit integer). Only the six low-order bits of operand2 are used when shifting a 64-bit integer (to prevent shifting more than the number of bits in a 64-bit integer). The shift doesn’t preserve negative values. Furthermore, it’s equivalent to (but faster than) dividing by a multiple of 2. Example: 42 >>> 2.

Example application: Shift operators

Listing 9 presents the source code to a ShiftOp application that lets you play with the shift operators.

Listing 9. Shift operators in Java (ShiftOp.java)

class ShiftOp
{
   public static void main(String[] args)
   {
      System.out.println(1 << 8);
      System.out.println(8 >> 2);
      System.out.println(-1 >> 1);
      System.out.println(-1 >>> 1);
   }
}

Compile Listing 9 (javac ShiftOp.java) and run the application (java ShiftOp). You should observe the following output:

256
2
-1
2147483647

The output reveals that bit shifting is equivalent to multiplying or dividing by multiples of 2 (but is faster). The first output line is equivalent to the value derived from 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 and the second output line is equivalent to the value derived from 8 / 4. The final two output lines show the difference between preserving and not preserving the sign bit where negative values are concerned.

Unary minus/plus operators

The final operators that Java supports are unary minus (-) and unary plus (+). Unary minus returns the negative of its operand (e.g., -8 returns -8 and --8 returns 8), whereas unary plus returns its operand unchanged (e.g., +8 returns 8 and +-8 returns -8). Unary plus is not commonly used, but is included in Java’s set of operators for completeness.

Operator precedence and associativity

Earlier in this tutorial, I mentioned that Java’s rules of precedence (priority in order) dictate the order in which compound expressions are evaluated. For the common arithmetic operators (such as addition and multiplication), Java follows the established precedence convention of multiplication first, and then addition. The order of evaluation isn’t as clear for other operators. however. For example, how does Java evaluate 6 > 3 * 2? Should comparison precede multiplication, or vice-versa?

The following list shows you the precedence of Java’s operators. Operators closer to the top have higher precedence than operators lower down. In other words, operators higher up in the list are performed first. Operators that have the same precedence are listed on the same line. Also note that when the Java compiler encounters multiple operators with the same precedence in the same compound expression, it generates code to perform the operations according to their associativity, which I’ll explain next.

  • Array index, member access, method call, postdecrement, postincrement
  • Bitwise complement, cast, logical complement, object creation, predecrement, preincrement, unary minus, unary plus
  • Division, multiplication, remainder
  • Addition, string concatenation, subtraction
  • Left shift, signed right shift, unsigned right shift
  • Greater than, greater than or equal to, less than, less than or equal to, type checking
  • Equality, inequality
  • Bitwise AND, logical AND
  • Bitwise exclusive OR, logical exclusive OR
  • Bitwise inclusive OR, logical inclusive OR
  • Conditional AND
  • Conditional OR
  • Conditional
  • Assignment, compound assignment

You won’t always want to follow this order. For example, you might want to perform addition before multiplication. Java lets you violate precedence by placing subexpressions between round brackets (parentheses). A parenthesized subexpression is evaluated first. Parentheses can be nested, in which a parenthesized subexpression can be located within a parenthesized subexpression. In this case, the innermost parenthesized subexpression is evaluated first.

During evaluation, operators with the same precedence level (such as addition and subtraction) are processed according to their associativity, meaning how operators having the same precedence are grouped when parentheses are absent. For example, 10 * 4 / 2 is evaluated as if it was (10 * 4) / 2 because * and / are left-to-right associative operators. In contrast, a = b = c = 50; is evaluated as if it was a = (b = (c = 50)); (50 is assigned to c, c‘s value is assigned to b, and b‘s value is assigned to a–all three variables contain 50) because = is a right-to-left associative operator.

Most of Java’s operators are left-to-right associative. Right-to-left associative operators include assignment, bitwise complement, cast, compound assignment, conditional, logical complement, object creation, predecrement, preincrement, unary minus, and unary plus.

Example application: Precedence and associativity

I’ve created a small application for playing with precedence and associativity. Listing 10 presents its source code.

Listing 10. Precedence and associativity in Java (PA.java)

class PA
{
   public static void main(String[] args)
   {
      System.out.println(10 * 4 + 2);
      System.out.println(10 * (4 + 2));
      int a, b, c;
      a = b = c = 50;
      System.out.println(a);
      System.out.println(b);
      System.out.println(c);
   }
}

Compile Listing 10 (javac PA.java) and run the application (java PA). You should observe the following output:

42
60
50
50
50

In Listing 10, suppose I specified (a = b) = c = 50; instead of a = b = c = 50; because I want a = b to be evaluated first. How would the compiler respond — and why?

Primitive-type conversions

My previous binary and ternary operator examples presented operands having the same type (as an example, each of 6 * 5‘s operands is an int). In many cases, operands will not have the same type, and the Java compiler will need to generate bytecode that converts an operand from one type to another before generating bytecode that performs the operation. For example, when confronted by 5.1 + 8, the compiler generates bytecode to convert 32-bit integer 8 to its double precision floating-point equivalent followed by bytecode to add these double precision values. (In the example, the compiler would generate an i2d instruction to convert from int to double and then a dadd instruction to add the two doubles.)

How does the compiler know which operand to convert? For primitive-type operands, its choice is based on the following widening rules, which essentially convert from a type with a narrower set of values to a type with a wider set of values:

  • Convert byte integer to short integer, integer, long integer, floating-point, or double precision floating-point.
  • Convert short integer to integer, long integer, floating-point, or double precision floating-point.
  • Convert character to integer, long integer, floating-point, or double precision floating-point.
  • Convert integer to long integer, floating-point, or double precision floating-point.
  • Convert long integer to floating-point or double precision floating-point.
  • Convert floating-point to double precision floating-point.

Regarding expression 5.1 + 8, we can see that the compiler chooses to convert 8 to a double based on the rule for converting an integer to double precision floating-point. If it converted 5.1 to an int, which is a narrower type, information would be lost because the fractional part would be effectively truncated. Therefore, the compiler always chooses to widen a type so information isn’t lost.

These rules also help to explain why, in BitwiseOp.java, the binary values resulting from expressions such as System.out.println(~x); were 32 bits long instead of 16 bits long. The compiler converts the short integer in x to a 32-bit integer value before performing bitwise complement, via iconst_m1 and ixor instructions — exclusive OR the 32-bit integer value with 32-bit integer -1 and produce a 32-bit integer result. The JVM (Java Virtual Machine) provides no sconst_m1 and sxor instructions for performing bitwise complement on short integers. Byte integers and short integers are always widened to 32-bit integers.

Widening rules in practice

Earlier, I mentioned that you would discover why 'C' - 'A' in (grades['C' - 'A']) produces an integer index. Character literals 'C' and 'A' are represented in memory by their Unicode values, which are unsigned 16-bit integers. When it encounters this expression, the Java compiler generates an iconst_2 instruction, which is int value 2. In this case, no subtraction is performed because of optimization. However, if I replaced 'C' - 'A' with 'C' - base, where base is a char variable initialized to 'A', the compiler would generate the following bytecode:

bipush 65 ; Push 8-bit Unicode value for A, which is sign-extended to 32-bit int, onto stack.
istore_1  ; Pop this 32-bit value into a special int variable.
...
bipush 67 ; Push 8-bit Unicode value for C, which is sign-extended to 32-bit int, onto stack.
iload_1   ; Push 32-bit Unicode value for A onto stack.
isub      ; Subtract 65 (A) from 67 (C). Push 32-bit result onto stack.

The bi in bipush stands for the byte integer type; the i in istore_1, iload_1, and isub stands for the 32-bit integer type. The compiler has converted the expression into an int value. It makes sense to do so because of the close relationship between character literals (really, unsigned Unicode integers) and Java’s signed integers.

In addition to the previous widening rules, Java provides a special widening rule for use with String objects (e.g., string literals). When either operand of the string concatenation operator is not a string, that operand is converted to a string before the concatenation operation is performed. For example, when confronted with "X" + 3, the compiler generates code to convert 3 to "3" before performing the concatenation.

Using cast operators for type narrowing

Sometimes, you’ll need to deliberately narrow a type where information may be lost. For example, you’re drawing a mathematical curve with floating-point coordinates (for accuracy). Because the screen’s pixels use integer coordinates, you must convert from floating-point to integer before you can plot a pixel. In Java, we can use cast operators to narrow a type. Cast operators are available to perform the following primitive-type conversions:

  • Convert from byte integer to character.
  • Convert from short integer to byte integer or character.
  • Convert from character to byte integer or short integer.
  • Convert from integer to byte integer, short integer, or character.
  • Convert from long integer to byte integer, short integer, character, or integer.
  • Convert from floating-point to byte integer, short integer, character, integer, or long integer.
  • Convert from double precision floating-point to byte integer, short integer, character, integer, long integer, or floating-point.

For example, the (float) cast in float circumference = (float) 3.14159 * 10 * 10; is necessary to convert from double precision floating-point to floating-point.

A cast operator isn’t always necessary for the above primitive-type conversions. For example, consider conversion from 32-bit integer to 8-bit byte integer. You don’t need to supply a cast operation when assigning a 32-bit integer literal that ranges from -128 to 127 to a variable of byte integer type. For example, you could specify byte b = 100; and the compiler wouldn’t complain because no information is lost. (This is why I was previously able to specify short x = 0B0011010101110010;, where the binary literal is of 32-bit integer type, without requiring a (short) cast operator, as in short x = (short) 0B0011010101110010;.) However, if you specified int i = 2; byte b = i;, the compiler would complain because i could contain a value outside the valid range of integers that can be assigned to a byte integer variable.

Example application: Primitive-type conversions

A short application should help to clarify all of this theory. Check out Listing 11’s Convert source code.

Listing 11. Primitive-type conversions in Java (Convert.java)

class Convert
{
   public static void main(String[] args)
   {
      float f = 1000;
      System.out.println("f = " + f);
      long l = 5000;
      System.out.println("l = " + l);
      System.out.println("'C' - 'A' = " + ('C' - 'A'));
      char base = 'A';
      System.out.println("'C' - base = " + ('C' - base));
      int i = (int) 2.5;
      System.out.println(i);
      byte b = 25;
      System.out.println(b);
      b = (byte) 130;
      System.out.println(b);
      i = 2;
      b = (byte) i;
      System.out.println(b);
   }
}

Listing 11 reveals a good way to identify variables when outputting their values (for debugging or another purpose). Simply concatenate a variable (of arbitrary type) to a string label, as in "f = " + f.

Compile Listing 11 (javac Convert.java) and run the application (java Convert). You should observe the following output:

f = 1000.0
l = 5000
'C' - 'A' = 2
'C' - base = 2
2
25
-126
2

Conclusion

Java’s support for expressions is extensive, and there is a lot of theory to grasp. I encourage you to experiment with the example applications in this tutorial, and modify them to reinforce what you’ve learned about using operators to write compound Java expressions. Your practice will come in handy for the next tutorial, which wraps up this series on Java’s fundamental language features.

Exit mobile version