---
title: 'Operators and Expressions'
source: 'https://academia.sh/en/courses/java-fundamentals/operators-and-expressions'
course: 'Java Fundamentals'
language: en
updated: '2026-08-17T18:09:40+00:00'
license: 'CC BY-SA 4.0'
---

# Operators and Expressions

One-character forms leave silent steps in the class file: adding an int and a long inserts a conversion, count += 300 puts in a narrowing never written in the source and the result comes out 45, short-circuit shows up as a branch instruction — with constants, no step is added at all.

The previous lesson measured where a name is written in the class file: into a field, or into
a slot. Once names have settled into their places, next come the operators that combine them.
An operator is one character in the source; in the class file it is one or several
instructions, and part of those instructions is never written in the source at all. This
lesson counts that part.

The Programming Fundamentals course established operators, precedence, short-circuit
evaluation, and explicit/implicit conversion as concepts — **those are not repeated here.**
There, when a conversion is widening versus narrowing, and why narrowing can produce silent
information loss, was explained. What is measured here is **who adds** that narrowing: a line
that writes no conversion anywhere in the source can carry a conversion instruction in the
class file.

## An Operator Is a Shorthand

Java's arithmetic operators only work at four widths: `int`, `long`, `float`, and `double`.
There is no separate addition instruction for `byte`, `short`, and `char`. The consequence is
this: values in smaller types are promoted to `int` before entering the operation, and if the
two operands have separate widths, the narrower one is converted to the wider one. This is
called **numeric promotion**, and it is invisible in the source.

Promotion has a reverse too. Compound assignment — `+=`, `-=`, `*=` — has to preserve the
left side's type. When an `int` is added to a `byte` variable with `+=`, the addition is done
at `int` width, then the result is **narrowed** back to `byte`. This narrowing is not written
in the source; if it were written, the compiler would not want it anyway. The same operation
written as `count = count + 300` would give a compile error, because there the programmer
would have to write the narrowing.

The real distinction between the two directions is safety. A crossing from a narrow type to a
wide one is **widening** and loses no information; the compiler does it without asking. A
crossing from a wide type to a narrow one is **narrowing**, and bits that do not fit are
discarded; the compiler does not normally do this, it asks the programmer to write it.
Compound assignment is the **one gap** in this rule: the left side's type is already known,
so the compiler puts the narrowing in itself, right where it would otherwise ask for it. The
measurement looks for this gap's counterpart in the class file.

The lesson measures these two directions on two fields of the record carried through the
course: `int count` and `long weight`. The width difference between them is the narrowest
example on which promotion can be measured.

## The Measurement Core

The core is the previous lessons' core; this lesson reads two measurements. The first is
**conversion instructions** — instructions in the class file that convert one width to
another form a separate family, and their names carry the two types converted (`i2l`,
`i2b`). The second is **branch instruction count**: every instruction that carries the flow
from one point to another is counted.

- **BS11** — The right column lists conversions, dynamic calls, and calls in the class file;
  the reader drops what was written in the source, and what is left is what the compiler
  added.
- **BS12** — All measured methods are static and their bodies are a single line; the
  difference in instruction count comes not from method-call overhead but only from the
  expression itself.

