Polyglot
Languages Java operators
Java § operators

Operators

Java inherits the operator surface of C and adds the instanceof type-test operator (with patterns since Java 16), the string concatenation + operator, and a small set of refinements (the unsigned right shift >>>, the lambda arrow ->, the method reference ::). Most of the operator surface is well-defined: integer overflow wraps, division by zero throws ArithmeticException, the bit shifts have specified semantics. Java does not admit user-defined operator overloading: + for class types is reserved for String (which the compiler handles specially) and is a compile-time error for any other class. The restriction simplifies reading non-trivial code at the cost of some expressive density.

Inherited operators

The arithmetic, comparison, logical, and bitwise operators are essentially those of C with several refinements:

int  a = 7,  b = 2;
int  q = a / b;                  // 3
int  r = a % b;                  // 1

boolean t = (a > b) && (b != 0);

int  flags = 0b1010 | 0b0101;     // 0b1111
int  high  = flags & 0b1100;      // 0b1000
int  xor   = flags ^ 0b1100;      // 0b0011
int  not   = ~flags;
int  shl   = flags << 2;
int  shr   = flags >> 1;          // arithmetic (sign-extends)
int  ushr  = flags >>> 1;         // logical (zero-fills) — Java-specific

The >>> operator is Java’s unsigned right shift; it fills the high bits with zero regardless of sign. C and C++ leave the corresponding behaviour implementation-defined; Java specifies it.

