Back to Blog
Java

Java Primitive vs Object Parameters: How Passing Works

java primitive vs object parameter: Understand how Java passes primitive values and object references to methods, why reassignment never affects the caller, and when t...

JavaMethod ParametersPass by ValuePrimitive TypesObject References
Diagram showing a primitive value copied into a method frame versus an object reference copied while both references share the same heap object

The java primitive vs object parameter distinction is one of the most misunderstood areas of the language. Java is strictly pass-by-value. When you call a method, each argument produces a copy of the value held in the caller's variable. The distinction between primitives and objects is not about pass-by-value versus pass-by-reference. It is about what the value being copied actually is.

For a primitive like int, double, or boolean, the variable holds the data itself. Copying that value means the method receives an independent copy.

For an object, the variable holds a reference — an address-like handle to the object on the heap. Copying that value means the method receives a copy of the reference, not a copy of the object.

Primitive Parameters: Independent Copies

public class PrimitiveDemo { public static void main(String[] args) { int count = 10; modify(count); System.out.println(count); // 10 } static void modify(int value) { value = 99; } }

The output is 10. The method received a copy of the value 10. Assigning 99 to the parameter changes only the local copy. The caller's variable remains untouched.

This behavior is consistent for all eight primitive types: byte, short, int, long, float, double, char, and boolean.

Object Parameters: Shared State Through a Copied Reference

import java.util.ArrayList; import java.util.List; public class ObjectDemo { public static void main(String[] args) { List<String> items = new ArrayList<>(); items.add("first"); addItem(items); System.out.println(items.size()); // 2 } static void addItem(List<String> list) { list.add("second"); } }

The output is 2. The method received a copy of the reference to the same ArrayList instance. Calling add on that reference mutates the object that the caller's variable still points to.

The key distinction: reassigning the parameter does not affect the caller.

public class ReassignDemo { public static void main(String[] args) { StringBuilder builder = new StringBuilder("hello"); replace(builder); System.out.println(builder); // hello } static void replace(StringBuilder value) { value = new StringBuilder("world"); } }

The output is "hello". The method reassigned its local reference to a new StringBuilder. The caller's reference still points to the original object.

Why This Confusion Persists

Many developers describe object parameters as "pass by reference" because mutations inside the method are visible to the caller. That behavior does not come from reference passing. It comes from the fact that both the caller and the method hold copies of the same reference, and those copies point to the same heap object.

If Java truly passed by reference, the reassignment example above would change the caller's variable to point to the new StringBuilder. It does not.

This distinction matters when you design methods that intend to change what the caller's variable points to. In Java, you cannot do that directly. You need a different mechanism, such as returning the new value or using a mutable holder object.

Mutable vs Immutable Parameters

The behavior of an object parameter depends on whether the object is mutable.

Mutable objects like ArrayList, HashMap, and StringBuilder can be changed through the copied reference. Those changes are visible to the caller.

Immutable objects like String, Integer, and LocalDate cannot be changed. Any operation that appears to modify them returns a new instance instead.

public class StringDemo { public static void main(String[] args) { String text = "hello"; appendWorld(text); System.out.println(text); // hello } static void appendWorld(String value) { value = value + " world"; } }

The output is "hello". The concatenation creates a new String, and the local reference is reassigned to it. The caller's reference still points to the original "hello" string.

This is not a special property of String parameters. It is the combination of reference copying and immutability. The same code with a mutable type would show different behavior.

Performance and Memory Implications

Primitive parameters are passed on the call stack. Copying an int or a double is a single machine-word operation with no allocation.

Object parameters copy a reference, which is also a machine-word-sized value. The object itself is not copied. The cost of passing an object parameter is therefore similar to the cost of passing a primitive, in terms of the copy operation itself.

The real cost difference appears with boxing. When you pass a primitive where an object type is expected, the compiler may box it into a wrapper like Integer or Double.

public class BoxingDemo { public static void main(String[] args) { Integer wrapped = 42; // autoboxing process(wrapped); } static void process(Integer value) { // value is an object, not a primitive } }

Autoboxing allocates an object on the heap. In a tight loop, that allocation adds pressure on the garbage collector. If you are writing performance-sensitive code, prefer primitive parameters over wrapper types when the method does not need null semantics or object identity.

The performance concern is not the parameter passing mechanism. It is the allocation that boxing introduces.

Choosing Between Primitive and Object Parameters

The choice is not really about the parameter syntax. It is about the type you declare for the parameter.

Use a primitive parameter when:

  • The value can never be null
  • The method only reads or computes from the value
  • You want to avoid allocation in hot paths

Use an object parameter when:

  • The method must mutate the caller's object state
  • The value can be null and you need to handle that case
  • You need to call methods on the object
  • The type is inherently object-based, like String or a domain entity

A common pattern is to use primitive types for numeric computations and object types for domain models, collections, and values that need behavior.

Handling Null in Object Parameters

An object parameter can be null. If the method dereferences it without checking, the call throws a NullPointerException at runtime.

public class NullDemo { public static void main(String[] args) { process(null); } static void process(List<String> list) { list.size(); // NullPointerException } }

Primitive parameters cannot be null. The compiler rejects any attempt to pass null to a primitive parameter. This is a compile-time guarantee that object parameters do not provide.

If a method accepts an object parameter and null is not a valid input, check it explicitly and fail fast with a clear message. This keeps the failure mode predictable and avoids the error surfacing far from the actual cause.

Final Decision Criteria

The practical rule is straightforward. Use primitives when the value is a simple number, flag, or character that cannot be null and does not need behavior. Use objects when the value is a domain entity, a collection, or any type that must support methods, null handling, or shared mutable state.

The parameter passing mechanism is the same in both cases. What differs is what the copied value represents, and that difference drives every observable behavior discussed above.

java primitive vs object parameter: Practical Usage and Code | RYUSLOG DEV