---
title: 'Value and Reference Passing'
source: 'https://academia.sh/en/courses/java-fundamentals/value-and-reference-passing'
course: 'Java Fundamentals'
language: en
updated: '2026-08-17T18:09:40+00:00'
license: 'CC BY-SA 4.0'
---

# Value and Reference Passing

Java has a single passing model, and the class file writes it down: five arguments of five separate types are loaded the same way, writing to a parameter's field is visible to the caller, rebinding the parameter is not, and on an immutable type the two situations cannot be told apart.

The previous lesson measured that a string can come out the same by `==` and equal by
`equals`; two names sometimes held a single object. The same question can be asked at a
call's boundary too.

Pass by value and pass by reference were established as concepts in the Programming
Fundamentals course and are not repeated here. The question here is: which of these concepts
does Java realize, and how do we read this from the class file? Someone looking at the source
cannot see, when a record is given to a method, whether the object passes or the reference
leading to it does; the class file can.

## Arguments Are Loaded in a Single Form

- **BS35** — The measured source is written inside the lesson; every extra found in the
  class file is what the compiler added.
- **BS36** — Two measurements from the `Gauge` core are used in this lesson: instruction
  count and added steps per method, and a method's instruction-name sequence.
- **BS37** — The five parameters in the call measurement are deliberately separate types: two
  primitive types (one occupying two slots), a class, an array, and an immutable class.

```java
// Gauge.java — shared measurement core: reads what the compiler puts 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, List<String> added) {}

    static ClassModel compile(String source, String className) throws Exception {
        Path d = Files.createTempDirectory("gauge");
        Path k = d.resolve(className + ".java");
        Files.writeString(k, source);
        PrintWriter sink = new PrintWriter(Writer.nullWriter());
        if (ToolProvider.findFirst("javac").orElseThrow()
                .run(sink, sink, "-d", d.toString(), k.toString()) != 0)
            throw new IllegalStateException("did not compile: " + className);
        return ClassFile.of().parse(d.resolve(className + ".class"));
    }

    static List<Method> read(String source, String className) throws Exception {
        List<Method> methods = new ArrayList<>();
        for (MethodModel m : compile(source, className).methods()) {
            if (m.code().isEmpty()) continue;
            List<String> added = new ArrayList<>();
            int instructions = 0;
            for (CodeElement e : m.code().get()) {
                if (e instanceof Instruction) instructions++;
                if (e instanceof InvokeInstruction iv)
                    added.add(shortName(iv.owner().asInternalName()) + "." + iv.name().stringValue());
                else if (e instanceof InvokeDynamicInstruction id)
                    added.add("dynamic:" + id.name().stringValue());
                else if (e instanceof NewObjectInstruction n)
                    added.add("new " + shortName(n.className().asInternalName()));
            }
            methods.add(new Method(m.methodName().stringValue(), instructions, added));
        }
        return methods;
    }

    static List<String> instructionNames(String source, String className, String method) throws Exception {
        List<String> names = new ArrayList<>();
        for (MethodModel m : compile(source, className).methods())
            if (m.methodName().stringValue().equals(method))
                for (CodeElement e : m.code().orElseThrow())
                    if (e instanceof Instruction i)
                        names.add(i.opcode().name().toLowerCase(Locale.ROOT));
        return names;
    }

    static String shortName(String internal) { return internal.substring(internal.lastIndexOf('/') + 1); }
}
```

```java
// Passing.java — argument loading and writing to a parameter in the class file
public class Passing {
    static final String K = """
        class PassingSample {
            static class Item { String name; int count; long weight; }
            static void takeNumber(int count) {}
            static void takeWeight(long weight) {}
            static void takeItem(Item k) {}
            static void takeArray(int[] d) {}
            static void takeText(String s) {}
            static void call(int count, long weight, Item k, int[] d, String s) {
                takeNumber(count); takeWeight(weight); takeItem(k); takeArray(d); takeText(s);
            }
            static void changeField(Item k) { k.count = 99; }
            static void rebind(Item k) { k = new Item(); k.count = 99; }
            static void incrementNumber(int count) { count = count + 1; }
            static void extendText(String s) { s = s + " x"; }
        }
        """;

    public static void main(String[] args) throws Exception {
        System.out.println("call: " + Gauge.instructionNames(K, "PassingSample", "call"));
        System.out.println();
        System.out.printf("%-16s %6s  %s%n", "method", "instr", "step not written in source");
        for (Gauge.Method y : Gauge.read(K, "PassingSample"))
            if (y.name().equals("changeField") || y.name().equals("rebind")
                    || y.name().equals("incrementNumber") || y.name().equals("extendText"))
                System.out.printf("%-16s %6d  %s%n", y.name(), y.instructions(),
                        y.added().isEmpty() ? "-" : String.join(", ", y.added()));
        for (String y : new String[] {"changeField", "rebind"})
            System.out.printf("%-16s %s%n", y, Gauge.instructionNames(K, "PassingSample", y));
    }
}
```

