Primitive vs Object Types

advertisement
COMP 114
Prasun Dewan1
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,
1
 Copyright Prasun Dewan, 1998.

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.
8
5.5
5.5
16
5
5
5.5
double d
5
int i
5.5
double e
48
52
80
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 I2 = 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
2
Just in this chapter, we will break the Java convention of starting the names of variables with
lowercase letters so that we can easily distinguish between primitive and object variables.
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.
8
5.5
Double@8
16
5
Integer@16
48
8
Double D
60
16
Integer I
72
8
Number N
80
50
APoint@80
100
96
80
Figure 2: Object Values and Variables
APoint P
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.
8
50
APoint@8
50
16
100
APoint@16
100
48
75
ABoundedPoint@48
75
8
16
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.
8
100
APoint@8
50
16
8
p1
48
8
p2
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:
P1
P2
APoint@8
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:
8
100
APoint@8
50
16
64
p1
48
8
p2
64
200
APoint@64
200
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.
16
64
p1
48
64
p2
64
200
APoint@64
200
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.
72
200
APoint@8
200
16
64
p1
48
72
p2
64
200
APoint@64
200
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.
In String, this method does a character-by-character comparison of the strings that are compared,
an returns true if the two strings have the same sequence of characters.The following interaction
shows the difference between == and equals() for strings:
String s1 = “hello world”;
String s2 = “hello world”;
System.out.println(s1==s2);
System.out.println(s1.equals(s2));
s1 == s2;
System.out.println(s1==s2);
System.out.println(s1.equals(s2));
All print statements except the first one will print “true”. The equals() method for String
For each new type we define, we must provide our own implementation of equals(). For
example, in the class APoint, we might define equals() as:
public boolean equals(Point otherPoint) {
return x == otherPoint.getX() && y == otherPoint.getY();
}
Now, the statement:
System.out.println(p1.equals(p2))
will indeed print “true”, assuming the memory configuration of Figure 7.
Shallow vs. Deep Copying
Consider the example:
p1 = new APoint(200, 200);
p2 = p1;
p1.setX (100);
What if did not want p1 and p2 to share the same object? Instead, we wanted the initial value of
p2to be a copy of the object referenced by p1; and later wanted to change the copy without
affecting the original object? We might want to do so because we want a backup of p1 to which
we would like to revert later.
Since assignment does not do the job for us, we somehow need to extract the information in
APoint and use this to create a new instance with the same state. AnUCEditableString does not
currently allow us to do so, so we can try one of two approaches to changing it:

We can use the getters to retrieve the values of the properties of the original object, and use
them in the copier:
p2 = new APoint (p1.get(), p1.getY())\
This approach requires the copier to do the copying, which may not seem much work in this
case, but would be more if the object to be copied had several instance variables.

