<< Home

Chapter 3: Operators and Expressions

Operators are the symbols and keywords that allow you to perform calculations, make comparisons, and manipulate data in Java. They are the building blocks of expressions, which in turn are the heart of almost every statement you write. Understanding how operators work, how they interact, and how expressions are evaluated is essential to mastering the flow of logic in any Java program.

In this chapter you will explore Java’s full set of operators: arithmetic and assignment, comparison and logical, bitwise and shift, and the rules that determine how they combine through precedence and associativity. You will also learn how Java automatically promotes types during mixed calculations and how to control conversions explicitly through casting.

Operators work on values and variables, while expressions combine them into units of meaning that the compiler can evaluate. For example, the simple line int total = 3 + 4; contains both assignment and arithmetic operators, forming an expression that computes a value and stores it in a variable. Java evaluates such expressions from left to right according to strict precedence rules, ensuring consistent and predictable behavior across all platforms.

💡 Think of expressions as the “sentences” of Java’s language and operators as its “verbs.” Once you understand how operators connect values and variables, you can express almost any computation or decision cleanly and efficiently.

As you progress through this chapter, you will see how operators shape both the arithmetic and logical structure of programs. By the end, you will not only know what each operator does, but also how to combine them safely, read them fluently, and recognize subtle effects such as type promotion and evaluation order that influence every Java expression you write.

⚠️ Operator misuse is one of the most common sources of subtle bugs. A misplaced parenthesis, an unexpected type conversion, or confusion between = and == can change the logic of a program completely. Understanding the order and meaning of operators prevents these errors before they happen.

Arithmetic and assignment operators

Arithmetic and assignment operators are the core tools for performing mathematical operations and storing results in variables. They handle everything from simple addition and subtraction to compound updates that combine computation and assignment in one concise expression.

Basic arithmetic operators

Java supports the standard arithmetic operators found in most programming languages. These work on numeric types such as int, double, float, and long.

OperatorMeaningExample
+Addition5 + 2 → 7
-Subtraction5 - 2 → 3
*Multiplication5 * 2 → 10
/Division5 / 2 → 2 (integer division)
%Remainder (modulus)5 % 2 → 1

Division between integers always produces an integer result. If either operand is a floating-point value, the result will be a floating-point number:

int a = 5 / 2;      // 2
double b = 5 / 2.0; // 2.5

Unary operators can also change the sign or increment values directly:

OperatorDescriptionExample
+Unary plus (no effect)+x
-Unary minus (negates value)-x
++Increment by 1x++ or ++x
--Decrement by 1x-- or --x

The prefix and postfix forms of ++ and -- differ in when the value changes. Prefix updates the variable before its value is used; postfix updates it after.

int x = 5;
System.out.println(++x); // 6 (prefix: increments, then prints)
System.out.println(x++); // 6 (postfix: prints, then increments)
System.out.println(x);   // 7
💡 Avoid overusing increment or decrement operators inside complex expressions. Using them in multiple places within the same statement can make code difficult to read and predict.

Assignment operators

Assignment operators store values in variables. The simplest is the = operator, which copies the result of an expression into a variable. Compound assignment operators combine an operation with assignment, simplifying repeated updates.

Operator Equivalent to       Example
=—x = 5
+=x = x + valuex += 5
-=x = x - valuex -= 5
*=x = x * valuex *= 5
/=x = x / valuex /= 5
%=x = x % valuex %= 5

Compound assignments are especially useful for counters, accumulators, or iterative calculations.

int total = 10;
total += 5;  // total = 15
total *= 2;  // total = 30
⚠️ In compound assignments, Java may automatically convert the result to the type of the left-hand variable. For example, adding a double to an int using += will silently cast the result back to int, which may lose precision.

Order of evaluation

Arithmetic expressions are evaluated according to standard mathematical precedence. Multiplication, division, and modulus come before addition and subtraction. Parentheses can be used to override this order.

int result = 2 + 3 * 4;    // 14
int fixed  = (2 + 3) * 4;  // 20

Assignments, by contrast, are evaluated from right to left, meaning the value on the right-hand side is always computed before it is stored in the variable on the left.

int x = 5;
int y = x = 10;  // both x and y become 10
💡 Parentheses are your friend. Even when not strictly required, using them makes the intent of complex expressions clear to both the compiler and future readers.

Comparison and logical operators

