Evaluation Strategies in Programming Languages

Java is neither pure pass-by-value nor pass-by-reference. The answer hides in a term most discussions miss: call-by-sharing.

Every few months, the “is Java pass-by-value or pass-by-reference” debate flares up again on some forum. One side shows that mutating an object’s field is visible outside the function and concludes pass-by-reference. The other side shows that reassigning a parameter does not propagate, and concludes pass-by-value. Both sides can produce code that supports their claim, and neither side convinces the other.

Untangling this requires stepping up to a higher-level concept first — evaluation strategy. Java’s parameter passing is just one facet of its evaluation strategy; once we have the full vocabulary, the binary “value or reference?” question dissolves on its own.

Parameters and arguments

Two terms appear throughout the rest of this post, so let’s pin them down first:

  • Parameter — a variable declared in a function definition, used inside the body to receive whatever value is passed in. In def foo(a: int), a is the parameter.
  • Argument — the actual expression written in the parentheses at the call site. In y = foo(x), x is the argument.

Evaluation strategy is the set of rules governing how an argument gets evaluated and how that value is wired to the parameter.

What is an evaluation strategy

In computer science, an evaluation strategy is a set of rules for evaluating expressions in a programming language. Emphasis is typically placed on functions or operators — an evaluation strategy defines when and in what order the arguments to a function are evaluated, when they are substituted into the function, and what form that substitution takes.

Wikipedia

Pulling this apart, an evaluation strategy answers two independent questions:

  1. When is the argument evaluated? Is it computed before the function is called, or only when the function actually uses it?
  2. How is it passed? Once computed, is the value copied into the parameter, or does the parameter share storage with the argument?

The first question splits languages into strict vs. non-strict. The second splits them into pass-by-value, pass-by-reference, and pass-by-sharing. Java makes a choice on each axis.

Strict and non-strict evaluation

Consider this:

foo(expensive())

Will expensive() actually run? Both designs are reasonable, and real languages pick differently.

Strict evaluation requires the argument to be fully evaluated before the function is called. Java, C, and Python all work this way. expensive() runs first, its result is bound to the parameter, and then foo runs — even if foo never touches the parameter, the computation has already happened.

Non-strict evaluation defers evaluation until the parameter is actually used. Haskell’s lazy evaluation is the canonical example:

Haskell function definitions skip both parentheses and return:

foo x = 1

It’s equivalent to:

def foo(x):    return 1

The result is always 1, no matter what’s passed.

foo x = 1result = foo expensive

Here expensive is never evaluated, because foo’s body never references x. This avoids unnecessary work and is also what lets Haskell express constructs like infinite lists.

Strict vs. non-strict only decides when things are evaluated. How the evaluated value reaches the parameter is a separate set of rules.

Three flavors of parameter passing

Textbooks usually contrast two: pass-by-value and pass-by-reference. That dichotomy isn’t enough to describe Java, so we add a third — call-by-sharing.

Call by Value

The argument’s value is copied into the parameter. Parameter and argument live in separate storage from that point on, with no link between them.

void foo(int x) {    x = 20;}int a = 10;foo(a);// a == 10

x is just a copy of a; mutating x can’t reach a. C passes every argument this way.

Call by Reference

The parameter is an alias for the argument — both names refer to the same storage cell. Mutating the parameter is mutating the argument.

void foo(int& x) {    x = 20;}int a = 10;foo(a);// a == 20

C++ declares this with &; Pascal uses the var keyword. This semantics does not exist in Java.

Call by Sharing

Barbara Liskov introduced this term for the CLU language, and it accurately describes how Java, Python, Ruby, and JavaScript pass objects.

The parameter receives a copy of the argument’s reference value. Concretely:

  • The argument and parameter each hold their own reference, but both references point to the same heap object.
  • Mutating the object’s internal state through the parameter is visible from outside (same object).
  • Reassigning the parameter to a new object does not affect the outside (the outside reference never moved).

Semantically it’s closer to pass-by-value — the thing being copied (a reference value) does not change. Behaviorally it can look like pass-by-reference, because you can reach mutable state that the outside can see. That “neither A nor B” quality is exactly why the debate never converges.

Looking at Java with the right vocabulary

Armed with the terms above, three snippets are enough.

Primitives: ordinary call-by-value

public static void changePrimitive(int num) {    num = 20;}int x = 10;changePrimitive(x);// x == 10

int is a primitive, num is a literal-value copy of x. Nothing surprising.

Object references: call-by-sharing

public static void changeReference(Integer num) {    num = new Integer(20);}Integer y = new Integer(10);changeReference(y);// y still points to Integer(10)

At the moment of the call, y and num both point to the same Integer(10) on the heap:

y   ─────► Integer(10)num ─────┘

After num = new Integer(20), num points to a new object while y is untouched:

y   ─────► Integer(10)num ─────► Integer(20)

num was a copy of y’s reference value; reassigning it only updates the copy. That is the definition of call-by-sharing.

Arrays: mutating shared object state

public static void changeArray(int[] arr) {    arr[0] = 20;}int[] z = {10};changeArray(z);// z[0] == 20

This snippet is often cited as proof that “Java is pass-by-reference,” but it’s really just call-by-sharing playing out as expected. arr and z are two references to the same array object; arr[0] = 20 mutates state inside the object, not the reference itself, so the change is visible outside.

By contrast, doing this inside the function:

arr = new int[]{30};

leaves the outer z untouched — exactly analogous to the Integer reassignment example above.

So, “Java is pass-by-value only” — is that right?

If you measure strictly by what gets copied, yes: every Java call copies some value — a literal value for primitives, a reference value for objects. The argument and parameter are never bound to the same storage cell.

The cost of saying it that way is redefining “value” to include “reference values.” For someone new to Java, that leap often produces a misunderstanding: they assume “pass-by-value” means the object itself is copied, get burned by the array example, swing over to “pass-by-reference,” then get burned by the reassignment example, and bounce back and forth.

A more precise framing is:

  • Java primitives use call-by-value.
  • Java objects use call-by-sharing.

Both fall under strict evaluation. Java has no call-by-reference.

Summary

  • An evaluation strategy lives on two independent axes: when arguments are evaluated (strict vs. non-strict) and how they are passed (by value, by reference, or by sharing).
  • Strict evaluation computes arguments before the call; lazy evaluation defers until they are actually used.
  • The classic “pass-by-value vs. pass-by-reference” split isn’t enough to describe modern languages with reference types.
  • Java objects use call-by-sharing: what’s copied is the reference value — not the object, and not a reference alias.
  • The “visible mutation” in the array example comes from changing the shared object’s internal state, not from binding. Reassigning a parameter never affects the outside.

Once the vocabulary is right, there’s no debate left: Java is neither pure pass-by-value nor pass-by-reference. It’s call-by-sharing — which can also be described, if you insist, as “pass-by-value where the value happens to be a reference.”

CompactRelaxed
Normal1.70