```java
// Gauge.java — reads the conversion, call, and branch instruction an operator leaves in the class file
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.lang.classfile.instruction.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

class Gauge {
    record Method(String name, int instructions, int branches, List<String> added) {}

    static List<Method> read(String source, String className) throws Exception {
        Path d = Files.createTempDirectory("gauge");
        Files.writeString(d.resolve(className + ".java"), source);
        PrintWriter sink = new PrintWriter(Writer.nullWriter());
        if (ToolProvider.findFirst("javac").orElseThrow().run(sink, sink, "-d", d.toString(),
                d.resolve(className + ".java").toString()) != 0)
            throw new IllegalStateException("did not compile: " + className);
        List<Method> methods = new ArrayList<>();
        for (MethodModel m : ClassFile.of().parse(d.resolve(className + ".class")).methods()) {
            if (m.code().isEmpty()) continue;
            List<String> added = new ArrayList<>();
            int instructions = 0, branches = 0;
            for (CodeElement e : m.code().get()) {
                if (e instanceof Instruction) instructions++;
                if (e instanceof BranchInstruction) branches++;
                if (e instanceof ConvertInstruction c)
                    added.add("convert " + c.opcode().name().toLowerCase(Locale.ROOT));
                else if (e instanceof InvokeDynamicInstruction id)
                    added.add("dynamic:" + id.name().stringValue());
                else if (e instanceof InvokeInstruction iv)
                    added.add(iv.name().stringValue());
            }
            methods.add(new Method(m.methodName().stringValue(), instructions, branches, added));
        }
        return methods;
    }
}
```

## Thirteen Expressions, What They Leave in the Class File

The measured class carries thirteen methods, each a single expression. `narrow` and
`explicitNarrow` do the same work; the only difference is whether the narrowing is written in
the source. `lateConvert` and `earlyConvert` do the same multiplication; the only difference
is whether the conversion is asked for before or after the multiplication. `constantArithmetic`,
`finalConstantProduct`, and `variableProduct` give **the same product** in three separate
forms.

```java
// Operators.java — thirteen expressions, what they leave in the class file
public class Operators {
    static final String SOURCE = """
        class Warehouse {
            static long promote(int count, long weight) { return count + weight; }
            static byte narrow(byte count) { count += 300; return count; }
            static byte explicitNarrow(byte count) { count = (byte) (count + 300); return count; }
            static int smallSum(byte count, short shelf) { return count + shelf; }
            static long lateConvert(int count, int unit) { return count * unit; }
            static long earlyConvert(int count, int unit) { return (long) count * unit; }
            static String concat(String name, int count) { return name + count; }
            static String constantConcat() { return "bo" + "lt"; }
            static int constantArithmetic() { return 24 * 60 * 60; }
            static final int SHELVES = 24;
            static int shelfCount = 24;
            static int finalConstantProduct() { return SHELVES * 60 * 60; }
            static int variableProduct() { return shelfCount * 60 * 60; }
            static boolean shortCircuit(int count, long weight) { return count > 0 && weight > 0; }
            static boolean fullEval(int count, long weight) { return (count > 0) & (weight > 0); }
        }
        """;

    public static void main(String[] args) throws Exception {
        System.out.printf("%-18s %6s %4s  %s%n", "method", "instr", "br", "step not written in source");
        for (Gauge.Method y : Gauge.read(SOURCE, "Warehouse")) {
            if (y.name().startsWith("<")) continue;
            System.out.printf("%-18s %6d %4d  %s%n", y.name(), y.instructions(), y.branches(),
                    y.added().isEmpty() ? "-" : String.join(", ", y.added()));
        }
    }
}
```

```
method              instr   br  step not written in source
promote                 5    0  convert i2l
narrow                  7    0  convert i2b
explicitNarrow          7    0  convert i2b
smallSum                4    0  -
lateConvert             5    0  convert i2l
earlyConvert            6    0  convert i2l, convert i2l
concat                  4    0  dynamic:makeConcatWithConstants
constantConcat          2    0  -
constantArithmetic      2    0  -
finalConstantProduct      2    0  -
variableProduct         6    0  -
shortCircuit           10    3  -
fullEval               14    4  -
```

The first row is numeric promotion's name in the class file. In the expression
`count + weight`, `count` is an `int`, `weight` is a `long`; the addition instruction cannot
take both at once, so the compiler inserts an **`i2l`** in between. It writes no conversion in
the source. One of five instructions is a conversion the programmer did not write.