Comparison and logical operators allow Java programs to make decisions. They evaluate relationships between values and combine true or false results into meaningful conditions for control structures such as if, while, and for. Together, they form the foundation of conditional logic.

Comparison operators

Comparison (or relational) operators compare two values and produce a boolean result, either true or false. These operators can be used with numeric, character, and some object types such as String (when compared with specific methods like equals()).

Operator MeaningExample
==Equal to5 == 5 → true
!=Not equal to5 != 3 → true
>Greater than5 > 3 → true
<Less than3 < 5 → true
>=Greater than or equal to5 >= 5 → true
<=Less than or equal to3 <= 4 → true

These operators work reliably for primitive types. When comparing object references (for example, two String values), == checks whether both references point to the same object in memory, not whether their contents match. To compare object content, use the equals() method instead.

String a = "Java";
String b = new String("Java");

System.out.println(a == b);       // false (different objects)
System.out.println(a.equals(b));  // true  (same text)
💡 Always use equals() or equalsIgnoreCase() when comparing strings or other objects for value equality. The == operator only checks if both variables refer to the same memory location.

Logical operators

Logical operators combine or invert boolean values. They are used to form complex conditions in control flow statements. Each operator returns true or false depending on the logical relationship between operands.

OperatorNameDescriptionExample
&&Logical ANDTrue only if both operands are true(x > 0 && y > 0)
||Logical ORTrue if either operand is true(x > 0 || y > 0)
!Logical NOTInverts a boolean value!ready

Java uses short-circuit evaluation for && and ||. This means that evaluation stops as soon as the outcome is known. For example, in (x > 0 && y / x > 2), if x is not greater than zero, the second expression is never executed, preventing a potential division-by-zero error.

int x = 0;
int y = 5;
if (x != 0 && y / x > 1) {
  System.out.println("Safe to divide");
}
// No error because the second condition is skipped
💡 Use && and || rather than their non-short-circuit counterparts (& and |) unless you specifically need both sides of the expression to evaluate for side effects.

Combining comparison and logic

Conditions often combine comparison and logical operators to create meaningful decisions. Parentheses can group expressions and control evaluation order, improving clarity.

int age = 25;
boolean hasLicense = true;

if (age >= 18 && hasLicense) {
  System.out.println("You may drive.");
}

Logical operators can also be nested or chained to handle more complex decision trees:

boolean eligible = (age >= 18 && age <= 70) || hasLicense;

This expression evaluates to true if the person is between 18 and 70 years old, or already holds a valid license.

⚠️ Because of short-circuit evaluation, expressions with side effects (such as incrementing a variable or calling a method) can behave unexpectedly if the second operand never executes. Keep your conditions free of side effects whenever possible.

Ternary conditional operator

The ternary operator (?:) is a compact form of if that selects one of two values based on a condition. It is useful for short expressions where full control structures would add clutter.

int age = 20;
String access = (age >= 18) ? "granted" : "denied";
System.out.println("Access " + access);

The expression before the question mark is the condition. If it evaluates to true, the expression immediately after the ? is chosen; otherwise, the one after the : is used.

💡 The ternary operator can be nested, but this quickly reduces readability. Use it only for simple decisions, and prefer an explicit if statement for anything more complex.

Bitwise and shift operators

Bitwise and shift operators work at the level of individual bits within integer types such as byte, short, int, and long. They are used for low-level programming tasks, including data encoding, cryptography, graphics, and performance-sensitive computations. Although they may seem specialized, understanding them deepens your knowledge of how Java represents numbers internally.

Bitwise operators

Bitwise operators treat their operands as binary values and perform logical operations on each corresponding bit. The result is another integer whose bits reflect the chosen operation.

OperatorNameDescriptionExample
&Bitwise AND1 if both bits are 15 & 3 → 1
|Bitwise OR1 if either bit is 15 | 3 → 7
^Bitwise XOR1 if bits differ5 ^ 3 → 6
~Bitwise NOTInverts all bits~5 → -6

To understand how these work, it helps to visualize binary representations:

int a = 5;  // 0101
int b = 3;  // 0011

System.out.println(a & b); // 0001 → 1
System.out.println(a | b); // 0111 → 7
System.out.println(a ^ b); // 0110 → 6
System.out.println(~a);    // 1010 → -6 (two’s complement)

These operations are purely bitwise and do not perform logical short-circuiting as && and || do. Each bit position is compared independently.

💡 Bitwise operators are extremely efficient because they act directly on binary data. They are often used for compact flags, masks, or toggles where multiple boolean states are stored in a single integer.