```
call: [iload_0, invokestatic, lload_1, invokestatic, aload_3, invokestatic, aload, invokestatic, aload, invokestatic, return]

method            instr  step not written in source
changeField           4  -
rebind                8  new PassingSample$Item, PassingSample$Item.<init>
incrementNumber       5  -
extendText            4  dynamic:makeConcatWithConstants
changeField      [aload_0, bipush, putfield, return]
rebind           [new, dup, invokespecial, astore_0, aload_0, bipush, putfield, return]
```

The first row is this lesson's share of the course's thesis. Five arguments of five separate
types pass, and there are **five loading instructions** in the class file: `iload`, `lload`,
and three `aload`. All do the same work — putting the value in a local variable slot onto the
stack — and the only difference between them is the value's width. An integer takes one slot,
a long integer takes two; this is why the third argument loads with `aload_3` — the second
argument has already taken up two numbers.

What is not here speaks louder than what is. There is no instruction that copies an object.
There is no separate instruction that passes a reference by its address. The class file does
not have two separate paths named "pass by value" and "pass by reference"; there is **one**
path, and that path is copying the value sitting in the slot and putting it on the stack. For
a record, an array, and a string, the value sitting in the slot is a **reference**; what gets
copied is not the object, it is that reference. Java's passing model is this single sentence,
and the rest of the measurement is this sentence's consequences.

The table below separates two outcomes. `changeField` is **4** instructions and no step is
added: load the reference, load the value, write to the field. The write goes **into the
object** with `putfield`. `rebind`, by contrast, is **8** instructions and adds two steps: an
object production and a constructor call. The instruction sequence writes the continuation —
`astore_0`, meaning the new reference is written **over the slot.** The slot is the called
method's own slot; there is no link at all between it and the caller's slot.

The last two rows are the same distinction's counterpart on a primitive type and a string:
`incrementNumber` writes to the slot, `extendText` builds a new string with a dynamic call
and again writes to the slot.

## The Visible Change and the Invisible

- **BS38** — No identity number is printed in the visibility measurement; identity is only
  asked with `==`.
- **BS39** — Before every measurement, the field is set to a known value, so what changed can
  be separated from what did not.
- **BS40** — In the immutable-type measurement, the primitive type and the string are tried
  in the **same** form. For both, only rebinding can be written, because a string has no
  writable member.

```java
// Visible.java — which change is visible to the caller
class Item {
    String name;
    int count;
    long weight;
}

public class Visible {
    static boolean sameObject(Item incoming, Item outside) { return incoming == outside; }
    static void changeField(Item k) { k.count = 99; }
    static void rebind(Item k) { k = new Item(); k.count = 99; }
    static void swap(Item a, Item b) { Item g = a; a = b; b = g; }
    static void writeArray(int[] d) { d[0] = 99; }
    static void incrementNumber(int count) { count = count + 1; }
    static void extendText(String s) { s = s + " x"; }

    public static void main(String[] args) {
        Item k = new Item();
        k.name = "north";
        k.count = 12;
        System.out.println("is the parameter the same object as outside: " + sameObject(k, k));

        changeField(k);
        System.out.println("count after changeField : " + k.count);
        k.count = 12;
        rebind(k);
        System.out.println("count after rebind      : " + k.count);

        Item a = new Item();
        a.name = "north";
        Item b = new Item();
        b.name = "south";
        swap(a, b);
        System.out.println("a.name, b.name after swap : " + a.name + " " + b.name);

        int[] d = {12, 18};
        writeArray(d);
        System.out.println("d[0] after writeArray     : " + d[0]);

        int count = 12;
        incrementNumber(count);
        String text = "north";
        extendText(text);
        System.out.println("number and text after     : " + count + " " + text);
    }
}
```

```
is the parameter the same object as outside: true
count after changeField : 99
count after rebind      : 12
a.name, b.name after swap : north south
d[0] after writeArray     : 99
number and text after     : 12 north
```

The first line confirms the previous section's sentence at run time: the reference in the
parameter and the reference outside point to **the same object.** The object was not copied.
This is why the second line is not surprising — writing to the field is visible to the
caller, `count` became **99.**

The third line places the distinction. `rebind` also writes **99** to a field, but because it
first binds the parameter to a new object, what it writes to is a separate object; the
caller's record stays **12.** What makes the difference is not the write itself, it is
**which object** is written to. The fourth line is the best-known form of this: a method
swapping two parameters does nothing to the caller, because what it swaps is two slots.