The second row is this lesson's quietest step. The form `count += 300` produces seven
instructions and one of them is an **`i2b`**: the addition is done at `int` width, the result
is narrowed to `byte`. The compiler adds the narrowing on its own. This is the class file's
counterpart of the silent information loss explained in the previous course — the concept was
established there, here its point of insertion is shown.

The row below measures how exactly identical this is. `explicitNarrow` writes the narrowing
**in the source** and comes out the same as `narrow` in the class file: the same **7**
instructions, the same `i2b`. The two forms are indistinguishable in the class file. `+=` is
not an operator, it is a **shorthand** for this line; what it shortens has a conversion inside
it, and the shorthand hides it.

The fourth row shows that promotion does not always produce an instruction. When a `byte` and
a `short` are added, both are promoted to `int`, but there is **no conversion at all** in the
class file: four instructions, zero added steps. These types are already loaded at `int`
width, so promotion pays no instruction. Promotion exists everywhere as a rule; as an
instruction it shows up only when a width actually changes.

The next two rows compare conversion's **location**. Both return the same product as a
`long`. `lateConvert` carries a single `i2l`, and that conversion is **after** the
multiplication: two `int`s are multiplied, the result is converted to `long`.
`earlyConvert` carries two `i2l`s — one the conversion written in the source, the other the
promotion added for the second operand — and the multiplication is now done at `long` width.
One instruction more, but a separate computation.

The concatenation rows tie the same `+` operator to two separate outcomes. Concatenation
involving a variable leaves a **dynamic call** in four instructions; concatenating two
constants is two instructions and **no** added step at all. The same operator, one a call,
one zero steps.

The last two rows are short-circuit evaluation's trace in the class file. The form using `&&`
produces **10** instructions and **3** branches, the form using `&` produces **14**
instructions and **4** branches. The `&` form has to convert both comparisons to a boolean
value; `&&` puts in a branch that never enters the second comparison at all when the first
comes out false. Short circuit is not a run-time convenience, it is a **jump** sitting in the
class file.

## The Added Conversion's Visible Result

Every step counted so far has a counterpart that changes a value.

- **BS13** — In the overflow measurement, record count is 100000, unit weight is 50000
  grams; the product is too large to hold at `int` width, and the only difference between the
  two forms is where the conversion sits.
- **BS14** — In the short-circuit measurement, the warehouse carries three records and two of
  them have a `count` field of zero; the right operand is a method call, and that method
  increments a counter every time it is called, so how many times the skipped branch was
  skipped can be counted.

```java
// Result.java — the visible result of an added conversion and a skipped branch
public class Result {
    record Item(String name, byte count, long weight) {}

    static int calls = 0;

    static boolean isHeavy(Item k) { calls++; return k.weight() > 0; }

    public static void main(String[] args) {
        byte count = 1;
        count += 300;
        System.out.println("byte count = 1; count += 300      ->  " + count);

        int stock = 100000, unit = 50000;
        System.out.println("long lateConvert(100000, 50000)   ->  " + (long) (stock * unit));
        System.out.println("long earlyConvert(100000,50000)   ->  " + (long) stock * unit);

        Item[] warehouse = { new Item("bolt", (byte) 0, 40L),
                              new Item("screw", (byte) 3, 90L),
                              new Item("nut", (byte) 0, 12L) };
        calls = 0;
        int a = 0;
        for (Item k : warehouse) if (k.count() > 0 && isHeavy(k)) a++;
        int shortCircuit = calls;
        calls = 0;
        int b = 0;
        for (Item k : warehouse) if ((k.count() > 0) & isHeavy(k)) b++;
        System.out.println("&&  result: " + a + "  isHeavy calls: " + shortCircuit);
        System.out.println("&   result: " + b + "  isHeavy calls: " + calls);
    }
}
```

```
byte count = 1; count += 300      ->  45
long lateConvert(100000, 50000)   ->  705032704
long earlyConvert(100000,50000)   ->  5000000000
&&  result: 1  isHeavy calls: 1
&   result: 1  isHeavy calls: 3
```

