Operators #
An operator is a symbol that instructs the compiler to perform a specific operation on one or more values (operands). Java divides operators into seven categories based on their function. What sets this article apart from a mere list of symbols: each category comes with its unintuitive behaviors and anti-patterns that often cause bugs — like integer division that discards decimals, the difference between && and &, and the trap of ++ pre vs post increment. Understanding why each operator behaves the way it does is far more useful than just knowing what it does.
Arithmetic Operators #
Arithmetic operators perform basic mathematical operations. All of them work on primitive numeric types (int, long, double, etc.) and produce a numeric-typed value.
| Operator | Name | Example | Result |
|---|---|---|---|
+ | Addition | 10 + 3 | 13 |
- | Subtraction | 10 - 3 | 7 |
* | Multiplication | 10 * 3 | 30 |
/ | Division | 10 / 3 | 3 (not 3.33!) |
% | Remainder (modulo) | 10 % 3 | 1 |
+ | String concatenation | "a" + "b" | "ab" |
The Integer Division Trap #
Dividing two ints produces an int — the decimal part is discarded, not rounded. This is one of the most frequently unnoticed sources of bugs for new developers:
// ANTI-PATTERN: integer division discards the decimal
int a = 10, b = 3;
double result = a / b; // 3.0 — not 3.333...!
System.out.println(a / b); // 3 — the decimal is discarded
double avg = (1 + 2 + 3) / 3; // 2.0 — not 2.0!
// 1+2+3 = 6, then 6/3 = 2 (integer), only then converted to double
// CORRECT: cast one operand to double first
double result = (double) a / b; // 3.3333...
double result = a / (double) b; // 3.3333...
double avg = (1 + 2 + 3) / 3.0; // 2.0 — correct
double avg = (double)(1 + 2 + 3) / 3; // 2.0 — correct
Modulo on Negative Numbers #
% in Java follows the sign of the dividend (the number being divided), not the divisor:
System.out.println( 7 % 3); // 1
System.out.println(-7 % 3); // -1 — negative! (not 2)
System.out.println( 7 % -3); // 1
// If you need a result that's always positive (true modulo):
int result = ((-7) % 3 + 3) % 3; // 2 — always non-negative
Increment and Decrement Operators #
++ and -- have two forms with an important difference:
int x = 5;
// Post-increment: the old value is used first, THEN incremented
int a = x++; // a = 5, then x becomes 6
System.out.println(a); // 5
System.out.println(x); // 6
// Pre-increment: incremented first, THEN the new value is used
int b = ++x; // x becomes 7 first, then b = 7
System.out.println(b); // 7
System.out.println(x); // 7
// ANTI-PATTERN: ++/-- inside complex expressions — hard to read
int y = 5;
int z = y++ + ++y; // confusing: y=5, then 5 + 7 = 12? or?
// CORRECT: use ++/-- as standalone statements
y++;
z = y + y;
Assignment Operators #
Assignment operators store a value into a variable. All compound assignment operators (+=, -=, etc.) perform the operation and store the result at once — and they implicitly do a narrowing cast that can be surprising.
| Operator | Equivalent to | Example |
|---|---|---|
= | — | x = 10 |
+= | x = x + n | x += 5 |
-= | x = x - n | x -= 3 |
*= | x = x * n | x *= 2 |
/= | x = x / n | x /= 4 |
%= | x = x % n | x %= 3 |
&= | x = x & n | x &= 0xFF |
|= | x = x | n | x |= 0x01 |
^= | x = x ^ n | x ^= mask |
<<= | x = x << n | x <<= 2 |
>>= | x = x >> n | x >>= 1 |
Compound Assignment and Implicit Cast #
Compound assignment operators include an implicit narrowing cast that their manual equivalents don’t have:
byte b = 10;
// This is a COMPILE ERROR — int can't automatically fit into byte
b = b + 1; // ✗ ERROR: possible lossy conversion from int to byte
// This is CORRECT — += includes an implicit cast
b += 1; // ✓ equivalent to b = (byte)(b + 1)
// Implication: compound assignment can silently narrow
byte value = 100;
value += 100; // value = (byte)(100 + 100) = (byte)200 = -56! (overflow)
Comparison Operators #
Comparison operators compare two values and always produce a boolean. They’re used as conditions in if, while, for, and other boolean expressions.
| Operator | Meaning | Example | Result |
|---|---|---|---|
== | Equal to | 5 == 5 | true |
!= | Not equal to | 5 != 3 | true |
> | Greater than | 5 > 3 | true |
< | Less than | 5 < 3 | false |
>= | Greater than or equal | 5 >= 5 | true |
<= | Less than or equal | 5 <= 4 | false |
==on reference types compares addresses, not values. This was covered in the Data Types article, but it’s worth repeating because it causes bugs so often:String s1 = new String("hello"); String s2 = new String("hello"); s1 == s2; // false — different memory addresses s1.equals(s2); // true — same contents // For null-safe comparison specifically, use Objects.equals: Objects.equals(s1, null); // false, without NullPointerException Objects.equals(null, s2); // false, safe
Logical Operators #
Logical operators combine two boolean expressions into one boolean result. There are two versions: short-circuit (&&, ||) and non-short-circuit (&, |).
| Operator | Name | Description |
|---|---|---|
&& | AND (short-circuit) | true if both operands are true; stops at the left if the left is false |
|| | OR (short-circuit) | true if either one is true; stops at the left if the left is true |
! | NOT | Flips the boolean value |
& | AND (non-short-circuit) | Always evaluates both sides |
| | OR (non-short-circuit) | Always evaluates both sides |
^ | XOR | true if exactly one operand is true |
Short-Circuit Evaluation #
This is an important behavior often exploited in Java:
// && stops at the left operand if the result is already false
String name = null;
if (name != null && name.length() > 0) { // ✓ safe
System.out.println(name);
// if name is null, name.length() is never called
}
// ANTI-PATTERN: no null check on the left
if (name.length() > 0 && name != null) { // ✗ NullPointerException if name is null!
// || stops at the left operand if the result is already true
int cache = -1;
int value = (cache != -1) || calculateExpensiveValue(); // calculateExpensiveValue() isn't called if cache is valid
flowchart LR
subgraph "&& Short-Circuit"
A["Evaluate\nleft operand"] --> B{Left == false?}
B -- Yes --> C["Return false\nRight NOT evaluated"]
B -- No --> D["Evaluate\nright operand"] --> E["Return\nthe right value"]
endflowchart LR
subgraph "|| Short-Circuit"
A["Evaluate\nleft operand"] --> B{Left == true?}
B -- Yes --> C["Return true\nRight NOT evaluated"]
B -- No --> D["Evaluate\nright operand"] --> E["Return\nthe right value"]
end&& vs & — When to Use Which
#
// Use && (short-circuit) for normal conditions
// The right operand isn't evaluated if not needed
if (list != null && list.size() > 0) { }
// Use & (non-short-circuit) ONLY if the right operand must always execute
// because it has a needed side effect
if (validateField1() & validateField2()) {
// both validations always run, error messages from both are collected
}
Bitwise Operators #
Bitwise operators work directly at the bit level of integers. They’re most commonly used for bitmask flags, high-performance operations, or when interacting with binary protocols.
| Operator | Name | Description |
|---|---|---|
& | AND | A bit is 1 only if both bits are 1 |
| | OR | A bit is 1 if either bit is 1 |
^ | XOR | A bit is 1 if exactly one bit is 1 |
~ | NOT (complement) | Flips all bits |
<< | Left shift | Shifts bits left, fills 0 on the right |
>> | Signed right shift | Shifts right, preserves the sign bit |
>>> | Unsigned right shift | Shifts right, fills 0 on the left |
int a = 0b1010; // 10 in decimal
int b = 0b1100; // 12 in decimal
System.out.println(a & b); // 0b1000 = 8 (AND)
System.out.println(a | b); // 0b1110 = 14 (OR)
System.out.println(a ^ b); // 0b0110 = 6 (XOR)
System.out.println(~a); // -11 (NOT: flips all bits + sign)
// Shift: faster multiplication/division by powers of 2
int x = 4;
System.out.println(x << 2); // 16 (x * 4)
System.out.println(x >> 1); // 2 (x / 2)
// Bitmask — store many flags in a single int
final int FLAG_READ = 0b001; // 1
final int FLAG_WRITE = 0b010; // 2
final int FLAG_EXECUTE = 0b100; // 4
int permission = FLAG_READ | FLAG_WRITE; // permission = 0b011 = 3
// Check whether a specific flag is active
boolean canRead = (permission & FLAG_READ) != 0; // true
boolean canExec = (permission & FLAG_EXECUTE) != 0; // false
// Add a flag
permission |= FLAG_EXECUTE; // permission = 0b111 = 7
// Remove a flag
permission &= ~FLAG_WRITE; // permission = 0b101 = 5
The Ternary Operator #
The ternary operator ? : is a three-operand conditional expression — the only Java operator with three parts. It produces a value (not a statement), so it can be used anywhere an expression is expected.
// Syntax: condition ? valueIfTrue : valueIfFalse
int max = (a > b) ? a : b;
String status = (age >= 18) ? "adult" : "minor";
// Useful for default values
String name = (input != null) ? input : "Anonymous";
// Or use its cleaner equivalent:
String name = Objects.requireNonNullElse(input, "Anonymous");
Ternary vs if-else — When to Use Which #
// CORRECT: ternary for simple one-line expressions
int abs = (x >= 0) ? x : -x;
String label = active ? "Active" : "Inactive";
// ANTI-PATTERN: nested ternary — very hard to read
String grade = (score >= 90) ? "A"
: (score >= 80) ? "B"
: (score >= 70) ? "C"
: (score >= 60) ? "D" : "E"; // ✗ use if-else or switch
// CORRECT: if-else for complex or nested logic
String grade;
if (score >= 90) grade = "A";
else if (score >= 80) grade = "B";
else if (score >= 70) grade = "C";
else if (score >= 60) grade = "D";
else grade = "E";
The instanceof Operator
#
instanceof checks whether an object is an instance of a specific type. Its result is a boolean. Since Java 16, there’s instanceof pattern matching that combines type checking and casting in one step.
// Old way — check then cast separately
Object obj = "Hello Java";
if (obj instanceof String) {
String s = (String) obj; // manual cast
System.out.println(s.length());
}
// New way (Java 16+) — pattern matching: check + bind at once
if (obj instanceof String s) { // s is automatically a String inside this block
System.out.println(s.length()); // no manual cast needed
}
// Pattern matching makes code far more concise for polymorphism
void process(Object shape) {
if (shape instanceof Circle c) {
System.out.println("Radius: " + c.getRadius());
} else if (shape instanceof Square p) {
System.out.println("Side: " + p.getSide());
} else if (shape instanceof Triangle s) {
System.out.println("Base: " + s.getBase());
}
}
// instanceof is always false for null
String s = null;
System.out.println(s instanceof String); // false — doesn't throw NPE
Precedence (Priority Order) #
When several operators appear in one expression, Java evaluates them based on precedence — the priority order from highest to lowest:
| Priority | Operator | Associativity |
|---|---|---|
| 1 (highest) | ++ -- (post), (), [], . | Left to right |
| 2 | ++ -- (pre), + - (unary), ~, ! | Right to left |
| 3 | * / % | Left to right |
| 4 | + - | Left to right |
| 5 | << >> >>> | Left to right |
| 6 | < > <= >= instanceof | Left to right |
| 7 | == != | Left to right |
| 8 | & | Left to right |
| 9 | ^ | Left to right |
| 10 | | | Left to right |
| 11 | && | Left to right |
| 12 | || | Left to right |
| 13 | ? : (ternary) | Right to left |
| 14 (lowest) | = += -= etc. | Right to left |
// Precedence examples in practice
int result = 2 + 3 * 4; // 14 — not 20: * has higher precedence than +
int result = (2 + 3) * 4; // 20 — parentheses force the order
boolean check = 5 > 3 && 2 < 4; // true: > and < are evaluated first, then &&
// ANTI-PATTERN: relying on precedence for complex expressions
int x = a++ * b-- + c >> 2; // ✗ very hard to read
// CORRECT: use parentheses to clarify the intended order
int x = ((a++) * (b--) + c) >> 2; // ✓ the intent is clear
Summary #
- Integer division discards decimals —
7 / 2gives3, not3.5; cast one operand todoublefirst:(double) 7 / 2or7 / 2.0.&&and||are short-circuit — the right operand isn’t evaluated when the result is already determined; exploit this for null checks:obj != null && obj.method().==on references compares addresses — use.equals()for object contents andObjects.equals()for null-safe comparison.- Compound assignment includes an implicit cast —
b += 1isn’t exactly the same asb = b + 1forbyte/short; the former includes an automatic narrowing cast.- Pre vs post increment differ —
a++uses the old value then increments;++aincrements first then uses the new value; avoid both in complex expressions.instanceofpattern matching (Java 16+) —if (obj instanceof String s)combines type checking and casting in one step;instanceofis alwaysfalsefornull.- Ternary for simple expressions only —
condition ? a : bis good for default values or one-line choices; avoid nested ternaries, useif-elsefor complex logic.- Use parentheses for clarity — don’t rely on precedence for expressions with more than two operators; parentheses make the intent clearer to readers.