COMP 114

Prasun Dewan[1]

12. Primitive Values vs, Objects

By now, we are used to distinguishing between primitive values and objects. We know, for instance, that subtyping is provided for object types but not primitive types. In this chapter, we will focus a bit more in-depth on the differences between these two kinds of values. We will look at special types, called wrapper classes, that provide bridges between primitive values and objects. We will also study the difference between the way these two kinds of values are stored in memory, and the implication this difference has for the assignment statement. This will lead us to the concept of garbage collection, an important feature of Java.

Wrapper Classes

The class, Object, we saw earlier defines the behavior of all Java objects because the type of each object is directly or indirectly a subtype of Object. It does not, however, define the behavior of primitive values, which are not objects. In fact, its not possible to define in Java a type, Primitive, that describes all primitive values, or a type, Value, that describes all Java values, because subtyping is not available for primitive types.

Treating primitive values and objects as fundamentally different has two related disadvantages. First, primitive values cannot be assigned passed as arguments to methods expecting objects. For instance, given a vector, v, the following is illegal:

v. addElement(4)

because the type of the argument of the add element method is Object. It is not possible to create another kind of add element method that adds any primitive value or any value because Java does not have a type to describe the argument of such a method.

Second, primitive types are second-class types in that the benefits of inheritance are not applicable to them. For instance, we cannot create a new primitive type, say, natural,that describes natural numbers (positive integers) and inherits the implementation of arithmetic operations from int.

Primitive types are also present in other object-oriented programming languages such as C++, but some more pure object-oriented languages such as Smalltalk only have object types. Primitive types can be more efficiently implemented than object types, which was probably the reason for including them in Java. However, this benefit comes at the expense of ease of programming and elegance.

Fortunately, for each primitive type, Java defines a corresponding class, called a wrapper for that type, and provides mechanisms for automatically converting among instances of a primitive type and the corresponding wrapper class. A wrapper class:

·  provides a constructor to create a wrapper object from the corresponding primitive value,

·  stores the primitive value in an instance variable,

·  provides a getter method to read the value.

For example, it provides the wrapper class, Integer, for the primitive type, int. The constructor:

public Integer(int value);

can be used to wrap a new object around the int value passed to the constructor, and the getter instance method:

public int intValue()

can be used to retrieve the value.

To illustrate how a wrapper class may be used, consider the problem of storing the primitive value, 4 in the vector, v. We can wrap the value into an integer object, and store this object in the vector:

v.addElement (new Integer (4))

To extract the primitive value from the vector, we must first access the corresponding object, and then use its getter method to unwrap the primitive value:

int i = v.elementAt(3).intValue()

Here we are assuming that the wrapper object was stored at the third position of the vector.

Besides wrapping and unwrapping primitive values, a wrapper class may also provide useful class methods for manipulating these values. For example, we have seen that we have used a class method to convert a string to the corresponding int value:

int i = Integer.parseInt(“4”);

and another Integer class method to convert an int value to the corresponding string:

String s = Integer.toString(4);

We must look at the documentation of each wrapper class to find what methods it provides.

The wrapper classes for the other primitive types, double, char, boolean, float, long, and short are Double, Character, Boolean, Float, Long and Short. The constructors and getter methods for wrapping and unwrapping values of these types are:

public Double(double value);

public double doubleValue();

public Character(char value);

public char charValue();

public Boolean(boolean value);

public boolean booleanValue();

public Float(float value);

public float floatValue();

public Long(long value);

public long longValue();

public Short(short value);

public short shortValue();

Storing Primitive Values and Variables

Consider how the assignment:

int i = 5;

is processed by the computer. From a programmer’s point of view, of course, the variable i gets assigned the value 5. However, let us look at under the hood and see what Java exactly does when processing this statement.

Java creates an integer value, 5, and stores it in a memory block. A memory block is simply a set of contiguous memory cells that store some program value. It also creates a memory block for the variable i. When the assignment statement is executed, the contents of the value block are copied into the variable block.

The two blocks have the same size because the value and the variable have the same type. As a result, the value “fits” exactly in the variable. The size of the block for an integer value is 1 word or 32 bits, as we saw earlier.

The statement:

double d = 5.5

is processed similarly except that Java manipulated blocks of 2 words instead of 1 word, because the size of a double is 2 words.

Assignment of one variable to another is handled similarly:

double e = d;

The contents of the RHS variable are copied into the block allocated for the LHS variable.

The following figure illustrates this discussion.

Figure 1: Primitive Values and Variables

Each memory block is identified by its memory address, which is listed on its left in the figure. While we think in terms of high-level specifiers such as i and 5, the processor, in fact, works in terms of these addresses. The compiler converts these names to addresses, so that the human and processor can speak different languages. It is for this reason that a compiler is also called a translator.

Sroring Object Values and Variables

Object values and variables, however, are stored differently. Consider:

Integer I[2] = new Integer (5);

Double D = new Double(5.5);

As before, both values and the variables are allocated memory. However, each assignment copies into the variable’s block, not the contents of the value block, but instead its address. All Java addresses are 1 word long, so all variables are allocated a 1-word block, regardless of their types. Thus, both the Double variable, D, and the Integer variable, I, are the same size, which was not the case with the double variable, d, and integer variable, i, we saw above.