We can let the object to be copied do the work and provide a method for copying itself. The
copier simply calls this method:
p2 = p1.copy()
The method would do the work that the copier was doing in the previous case:
public Point copy() { return new APoint(x, y); }
Let us consider a more complicated class such as ABoundedPoint. We can define a similar copy
method:
public ABoundedPoint copy() { return new ABoundedPoint (x, y, upperLeftCorner,
lowerRightCorner); };
When this method is invoked in an instance, p1, it creates a new instance, p2, of ABoundedPoint,
passing to its constructor the four instance variables in p1. The constructor of p2 assigns the
arguments to its instance variables. As a result, we end up with the following picture:
ABoundedPoint@48
75
75
APoint@16
50
50
ABoundedPoint@96
APoint@24
100
75
75
100
Pointer Variable
Primitive Variable
Figure 8: Shallow Copy
Each line in the picture represents an instance variable of the object. The two primitive instance
variables, x and y, of the new copy are assigned copies of the values of the x and y variables of
the original object. The two pointer variables in the copy, however are the assigned copies of the
pointers to the (APoint) objects referenced by the upperLeftCorner and lowerRightCorner of the
original object. These subobjects of the original object are not themselves copied. As a result, the
copy shares these subobjects with the original .
This kind of copy is called a shallow copy. A shallow copy of an object makes a copy of the
object but not its subobjects.
The opposite of shallow copy is a deep copy. A deep copy of an object makes a copy of the
object and (deep or shallow) copies of its subobjects. Thus, the following is an example of a deep
copy:
public ABoundedPoint copy() {
return new ABoundedPoint (x, y, upperLeftCorner.copy(), lowerRightCorner.copy());
};
Here we assume that APoint defines an appropriate copy method:
public APoint copy() {
return new APoint(x,y);
}
Since APoint has no subobjects (it only has primitive variables) this copy is both a shallow and a
deep copy. In general, however, this copy would have been either a shallow or a deep copy. In
either case, the second copy of ABoundedPoint would be called deep copy.
We see here in the copy() method of ABoundedPoint our first example of a recursive method on a
recursive data structure. It will result in the following picture:
ABoundedPoint@48
75
75
APoint@16
50
50
ABoundedPoint@96
APoint@24
100
Pointer Variable
75
100
75
APoint@32
50
50
APoint@36
100
100
Primitive Variable
Figure 9: Deep Copy
The method works from bottom to up, that is, it creates copies of subobjects before it creates
copies of the parent object. Thus, the copies of the two APoint objects are created before the copy
of the ABoundedPoint instance.
Summary





Primitive values cannot be passed as arguments to methods expecting arbitrary objects.
Moreover, the benefits of inheritance are not available to their types.
They can be wrapped into and unwrapped from instances of wrapper classes.
The size of the memory block created for a primitive value is the number of bits required to
store it.
The size of the memory block created for a primitive variable of a certain primitive type is the
size of a value of that type. Assigning a primitive value to a primitive variable copies the
memory block created for the value to the memory block created for the variable.
The size of the memory block created for an object is the sum of the sizes of the instance
variables of the object.

The size of the memory block created for an object variable is the size of an address, which is
1 word. Assigning an object to an object variable stores the address of the object in the
variable.
 Two object variables can point to the same object by storing the same address.
 When no variables points to an object, the object is garbage collected.
 The operation == checks if two objects are the same physical object, while the operation
equals() should check if two objects represent the same logical entity.
Exercises
1. Write a class that implements a history of integers, providing the following interface:
public interface IntHistory {
public void addElement(int newVal);
public int size();
public Integer elementAt(int index);
public int intElementAt(int index);
}
The class should store the history as a vector consisting of instances of the wrapper class,
Integer. The difference between elementAt() and intElementAt() is that the former returns
the history element at the specified index as an Integer object while the latter returns it as
an int value. The following figure shows interaction with an instance of the class using
ObjectEditor:
2. Consider the following statements:
String s1 == “hello world”;
String s2 == “hello world”;
String s1 = s2;
Draw the memory contents at the end of the second and third statements, identifying any
garbage that is collected.
3. Define the equals() methods in the interfaces Loan and LoanPair, and implement
them in the classes, ALoanPair, ALoan, and AnotherLoan.
4. Consider the following program:
import java.awt.Rectangle;
public class RectangleDriver {
public static void main (String[] args) {
Rectangle r1 = new Rectangle(0, 0, 10, 20);
Rectangle r2 = copy(r1);
r1.x = 10;
System.out.println(r2.x);
}
public static Rectangle copy(Rectangle r) {
return new Rectangle(r.getBounds());
}
}
(a) If we run the program, it will print 10. Explain why it does not print 0. Recall that the
getBounds() is a method provided on a shape that returns a Rectangle defining the
bounds (x, y, width, height) of the shape.
(b) Rewrite copy() so that it makes a true copy of the rectangle, allowing the original
rectangle and the copy to be changed independently.
(c) The kind of copy() the method above does is called shallow copy. The kind of copy the
new method (of part(b) of this question) is required to do, is called deep copy. Can you
suggest the reasons for using these terms for the two kinds of copies?
Download