Pass by value and Pass by reference
Look at Pointers before reading this. They are not exactly the same.
The implications of the differences between handles and pointers are subtle but important.
High-level overview
- Primitive types and reference types have very different properties.
- All arguments to methods are passed by value in Java.
- Primitive types pass the value of the variable; reference types pass the value of the handle.
Understanding references
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class PointersTests {
@Test
public void testPointers() {
/*
* Primitive types
*/
int num1 = 11;
// Not working with a pointer
int num2 = num1;
assertEquals(num1, 11);
assertEquals(num2, 11);
num1 = 22;
assertEquals(num1, 22);
assertEquals(num2, 11);
//--------------------------------------------
/*
* Reference types - example using maps
*/
Map<String, Integer> map1 = new HashMap<>();
map1.put("value", 15);
// map2 points to map1 - points to the exact hash for map1 in memory
Map<String, Integer> map2 = map1;
assertEquals(map1.get("value"), 15);
assertEquals(map2.get("value"), 15);
map1.put("value", 20);
assertEquals(map1.get("value"), 20);
assertEquals(map2.get("value"), 20);
Map<String, Integer> map3 = new HashMap<>();
map3.put("value", 57);
assertEquals(map1.get("value"), 20);
assertEquals(map2.get("value"), 20);
assertEquals(map3.get("value"), 57);
map2 = map3;
assertEquals(map1.get("value"), 20);
assertEquals(map2.get("value"), 57);
assertEquals(map3.get("value"), 57);
map1 = map2;
assertEquals(map1.get("value"), 57);
assertEquals(map2.get("value"), 57);
assertEquals(map3.get("value"), 57);
// The previous value for map1 (value=20) is now eligible for garbage collection
//--------------------------------------------
/*
* Reference types - example using strings
*/
String s = new String( "Hello" );
assertEquals(s, "Hello");
change(s);
assertEquals(s, "Hello");
StringWrapper sw = new StringWrapper();
sw.s = "Hello";
changeString(sw);
assertEquals(sw.s, "Hello World");
}
public void change( String t )
{
assertEquals(t, "Hello");
t = new String( "World" );
assertEquals(t, "World");
}
class StringWrapper
{
public String s;
}
void changeString( StringWrapper t )
{
t.s += " World";
}
}
Passing Reference and Value Types
When calling a class member function, the developer will pass any required parameters to the method as arguments to the method call. Suppose that class foo has a member method declared as the following:
public int bar( String s, int i )
The caller of the method must supply a String (or an equivalent object that is automatically convertible to String) and an int to the call, or the compiler will generate an error. The questions here are, “What are we passing?” and “What are the consequences of passing any given parameter type?”
In very oversimplified terms, when a method is called, the system takes the arguments passed to the method from the calling routine and pushes them on the program stack. The execution point in the program then is jumped to the beginning of the method’s code. The system then pops the arguments off the stack and uses them as variables of the types declared in the method’s parameter list. This type of mechanism enables methods to be passed arguments that normally may be outside the method’s scope of visibility. When it is time for the method to return to the calling routine, it pushes the return value onto the stack. The program then jumps back to the calling routine and pops the return value back off the stack. For the purposes of this discussion, it is not important that we know the details of how a stack works. It is enough to know that a stack is a construct used to store data.
Java uses a mechanism called pass by value to handle argument passing in method calls. This means that the system makes a copy of the value of the argument and pushes that onto the stack for the called method to access.
In the following example, the value 4 is passed to the method foo():
int i = 4;
foo(i);
The method itself has no knowledge of the variable i. Changes made by foo() to the value passed will have no effect on i from the caller. If 4 is incremented to 5, for example, the value of i remains 4.
This pass by value approach is relatively straightforward for primitive types. But what about reference types? Aren’t they references to objects? Isn’t passing a reference equivalent to passing the original object itself? To answer these questions, take a closer look at the relationship between Java objects and the variables that are declared to hold them. Think about what really is happening in this statement:
String s = new String("Hello World");
Here, s is a variable of class String. The operator new allocates enough memory for a String object and calls the constructor for string with the argument “Hello World”. The return value for the operator new is a handle to the newly created String object. A handle to an object is basically an indicator to a location in memory. You might be familiar with pointers from the C and C++ programming languages. The handle is similar to a pointer; it does “point” to an object. Unlike the more traditional pointers, though, a handle to a Java object cannot be modified except in the case of assignment to variables. A Java reference variable can be reassigned to a different object.
The implications of the differences between handles and pointers are subtle but important. When a reference type is passed as an argument to a method, the handle to the object is copied and passed—not the object itself So, in this code segment, the output would be “Hello”:
String s = new String( "Hello" );
change( s );
System.out.println( s );
. . .
public void change( String t )
{
t = new String( "World" );
}
The handle to the object containing “Hello” is passed to change() as String t. t is reassigned to the new object containing “World”, but s remains unchanged. So, on the return of the function, “World” is left unreferenced, and the memory it occupies eventually is reclaimed by the garbage collector.
So, any handle that we want to be reassigned during a method call must be the return value for the method, or the handle must be a member of an enclosing or wrapper class.
In the following example, a new string containing “Hello” is created:
String s = new String("Hello");
s = s.concat(" World");
When the concat() method then is used, a new string is created in the concat() method containing “Hello World” and is returned to the calling routine. This new string is completely unrelated to the original string “Hello”. The concat() method is defined to return a String object.
In the next example, StringWrapper contains as a member field a String object:
class StringWrapper
{
public String s;
}
. . .
changeString( StringWrapper t )
{
t.s += " World";
}
StringWrapper s = new StringWrapper();
s.s = "Hello";
changeString(s);
Here, the StringWrapper object is passed as an argument to changeString(), and StringWrapper.s is reassigned to the new string “Hello World”. After returning from the call to changeString(), the calling routine has access to the new “Hello World” string. A core class called StringBuffer provides a mutable String class. This class is much more complete than this simple example here.