Download as doc, pdf, or txt
Download as doc, pdf, or txt
You are on page 1of 16

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 programmers 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.

5.5

5.5

16

48

5.5

double d

52

int i

80

5.5

double e

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 variables 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
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 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.

5.5

Double@8

16 48

5 8

Integer@16 Double D

60 72 80

16 8 50 100

Integer I Number N

APoint@80

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.

50 50

APoint@8

16

100 100

APoint@16

48

75 75 8 16

ABoundedPoint@48

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.

100 50

APoint@8

16

p1

48

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:

100 50

APoint@8

16

64

p1

48 64

8 200 200

p2 APoint@64

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

64 200 200

p2 APoint@64

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 200

APoint@ 8

16

64

p1

48 64

72 200 200

p2 APoint@ 64

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 ABoundedPoint@96

75

75

APoint@16

APoint@24

75

75

50

50

100

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 ABoundedPoint@96

75

75

APoint@16

APoint@24

75

75

APoint@32

APoint@36

50

50

100

100

50

50

100

100

Pointer Variable

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?

You might also like