The fifth line is the same rule applied to an array: an array is an object too, the reference
in the parameter points to the same array, and the write `d[0] = 99` is visible to the caller.

An applicable rule follows from here. A method's caller-visible effect arises only when it
**writes into a reachable object**; an assignment to the parameter itself is never visible.
If we do not want the object the caller gave to change, either the object must have no
mutable member, or the method must build its own copy.

## On an Immutable Type, the Distinction Cannot Be Observed

The last line is the lesson's bounding measurement, and it places two values side by side:
the number **12**, the text **north.** Neither changed.

That the number did not change is expected; `count` is a primitive type, and the value
sitting in the slot is the number itself. The real result is in the text. `extendText` took a
reference, the object that reference pointed to was the same as the caller's, and the caller
again saw no change at all. The previous lesson measured the answer: a string has no writable
member. The line `s = s + " x"` does not write into the object, it cannot; it builds a new
string and puts it in the parameter's slot.

What this means for the measurement is: **this lesson's question cannot be asked of an
immutable type.** A string parameter cannot be told apart from a primitive-type parameter, so
looking at a string cannot decide which model Java uses. What makes the distinction
observable is not the passing form, it is **whether the object has a mutable member.**

## Summary

- There is a single form of passing an argument in the class file: putting the value sitting
  in the slot onto the stack. Five arguments of five separate types pass with five loading
  instructions; the only difference is how many slots the value occupies.
- On reference types, the value sitting in the slot is a reference; what gets copied is not
  the object, it is that reference, and the parameter and the outside name point to the same
  object.
- Writing to a parameter's field goes into the object with `putfield` and is **visible** to
  the caller; rebinding the parameter writes to the method's own slot with `astore` and is
  **not visible.**
- A method swapping two parameters does nothing to the caller; a method writing to an
  element of an array, by contrast, has a visible effect, because an array is an object too.
- The distinction cannot be observed on an immutable type: a string parameter cannot be told
  apart from a primitive-type parameter, because there is no member to write into the object.

## Course Wrap-Up

The course opened with a single question: how many steps, and which steps, does a written
line turn into in the class file? Ten lessons asked this question in ten separate forms and
got the answer, every time, from a run measurement. Gathered together, a single rule comes
out: **in Java, syntax is a shorthand.** The source does not show the whole of the steps
placed into the class file; what it shows is in what form those steps will be requested.

| Lesson | Measured form | Added step | Bounding measurement |
|---|---|---|---|
| Source, Bytecode, and the Virtual Machine | comparing the source text with the class file | 8 of 12 methods, 13 added steps total | a concatenation sitting in the source is entirely absent from the class file |
| Program Life Cycle | entry point and initialization order | a member is added: default constructor, 3 fields and 5 methods | once a constructor is written, the added constructor disappears |
| Packages and Imports | import versus fully qualified name | zero steps: two files byte-for-byte identical | a package name grows the file by six bytes |
| Primitive Types and Wrappers | the crossing between a primitive type and its wrapper | `Integer.valueOf` and `Integer.intValue` | `==` is correct in the cache range, wrong outside it |
| Variables and Scope | where a declaration sits | a field instruction, or just a slot | block scope produces the same 20 instructions, with one fewer slot |
| Operators and Expressions | numeric promotion and compound assignment | `i2l` and `i2b` conversions, a branch on short circuit | no step is added on two constants |
| Control Flow | selection and loop forms | two separate lookup instructions for selection; 5 steps for the enhanced `for` over a list | the same loop produces 24 instructions and zero steps over an array |
| Arrays | array production, length, and access | array production; `arraylength`; a per-element write | covariance's type check does not show up in instruction count |
| Strings | concatenation, the pool, and the builder | a dynamic call; production and three calls in the builder | concatenating two constants is 2 instructions, zero steps |
| Value and Reference Passing | argument loading and writing to a parameter | production and a slot write on rebind | on an immutable type, the distinction cannot be observed |

The table's last column is the course's method. Every lesson also measured a situation where
the added step was **absent** or **disappeared**, because something found everywhere explains
nothing. Constant folding, a written constructor, the cache range, a primitive-typed array —
each draws the rule's boundary. The course's second reading comes from here too: instruction
count and added-step count do not move in the same direction, and fewer instructions does not
mean less work.

In this course, the class was used only as a container. Yet through the measurements the
class itself produced questions too: a constructor not written in the source, three fields
and five methods not written in the source, a change visible to the caller when a record's
field is written. The next course, Object-Oriented Java, takes the class out of being a
container and makes it the subject. The same measure will do its job there too — who decides
which method a call goes to, and when, can be read from the class file just the same.