The first line is what the `i2b` instruction costs. 300 was added to a count, and the result
came out **45**. No conversion is written in the source, the compiler produces no warning,
and the program keeps running. The decision was made not by the programmer but by the added
step.

The second and third lines show what conversion's **location** costs. When the product is
computed at `int` width, the result is **705032704**; at `long` width, **5000000000**. It is
not enough for the left-side variable to be `long` — the result's width cannot be rescued by a
conversion done after the multiplication. This is the clearest measurement showing that
promotion applies **per operation.**

The last two lines are short circuit's run-time counterpart. Both forms find the same result:
**1** record. The number of calls paid is separate: the `&&` form runs the right operand **1**
time, the `&` form runs it **3** times. A branch-instruction difference in the class file
turns here into a two-method-call difference. If the right operand carried a side effect, the
two forms would not give the same program; the choice between `&` and `&&` is not a matter of
style.

## The Bounding Measurement: No Added Step on Constants

Three rows in the table give the same product in three separate forms, and in all three the
added-step count is **zero.** Instruction counts are separate: **2**, **2**, and **6**.

- **BS15** — All three methods return the same number; the only difference between them is
  whether the factors are known at compile time.

`constantArithmetic` finishes in two instructions: there is **no** multiplication instruction
in the class file, the result is written directly. Constant folding, introduced in the
compilation chain, turns into something measured here. `finalConstantProduct` is also two
instructions — because `SHELVES` is a `static final` field and its initial value is known at
compile time; the compiler substitutes not the name but the **value** and does the folding.
`variableProduct` is six instructions: because `shelfCount` can change later, its value is
read and the two multiplications are done at run time.

This trio bounds the lesson's thesis. An added step is not a rule, it is a **conditional
counterpart**: a conversion is added when operand widths diverge, a narrowing is added when
the left side is narrow, a dynamic call is added when one of the operands is a variable. If
all operands are known at compile time, nothing is left to add — on the contrary, the
operation written in the source then **drops out** of the class file. An added step is
sometimes a removal.

The boundary has a practical counterpart too: declaring a constant `static final` is not just
a declaration of immutability, it grants the compiler permission to substitute that value. The
same constant declared without `final` pays two multiplications at run time. The difference in
this measurement is four instructions and is invisible by looking at the source.

## Summary

- Numeric promotion is a conversion instruction in the class file: when an `int` and a `long`
  are added, one of five instructions is an **`i2l`** never written in the source.
- Compound assignment adds the narrowing on its own; `count += 300` leaves one **`i2b`** in
  seven instructions and makes the result **45**, and the compiler produces no warning. The
  form that writes the narrowing in the source is indistinguishable from this in the class
  file.
- Promotion does not always produce an instruction: when a `byte` and a `short` are added,
  there is no conversion at all in the class file, because both are already loaded at `int`
  width.
- Promotion applies per operation: the same product gives **705032704** at `int` width,
  **5000000000** at `long` width, and assigning the result to a `long` variable does not fix
  this.
- Short circuit is a branch instruction: the `&&` form produces **10** instructions and **3**
  branches, the `&` form **14** instructions and **4** branches; at run time the right operand
  runs **1** and **3** times.
- No added step is left when all operands are known at compile time: the same product is
  **2** instructions with constants, **6** with a variable, and zero added steps in all
  three.

## Next Step

This lesson measured a branch instruction for the first time — one that carries the flow from
one point to another — and it came up as an operator's byproduct. The next lesson makes the
branch its main subject: the same behavior written with `if` and `switch` compiles to **two
separate instructions** in the class file, and the two instructions' lookup cost is not the
same. What a labeled jump earns in nested loops is measured in instruction count, why the
enhanced `for` loop produces separate instructions over an array versus a list is shown — and
what the gain costs in readability is written in the same place.