All objects, however, are not of the same size. When a new object is created, a composite memory block consisting of a series of consecutive blocks, one for each instance variable of the object, is created. Thus, assuming an Integer has a single int instance variable, a block consisting of a single integer variable is created. Similarly, for a Double instance, a block consisting of a single double instance variable is created. The sizes of the two objects are different because of the difference in the sizes of their instance variable. However, in both cases, the object block consists of a single variable.

Now consider the following class:

public class APoint implements Point {

int x,y;

public APoint (int initX, int initY) {

x = initX; y = initY;

}

public void setX(int newVal) {

x = newVal;

}

}

The figure below shows how the following assignment of an instance of this class is processed:

Point P = new APoint( 50, 100) ;

An instance of this class has a memory block consisting of two consecutive int blocks, as shown in the figure.

Figure 2: Object Values and Variables

Now consider the following subclass of APoint, called ABoundedPoint, that declares two APoint instance variables defining a rectangular area defining user-specified bounds of the point:

public class ABoundedPoint extends APoint {

APoint upperLeftCorner, lowerRightCorner;

public ABoundedPoint (int initX, int initY, Point initUpperLeftCorner, Point initLowerRightCorner) {

super(initX, initY);

upperLeftCorner = initUpperLeftCorner;

lowerRightCorner = initLowerRightCorner;

}

}

Recall that an instance has not only the instance variables defined in its class but also those defined the superclasses of its class. Therefore, an instance of ABoundedPoint, has a memory block consisting of memory blocks of four variables, two int variables, each of size 1 word, inherited from APoint, and two object variables, each also of size 1 word, defined in ABoundedPoint.

Figure 3: Inherited Variables

Since an object variable stores addresses, it also called a pointer variable or reference variable, and the address stored in it a pointer or reference.

Variable reference is more complicated when the variable is a pointer variable. Consider:

System.out.println(i)

Java accesses memory at the address associated with i, and uses the value stored in the println. In contrast, consider:

System.out.println(I)

Java accesses memory at the address associated with I, finds another address there, and then uses this address to find the integer value. Thus, we do not go directly from a variable address to its value, but instead, indirectly using the value address or pointer. In some languages, the programmer is responsible for doing the indirection or dereferencing. For instance, in Pascal, given an integer pointer variable I, we need to type:

I^

to refer to the value to which it refers. Thus, the equivalent statement in Pascal would be:

writeln(I^)

Java, however, automatically does the dereferencing for us. In fact, we cannot directly access the address stored in it. Thus, we are not even aware that the variable is a pointer variable. Sometimes, the term pointer is used for a variable that must be explicitly dereferenced and reference for a variable that is automatically dereferenced. For this reason, some people say that Java has no pointer variables. However, we will use these two terms interchangeably.

The special value, null, we saw before, can be assigned to a pointer variable:

Object O = null;

In fact, if we do not initialize a pointer variable, this is the value stored in its memory block. It denotes the absence of a legal object assigned to the variable. This value is not itself a legal object, and does not have a class. If we try to access a member of this value:

null.toString();

we get a NullPointerException, which some of you may have already seen. However, we can use it to determine the value of a pointer variable:

if (O == null)

else

….

Pointer Assignment

Assignment can be tricky with pointers. Consider:

Point p1 = new APoint (50, 50);

Point p2 = p1;

p1.setX(100);

System.out.println(p2.getX());

When p1 is assigned to p2, the pointer stored in p1 is copied into p2, not the object itself. Both variables share the same object, and thus, the code will print 100 and not 50, as you might expect.

Figure 4: Sharing an Object

Sharing allows us to create graph structures, such as the one shown in Figure 4, which may also be represented as:

Figure 5: Alternative Representation of Sharing

You will study such structures in more depth in a data structure course. They support useful applications such as two Web pages pointing to the same page.

Garbage Collection

What if we now assign to p1, another object:

p1 = new APoint(200, 200);

System.out.println(p2.getX());

The memory contents will now be:

We will still get the same output, since p2 continues to point to the previous object.

What if we now execute the code:

p2 = p1;

System.out.println(p2.getX());

Now, of course, we will print 200; but what happens to the previous object? No variable refers to the object, so it is garbage collected. With each object, Java keeps a count, called a reference count, that tracks how many object variables store pointers to it. When this count goes to zero, it collects the object as garbage, since no other variable will ever be able to point to it again.

Figure 6: Unreferenced Object

Automatic garbage collection is a really nice feature of Java since in most traditional languages such as C the programmer is responsible for deleting objects. In such languages, the danger is that we may accidentally delete something that is being used, thereby creating dangling pointers to it, or forget to delete something that is not being used, thereby creating a memory leak that keeps wasting memory.

equals() vs. ==

Now consider the following statements:

System.out.println(p1 == p2);

p1 = new APoint (200, 200);

System.out.println(p1 == p2);

The == operator dereferences the two pointers, and compares the resulting objects. When the first statement is executed, both p1 and p2 refer to the same object. Therefore, we can expect the first print statement to print “true”. But what about the second print statement? Both variables refer to the same logical point in the coordinate space, the point with the coordinates (200,200). However, they refer to different physical objects, as shown in Figure 7.

Figure 7 Two Physical Objects Representing the Same Logical Entity

The == operator, in fact, simply checks if its left and right hand side are the same physical object. If not, it returns the false value. It does not understand the concept of two physical objects being the same logical entity. It is the responsibility of each object to define a method that checks if two objects represent the same logical entity. The convention is to call this method, equals(). Several predefined classes such as String provide such a method.