Primitive Values vs Objects

advertisement
COMP 14
Prasun Dewan1
17. 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 indepth 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:
1
 Copyright Prasun Dewan, 1998.
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 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.
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.
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
APoint P
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.
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.
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