Bit masking and flags

A common use of bitwise operators is creating and testing bit masks. A mask is an integer where specific bits represent on/off states. You can combine masks with |, test with &, and toggle with ^.

final int READ  = 1;                            // 0001
final int WRITE = 2;                            // 0010
final int EXEC  = 4;                            // 0100

int permissions = READ | WRITE;                 // 0011

boolean canWrite = (permissions & WRITE) != 0;
System.out.println(canWrite);                   // true

permissions ^= WRITE;                           // toggle WRITE
System.out.println((permissions & WRITE) != 0); // false

By combining bit masks, you can store multiple binary states efficiently within a single variable. This approach is common in systems and game programming, where performance and memory layout are critical.

⚠️ Always ensure that masks use distinct bit positions (1, 2, 4, 8, 16, and so on). Reusing a bit for different meanings will make combinations impossible to interpret correctly.

Shift operators

Shift operators move the bits of a number left or right. Shifting left effectively multiplies by powers of two, while shifting right divides by powers of two. Java provides three shift operators:

OperatorNameDescriptionExample
<<Left shiftShifts bits left, filling with zeros5 << 1 → 10
>>Arithmetic right shiftShifts bits right, preserving sign bit-8 >> 2 → -2
>>>Logical right shiftShifts bits right, filling with zeros (no sign)-8 >>> 2 → large positive value

The left shift (<<) multiplies by two for each shift position, while the arithmetic right shift (>>) divides by two and keeps the sign for negative numbers. The logical right shift (>>>) fills empty bits with zeros, treating the number as unsigned.

int n = 5;   // 00000101
System.out.println(n << 1);  // 10  (00001010)
System.out.println(n >> 1);  // 2   (00000010)

int m = -8;  // 11111000
System.out.println(m >> 2);  // -2  (11111110)
System.out.println(m >>> 2); // 1073741822 (sign bit cleared)
💡 Bit shifting is faster than arithmetic multiplication or division by powers of two, but the difference is rarely significant on modern hardware. Use it only when clarity and intent justify it.

Practical uses of bitwise operations

Bitwise and shift operations appear throughout performance-oriented and embedded Java applications. Some common examples include:

// Combine RGB color values into a single 32-bit integer
int r = 120, g = 200, b = 255;
int color = (r << 16) | (g << 8) | b;
System.out.printf("0x%06X%n", color); // 0x78C8FF
⚠️ When using shift operations, remember that only the lower five bits of the shift count are used for 32-bit integers and the lower six bits for 64-bit longs. Shifting by more than the bit width wraps around and produces unexpected results.

Operator precedence and associativity

When an expression contains multiple operators, Java follows a strict set of rules to determine the order in which operations are evaluated. These rules are known as operator precedence. Operators with higher precedence are evaluated before those with lower precedence. When operators have the same precedence, associativity determines the direction in which they are applied (left-to-right or right-to-left).

Understanding precedence and associativity ensures that expressions behave as intended. Without parentheses to clarify order, even a small assumption can lead to unexpected results.

int result = 2 + 3 * 4;   // 14, not 20 (multiplication first)
int fixed  = (2 + 3) * 4; // 20 (parentheses change the order)
💡 When in doubt, use parentheses. They not only guarantee the correct order of evaluation but also make your code clearer to anyone reading it later.

Highest to lowest precedence

OperatorDescription
()Parentheses for grouping and method calls
[], .Array indexing and member access
++, --Postfix increment and decrement
++, --, +, -, ~, !Prefix increment/decrement, unary plus/minus, bitwise NOT, logical NOT
(), new, castObject creation and type casting
*, /, %Multiplication, division, modulus
+, -Addition and subtraction, or string concatenation
<<, >>, >>>Bitwise shift operators
<, <=, >, >=, instanceofRelational comparisons and type checking
==, !=Equality and inequality comparisons
&Bitwise AND
^Bitwise XOR
|Bitwise OR
&&Logical AND
||Logical OR
? :Ternary conditional operator
=, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=, >>>=Assignment and compound assignment (right-associative)
,Comma operator (evaluates expressions in sequence)

Associativity rules

Most operators in Java are evaluated from left to right. However, a few (notably assignment and the ternary conditional operator) are right-associative. The following table summarizes the general pattern:

AssociativityOperators
Left to right* / % + - << >> >>> < <= > >= == != & ^ | && ||
Right to left++ -- + - ~ ! ? : = += -= *= /= %= &= |= ^= <<= >>= >>>=

Right-associative operators are evaluated from the innermost right-hand expression first. For example:

int a = 2;
int b = 3;
int c = 4;
int result = a = b = c;  // assigns 4 to b, then to a
System.out.println(a);   // 4

Parentheses and clarity

Even though Java’s precedence rules are consistent, experienced developers rely on parentheses to make code readable. This not only avoids mistakes but also signals intention clearly to others maintaining the code.

boolean test = (x > 5 && y < 10) || (z == 0);

Such grouping makes the logic obvious at a glance. The compiler would interpret the same expression correctly even without parentheses, but human readers might not.

⚠️ Relying solely on operator precedence for complex expressions is risky. Parentheses are free, explicit, and self-documenting. Use them liberally to express intent rather than relying on memory of precedence tables.

Type promotion and casting

When expressions in Java involve values of different types, the language automatically converts them to a common type before performing the operation. This process is called type promotion. It ensures that the result is accurate and consistent, even when mixing integers and floating-point numbers. When automatic promotion is not what you want, you can use casting to explicitly convert one type to another.

Automatic type promotion

Java promotes smaller numeric types to larger ones to prevent data loss during operations. For example, if you add an int to a double, the int is automatically promoted to double before the addition occurs. The general promotion order (from smallest to largest) is:

byte → short → int → long → float → double

Character values are also promoted to integers when used in arithmetic expressions, since char is stored as a 16-bit Unicode number.

int result = 'A' + 1;  // 'A' is 65 → result is 66

When multiple different types appear in a single expression, Java promotes them to the widest type involved:

int i = 5;
double d = 2.5;
double total = i + d;  // int promoted to double
💡 Promotion only ever moves upward in the type hierarchy. Java never performs implicit narrowing (for example, from double to int) because that could lose precision or overflow.

Integer division and promotion

When dividing integers, Java performs integer division even if the result is not whole. To obtain a floating-point result, at least one operand must be a floating-point type.

int a = 5, b = 2;
System.out.println(a / b);    // 2 (integer division)
System.out.println(a / 2.0);  // 2.5 (automatic promotion to double)

This rule is important to remember when writing formulas, as the wrong type can silently change the outcome of your calculations.

⚠️ Mixed-type arithmetic is resolved by promoting all operands to the widest type involved. Be careful when mixing float and double, since rounding differences can appear when comparing results later.

Explicit casting

Casting is the process of manually converting one type into another. It is used when you want to control how data is interpreted or when you must narrow a value to a smaller type. Casting is written by placing the target type in parentheses before the value or expression.

double value = 9.8;
int rounded = (int) value;  // converts 9.8 → 9

Because narrowing conversions can lose data, Java requires an explicit cast to make sure you are aware of the risk.

long big = 12345678900L;
int small = (int) big;      // value truncated to fit into int
System.out.println(small);  // unpredictable numeric result
💡 Use explicit casting only when necessary, and document why the conversion is safe. Automatic promotion is generally safer and easier to maintain.

Casting between object types

In addition to primitive conversions, Java allows reference casting between related object types. This occurs within inheritance hierarchies, where a subclass object can be treated as its superclass (upcasting) or vice versa (downcasting).

Object obj = "Java";         // upcast: String → Object
String text = (String) obj;  // downcast: Object → String

Upcasting is always safe because every subclass instance is also an instance of its superclass. Downcasting, however, must be done carefully. If the object is not actually an instance of the target type, a ClassCastException will occur at runtime.

Object num = Integer.valueOf(42);
String s = (String) num;  // runtime error
⚠️ Before performing a downcast, use the instanceof operator to check the type safely. This prevents runtime exceptions and improves code reliability.

Type inference with var

Since Java 10, the var keyword allows the compiler to infer a variable’s type from its initializer. It does not change the rules of promotion or casting, but it makes declarations more concise while maintaining type safety.

var count = 10;       // inferred as int
var name = "Java";    // inferred as String
var ratio = 5.0 / 2;  // inferred as double

The inferred type is still static and fixed at compile time. You cannot later assign a different type to the same variable.

💡 var can make code shorter, but it should be used only when the type is obvious from context. Clarity should always take priority over brevity.

Together, type promotion, casting, and inference form the backbone of Java’s strong and predictable type system. They let the compiler detect errors early while giving developers the control needed for precision when it matters.