Integer arithmetic in Java wraps on overflow — the standard requires the modular semantics. There is no checked mode (as in C#); for overflow-detecting arithmetic, the standard library provides Math.addExact(a, b), Math.multiplyExact(a, b), etc., which throw ArithmeticException on overflow.

int max = Integer.MAX_VALUE;
int wrapped = max + 1;            // -2147483648, defined behaviour

int safe = Math.addExact(max, 1); // throws ArithmeticException

Floating-point arithmetic follows IEC 60559 (IEEE 754); division by zero yields Infinity, -Infinity, or NaN. The strictfp modifier (largely redundant since Java 17) historically constrained floating-point semantics to be platform-independent; modern JVMs are strictfp by default.

Compound assignment

Eleven compound operators combine an operation with assignment:

int n = 10;
n += 5;       // 15
n -= 3;       // 12
n *= 2;       // 24
n /= 4;       // 6
n %= 4;       // 2
n <<= 1;      // 4
n >>= 1;      // 2
n >>>= 1;     // 1
n &= 0xFF;
n ^= 0xFF;
n |= 0x10;

Compound assignment evaluates the left operand exactly once. For non-int types, the operator implicitly casts the right operand:

short s = 0;
s = s + 1;     // ERROR: int + short produces int, narrowing required
s += 1;        // OK: implicit narrowing (since the compound form admits the cast)

The implicit cast in compound assignment occasionally surprises but is consistent across the language.

The instanceof operator

instanceof tests whether an object is an instance of a given type:

Object o = "hello";

boolean isString = o instanceof String;     // true

if (o instanceof String s) {                 // pattern (since Java 16)
    System.out.println(s.length());          // s is bound to (String) o
}

The pattern form combines the test and the cast: if o is a String, s is bound to it (with type String) and the body executes. The form is the conventional contemporary replacement for the older if (o instanceof String) { String s = (String) o; … } pattern.

instanceof returns false for null regardless of the type — a null reference is not an instance of anything. The conventional null-check if (o != null) { … } is therefore unnecessary before an instanceof.

The pattern grammar extends to records and sealed types in switch expressions; the full treatment is in Pattern matching.

String concatenation

The + operator on strings concatenates:

String s = "hello, " + name + ", you are " + age + " years old";

The compiler translates the chain into a StringBuilder-based assembly (or, since Java 9, an invokedynamic call to StringConcatFactory); the cost is approximately one allocation per + chain rather than one per + operator.

For loops that build strings, an explicit StringBuilder is significantly faster than repeated +:

// Slow:
String s = "";
for (int i = 0; i < 1000; i++) s = s + i;     // quadratic

// Fast:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) sb.append(i);
String s = sb.toString();

Within a single expression (no loop), + is fine; the compiler optimises.

The conditional operator

The ternary ?: operator selects between two expressions:

int    max = (a > b) ? a : b;
String s   = (n == 1) ? "" : "s";

The two arms must have a common type, with the standard’s elaborate rules. Mixing types unwisely produces compilation errors. Nested conditionals are syntactically legal but quickly become unreadable.

The lambda arrow ->

The -> operator separates lambda parameters from the body:

Function<Integer, Integer> square = x -> x * x;
BinaryOperator<Integer>     add    = (a, b) -> a + b;
Runnable                    task   = () -> System.out.println("running");

The arrow is also used in switch expressions (since Java 14):

String name = switch (day) {
    case MONDAY -> "Monday";
    case TUESDAY -> "Tuesday";
    default -> "other";
};

The grammar contexts are different but the surface is the same.

The method reference ::

The :: operator yields a method reference — a value that designates a method without invoking it:

Function<String, Integer> length = String::length;     // instance method
Supplier<List<String>>    create = ArrayList::new;     // constructor
Consumer<String>          print  = System.out::println; // bound instance method
Function<Integer, Integer> abs   = Math::abs;          // static method

Method references are syntactic sugar for the corresponding lambda; String::length is (s) -> s.length(). The treatment is in Methods and lambdas.

Unsupported: operator overloading

Java does not admit user-defined operator overloading. The exception is the + operator on String, which the compiler handles specially. For arithmetic on user-defined types (a Money, a Complex, a matrix), the conventional pattern is named methods:

public class Money {
    private final long cents;
    public Money(long cents) { this.cents = cents; }
    public Money plus(Money other) { return new Money(cents + other.cents); }
    public Money times(int factor) { return new Money(cents * factor); }
    // ...
}

Money total = price.plus(tax).times(quantity);

The lack of operator overloading produces verbose arithmetic on user-defined types but eliminates a class of “what does + mean here?” confusion that other languages admit. Whether the trade-off is favourable is a long-standing debate; Java has held its position consistently across all revisions.

sizeof, typeof equivalents

Java has no sizeof operator. Memory layout is controlled by the JVM and is not directly observable from the language. For object-size queries, the java.lang.instrument API or third-party tools (JOL, the Java Object Layout library) are the conventional answer.

obj.getClass() returns the runtime Class<?> of an instance; MyType.class yields the Class<MyType> for a known type:

Class<? extends String> c1 = "hello".getClass();
Class<String>           c2 = String.class;

Both forms are conventionally used in reflection and in framework code that needs runtime type information.

Sequence points and order of evaluation

Java specifies the order of evaluation for all expression forms — there is no C-style undefined behaviour from unsequenced modifications:

  • Subexpressions of binary operators are evaluated left-to-right.
  • Function arguments are evaluated left-to-right before the call.
  • The two operands of &&, ||, and ?: are evaluated in order with short-circuiting.

The expression i = i++ is well-defined: the post-increment evaluates to the original i, increments i, then the assignment overwrites i with the post-increment’s result. The result is that i ends up unchanged. The standard’s specification eliminates an entire category of bugs that C programmers must guard against.

Lvalues and rvalues

Java does not use the lvalue/rvalue terminology of C and C++. The language admits assignment to:

  • Local variables.
  • Fields (instance and static).
  • Array elements.

That is, expressions of the form variable, expression.field, or expression[index]. There is no equivalent of C’s “I want to take the address of” — Java has no & operator.

Method-return expressions, the result of arithmetic, and the result of method calls are not assignable. The compiler diagnoses attempts to assign to them.

Operator precedence

The full precedence table from highest to lowest:

LevelOperatorsAssociativity
1++ -- (postfix), (), [], ., :: (method ref)left
2++ -- (prefix), +, - (unary), !, ~, (type), newright
3* / %left
4+ - (binary, including String concat)left
5<< >> >>>left
6< <= > >= instanceofleft
7== !=left
8& (bitwise / boolean)left
9^left
10|left
11&&left
12||left
13?:right
14=, +=, -=, *=, /=, %=, <<=, >>=, >>>=, &=, ^=, |=right
15-> (lambda)right

Three precedence facts worth memorising:

  1. & and | (bitwise) bind less tightly than == and !=. if ((x & MASK) == 0) requires the parentheses.
  2. The cast operator (T) binds at the unary level; (T)x.field is ((T) x).field, not (T)(x.field).
  3. The instanceof operator binds at the relational level, so x instanceof Type && y works as expected.

When in doubt, parenthesise. Java does not penalise redundant parentheses.