Storing objects on the heap
In this section, we are going to examine storing objects on the heap. Gaining a full understanding of this area requires a discussion comparing references and objects. We will examine their types, where they are stored, and crucially, their differences. A sample piece of code with an associated diagram will finish the section.
References
References refer to objects and enable us to access them. If we are accessing an object instance member, then we use the reference. If we are accessing a static (class) member, we use the class name.
References can be stored on both the stack and the heap. If the reference is a local variable in a method, then the reference is stored on the stack (in the local method array for that method frame). If the reference is an instance variable, then the reference is stored inside the object, on the heap.
By way of comparison with objects, we can have a reference of an abstract class but not an object of an abstract class. The same applies to interfaces – we can have an interface reference type, but you cannot instantiate an interface; that is, you cannot create an object of an interface type. Both situations are demonstrated in Figure 2.1:
Figure 2.1 – Object instantiation errors
In Figure 2.1, the references declared on lines 10
and 13
, an abstract class and an interface reference, respectively, have no issue. However, attempting to create an object of these types on lines 11
and 14
causes errors. Feel free to try out this code, contained in ch2
folder here: https://github.com/PacktPublishing/B18762_Java-Memory-Management/tree/main/ch2. The reason for the compiler errors is that you cannot create an object based on an abstract class or interface. We will address these errors in the next section.
Now that we have discussed references, let us examine objects.
Objects
All objects are stored on the heap. To understand objects, we must first understand a fundamental construct in OOP, the class. A class is similar to the plan of a house. With the plan of the house, you can view it and discuss it, but you cannot open any doors, put the kettle on, and so on. This is what classes are in OOP – they are views of what the object will look like in memory. When the house is built, you can now open the doors, have a cup of tea, and so forth. When the object is built, you have an in-memory representation of the class. Using the reference, we can access the instance members using the dot notation syntax.
Let us address the compiler issues from Figure 2.1 and, in addition, show the dot notation syntax in operation:
Figure 2.2 – The interface and abstract class references fixed
In Figure 2.2, as lines 11 and 15 compile without any error, they demonstrate that the class must be a non-abstract (concrete) class before an object based on it can be instantiated (created). Lines 12 and 16 demonstrate the dot notation syntax.
Let us now examine in more detail the creation of an object.
How to create objects
Objects are instantiated (created) using the new
keyword. The purpose of new
is to create an object on the heap and return its address, which we store in a reference variable. Line 11 from Figure 2.2 has the following line of code:
h =
new Person();
The reference is on the left-hand side of the assignment operator – we are initializing an h
reference of type Human
.
The object to be instantiated is on the right-hand side of the assignment operator – we are creating an object of type Person
, and the default Person
constructor is executed. This default constructor is synthesized by the compiler (as there is no explicit Person
constructor present in the code).
Now that we have looked at both objects and references, let us expand the example and, using a diagram, view both the stack and heap representations.
Understanding the differences between references and objects
In order to contrast the stack and the heap, both the Person
class and the main()
method have been changed:
Figure 2.3 – Stack and heap code
Figure 2.3 details a Person
class containing two instance variables, a constructor taking two parameters, and the toString()
instance method. The second class, StackAndHeap
, is the driver class (it contains the main()
method). In main()
, we initialize a local primitive variable, x
, and instantiate an instance of Person
.
Figure 2.4 shows the stack and heap representations after line 27 has been executed:
Figure 2.4 – A stack and heap representation of the code in Figure 2.3
Referring to Figure 2.3, the first method to execute is main()
on line 23. This results in a frame for main()
being pushed onto the stack. The local variables args
and x
are stored in the local variable array in this frame. On line 25, we create an instance of Person
passing in the String
literal, Joe Bloggs
, and the integer literal, 23
. Any String
literal is itself a String
object and is stored on the heap. In addition, as it is a String
literal, this String
object is stored in a special area of the heap called the String Pool (also known as the String Constant Pool).
The instance variable name
inside the Person
object resides on the heap and is a String
type; that is, it is a reference variable, and it refers to the Joe Bloggs object in the String pool. The other instance variable in Person
, namely age
, is a primitive, and its value of 23
is stored directly inside the object on the heap. However, the reference to the Person
object, joeBloggs
, is stored on the stack, in the frame for the main()
method.
On line 26 in Figure 2.3, we output the local variable, x
, which outputs 0
to the standard output device (typically the screen). Line 27 is then executed, as shown in Figure 2.4. First, the println()
method from PrintStream
(out
is of type PrintStream
) causes a frame to be pushed onto the stack. In order to simplify the diagram, we have not gone into any detail in that stack frame. Before println()
can complete execution, joeBloggs.toString()
must first be executed.
As the toString()
method in Person
has now been invoked/called, a new frame for toString()
is pushed onto the stack on top of the println()
frame. Next, toString()
builds up a local String
variable named decoratedName
using String
literals and the instance variables.
As you are probably aware, if you have a String
instance on the left or the right of a +
operator, the overall operation becomes a String
append and you end up with a String
result.
These String
literals are stored in the String Pool. The final String
result is My name is Joe Bloggs and I am 23 years old, which is assigned to the local variable, decoratedName
. This String is returned from toString()
back to the println()
statement on line 27
that called it. The returned String
is then echoed to the screen.
That concludes our section on storing objects on the heap. Now we will turn our attention to areas that can cause subtle issues in your code. However, now that we have separated the reference from the object, these issues will be much easier to understand and fix.