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.

OperatorNameExampleResult
+Addition10 + 313
-Subtraction10 - 37
*Multiplication10 * 330
/Division10 / 33 (not 3.33!)
%Remainder (modulo)10 % 31
+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.

OperatorEquivalent toExample
=x = 10
+=x = x + nx += 5
-=x = x - nx -= 3
*=x = x * nx *= 2
/=x = x / nx /= 4
%=x = x % nx %= 3
&=x = x & nx &= 0xFF
|=x = x | nx |= 0x01
^=x = x ^ nx ^= mask
<<=x = x << nx <<= 2
>>=x = x >> nx >>= 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.

OperatorMeaningExampleResult
==Equal to5 == 5true
!=Not equal to5 != 3true
>Greater than5 > 3true
<Less than5 < 3false
>=Greater than or equal5 >= 5true
<=Less than or equal5 <= 4false

== 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 (&, |).

OperatorNameDescription
&&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
!NOTFlips the boolean value
&AND (non-short-circuit)Always evaluates both sides
|OR (non-short-circuit)Always evaluates both sides
^XORtrue 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"]
    end
flowchart 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.

OperatorNameDescription
&ANDA bit is 1 only if both bits are 1
|ORA bit is 1 if either bit is 1
^XORA bit is 1 if exactly one bit is 1
~NOT (complement)Flips all bits
<<Left shiftShifts bits left, fills 0 on the right
>>Signed right shiftShifts right, preserves the sign bit
>>>Unsigned right shiftShifts 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:

PriorityOperatorAssociativity
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< > <= >= instanceofLeft 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 decimals7 / 2 gives 3, not 3.5; cast one operand to double first: (double) 7 / 2 or 7 / 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 and Objects.equals() for null-safe comparison.
  • Compound assignment includes an implicit castb += 1 isn’t exactly the same as b = b + 1 for byte/short; the former includes an automatic narrowing cast.
  • Pre vs post increment differa++ uses the old value then increments; ++a increments first then uses the new value; avoid both in complex expressions.
  • instanceof pattern matching (Java 16+)if (obj instanceof String s) combines type checking and casting in one step; instanceof is always false for null.
  • Ternary for simple expressions onlycondition ? a : b is good for default values or one-line choices; avoid nested ternaries, use if-else for 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.

← Previous: Data Types   Next: Conditional Selection →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact