Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

AhmadLang / Java, How To Program, 2004

.pdf
Скачиваний:
626
Добавлен:
31.05.2015
Размер:
51.82 Mб
Скачать

[Page 876]

18.4. Additional Compile-Time Translation Issues: Methods That Use a Type Parameter as the Return Type

Let's consider a generic method example in which type parameters are used in the return type and in the parameter list (Fig. 18.5). The application uses a generic method maximum to determine and return the largest of its three arguments of the same type. Unfortunately, the relational operator > cannot be used with reference types. However, it is possible to compare two objects of the same class if that class implements the generic interface Comparable< T > (package java.lang). All the type-wrapper classes for primitive types implement this interface. Like generic classes, generic interfaces enable programmers to specify, with a single interface declaration, a set of related types. Comparable< T > objects have a compareTo method. For example, if we have two Integer objects, integer1 and integer2, they can be compared with the expression:

integer1.compareTo( integer2 )

Figure 18.5. Generic method maximum with an upper bound on its type parameter.

(This item is displayed on page 877 in the print version)

1

//

Fig. 18

.5: MaximumTest

.java

2

//

Generic

method maximum

returns the largest of three objects.

3

 

 

 

 

4public class MaximumTest

5{

6

// determines

the

largest

of

three

Comparable

objects

7

public static

< T

extends

Comparable<

T >

> T

maximum( T x, T y, T z )

8

{

 

 

 

 

 

 

 

 

9

T max = x;

//

assume x

is

initially

the

largest

10

 

 

 

 

 

 

 

 

 

11

if ( y.compareTo( max ) >

0 )

 

 

 

 

12

max = y;

//

y is the

largest

so

far

 

 

13

 

 

 

 

 

 

 

 

 

14if ( z.compareTo( max ) > 0 )

15max = z; // z is the largest

17return max; // returns the largest object

18} // end method maximum

20public static void main( String args[] )

21{

22System.out.printf( "Maximum of %d, %d and %d is %d\n\n", 3, 4, 5,

23maximum( 3, 4, 5 ) );

24System.out.printf( "Maximum of %.1f, %.1f and %.1f is %.1f\n\n",

25

6.6, 8.8, 7.7, maximum( 6.6, 8.8, 7.7 ) );

26System.out.printf( "Maximum of %s, %s and %s is %s\n", "pear",

27"apple", "orange", maximum( "pear", "apple", "orange" ) );

28} // end main

29} // end class MaximumTest

Maximum of 3, 4 and 5 is 5

Maximum of 6.6, 8.8 and 7.7 is 8.8 Maximum of pear, apple and orange is pear

It is the responsibility of the programmer who declares a class that implements Comparable< T > to declare method compareTo such that it compares the contents of two objects of that class and returns the results of the comparison. The method must return 0 if the objects are equal, -1 if object1 is less than object2 or 1 if object1 is greater than object2. For example, class Integer's compareTo method compares the int values stored in two Integer objects. A benefit of implementing interface Comparable< T > is that Comparable< T > objects can be used with the sorting and searching methods of class Collections (package java.util). We discuss those methods in Chapter 19, Collections. In this example, we'll use method compareTo in method maximum to help determine the largest value.

Generic method maximum (lines 718) uses type parameter T as the return type of the method (line 7), as the type of method parameters x, y and z (line 7), and as the type of local variable max (line 9). The type parameter section specifies that T extends Comparable< T >only objects of classes that implement interface Comparable< T > can be used with this method. In this case, Comparable is known as the upper bound of the type parameter. By default, Object is the upper bound. Note that type parameter declarations that bound the parameter always use keyword extends regardless of whether the type parameter extends a class or implements an interface. This type parameter is more restrictive than the type parameter specified for printArray in Fig. 18.3, which was able to output arrays containing any type of object. The restriction of using Comparable< T > objects is important because not all objects can be compared. However, Comparable< T > objects are guaranteed to have a compareTo method.

[Page 877]

Method maximum uses the same algorithm that we used in Section 6.4 to determine the largest of its three arguments. The method assumes that its first argument (x) is the largest and assigns it to local variable max (line 9). Next, the if statement at lines 1112 determines whether y is greater than max. The condition invokes y 's compareTo method with the expression y.compareTo( max ), which returns -1, 0 or 1, to determine y 's relationship to max. If the return value of the compareTo is greater than 0, then y is greater and is assigned to variable max. Similarly, the if statement at lines 1415 determines whether z is greater than max. If so, line 15 assigns z to max. Then, line 17 returns max to the caller.

In main (lines 2028), line 23 calls maximum with the integers 3, 4 and 5. When the compiler encounters this call, it first looks for a maximum method that takes three arguments of type int. There is no such method, so the compiler looks for a generic method that can be used and finds generic method maximum. However, recall that the arguments to a generic method must be of a reference type. So the compiler autoboxes the three int values as Integer objects and specifies that the three Integer objects will be passed to maximum. Note that class Integer (package java.lang) implements interface Comparable< Integer > such that method compareTo compares the int values in two Integer objects. Therefore, Integers are valid arguments to method maximum. When the Integer representing the maximum is returned, we attempt to output it with the %d format specifier, which outputs an int primitive type value. So maximum's return value is output as an int value.

[Page 878]

A similar process occurs for the three double arguments passed to maximum in line 25. Each double is autoboxed as a Double object and passed to maximum. Again, this is allowed because class Double (package java.lang) implements the Comparable< Double > interface. The Double returned by maximum is output with the format specifier %.1f, which outputs a double primitive type value. So maximum's return value is auto-unboxed and output as a double. The call to maximum in line 27 receives three Strings, which are also Comparable< String > objects. Note that we intentionally placed the largest value in a different position in each method call (lines 23, 25 and 27) to show that the generic method always finds the maximum value, regardless of its position in the argument list.

When the compiler translates generic method maximum into Java bytecodes, it uses erasure (introduced in Section 18.3) to replace the type parameters with actual types. In Fig. 18.3, all generic types were replaced with type Object. Actually, all type parameters are replaced with the upper bound of the type parameterunless specified otherwise, Object is the default upper bound. The upper bound of a type parameter is specified in the type parameter section. To indicate the upper bound, follow the type parameter's name with the keyword extends and the class or interface name that represents the upper bound. In method maximum's type parameter section (Fig. 18.5), we specified the upper bound as type Comparable< T >. Thus, only Comparable< T > objects can be passed as arguments to maximumanything that is not Comparable< T > will result in compilation errors. Figure 18.6 simulates the erasure of method maximum's types by showing the method's source code after the type parameter section is removed and type parameter T is replaced with the upper bound, Comparable, throughout the method declaration. Note that the erasure of Comparable< T > is simply Comparable.

Figure 18.6. Generic method maximum after erasure is performed by the compiler.

1

public static Comparable maximum(Comparable x,

Comparable y, Comparable z)

2

{

 

 

 

 

 

 

 

 

3

Comparable

max

=

x;

//

assume x

is

initially

the largest

4

 

 

 

 

 

 

 

 

 

5

if ( y.compareTo( max )

> 0 )

 

 

 

6

max = y;

//

y

is

the

largest

so

far

 

7

 

 

 

 

 

 

 

 

 

8

if ( z.compareTo( max )

> 0 )

 

 

 

9

max = z;

//

z

is

the

largest

 

 

 

10

11return max; // returns the largest object

12} // end method maximum

After erasure, the compiled version of method maximum specifies that it returns type Comparable. However, the calling method does not expect to receive a Comparable. Rather, the caller expects to receive an object of the same type that was passed to maximum an argumentInteger, Double or String in this example. When the compiler replaces the type parameter information with the upper bound type in the method declaration, it also inserts explicit cast operations in front of each method call to ensure that the returned value is of the type expected by the caller. Thus, the call to maximum in line 23 (Fig. 18.5) is preceded by an Integer cast, as in

[Page 879]

(Integer) maximum( 3, 4, 5 )

the call to maximum in line 25 is preceded by a Double cast, as in

(Double) maximum( 6.6, 8.8, 7.7 )

and the call to maximum in line 27 is preceded by a String cast, as in

(String) maximum( "pear", "apple", "orange" )

In each case, the type of the cast for the return value is inferred from the types of the method arguments in the particular method call because, according to the method declaration, the return type and the argument types match. Note that you cannot use a method that accepts Objects because class Object provides only an equality comparison. Also note that without generics, programmers are responsible for the casting operation.

[Page 879 (continued)]

18.5. Overloading Generic Methods

A generic method may be overloaded. A class can provide two or more generic methods that specify the same method name but different method parameters. For example, generic method printArray of Fig. 18.3 could be overloaded with another printArray generic method with the additional parameters lowSubscript and highSubscript to specify the portion of the array to output (see Exercise 18.5).

A generic method can also be overloaded by non-generic methods that have the same method name and number of parameters. When the compiler encounters a method call, it searches for the method declaration that most precisely matches the method name and the argument types specified in the call. For example, generic method printArray of Fig. 18.3 could be overloaded with a version that is specific to Strings, which outputs the Strings in neat, tabular format (see Exercise 18.6).

When the compiler encounters a method call, it performs a matching process to determine which method to invoke. The compiler tries to find and use a precise match in which the method names and argument types of the method call match those of a specific method declaration. If there is no such method, the compiler determines whether there is an inexact but applicable matching method.

[Page 879 (continued)]

18.6. Generic Classes

The concept of a data structure, such as a stack, can be understood independently of the element type it manipulates. Generic classes provide a means for describing the concept of a stack (or any other class) in a type-independent manner. We can then instantiate type-specific objects of the generic class. This capability provides a wonderful opportunity for software reusability.

[Page 880]

Once you have a generic class, you can use a simple, concise notation to indicate the actual type(s) that should be used in place of the class's type parameter(s). At compilation time, the Java compiler ensures the type safety of your code and uses the erasure techniques described in Section 18.3 and Section 18.4 to enable your client code to interact with the generic class.

One generic Stack class, for example, could be the basis for creating many Stack classes (e.g., "Stack of

Double," "Stack of Integer," "Stack of Character," "Stack of Employee," etc.). These classes are known as parameterized classes or parameterized types because they accept one or more parameters. Recall that type parameters represent only reference types, which means the Stack generic class cannot be instantiated with primitive types. However, we can instantiate a Stack that stores objects of Java's typewrapper classes and allow Java to use autoboxing to convert the primitive values into objects. Autoboxing occurs when a value of a primitive type (e.g., int) is pushed onto a Stack that contains wrapper-class objects (e.g., Integer). Auto-unboxing occurs when an object of the wrapper class is popped off the Stack and assigned it to a primitive type variable.

Figure 18.7 presents a generic Stack class declaration. A generic class declaration looks like a non-generic class declaration, except that the class name is followed by a type parameter section (line 4). In this case, type parameter E represents the element type the Stack will manipulate. As with generic methods, the type parameter section of a generic class can have one or more type parameters separated by commas. (You will create a generic class with two type parameters in Exercise 18.8.) Type parameter E is used throughout the Stack class declaration to represent the element type. [ Note: This example implements a Stack as an array.]

Figure 18.7. Generic class Stack declaration.

(This item is displayed on page 881 in the print version)

1// Fig. 18.7: Stack.java

2// Generic class Stack.

4public class Stack< E >

5

{

 

 

6

private final int size; // number of elements in

the stack

7

private int top; // location of the

top element

 

8

private E[] elements; // array that

stores stack

elements

9

 

 

 

10// no-argument constructor creates a stack of the default size

11public Stack()

12{

13this( 10 ); // default stack size

14} // end no-argument Stack constructor

15

16// constructor creates a stack of the specified number of elements

17public Stack( int s )

18{

19

size = s > 0

? s :

10; // set size of Stack

20

top = -1

;

//

Stack

initially empty

21

 

 

 

 

 

22

elements

=

(

E[] )

new Object[ size ]; // create array

23

} // end Stack

constructor

24

 

 

 

 

 

25// push element onto stack; if successful, return true;

26// otherwise, throw FullStackException

27public void push( E pushValue )

28

{

 

 

 

 

29

if ( top

== size - 1 ) // if

stack is full

30

throw

new

FullStackException(

String.format(

31

"Stack

is full, cannot

push

%s", pushValue ) );

32

 

 

 

 

 

33elements[ ++top ] = pushValue; // place pushValue on Stack

34} // end method push

35

36// return the top element if not empty; else throw EmptyStackException

37public E pop()

38{

39

if ( top

== -1 )

// if

stack

is empty

 

 

 

 

40

throw

new EmptyStackException( "Stack

is empty,

cannot

pop"

);

41

 

 

 

 

 

 

 

 

 

42

return elements[

top--

]; //

remove and

return top

element

of

Stack

43} // end method pop

44} // end class Stack< E >

Class Stack declares variable elements as an array of type E (line 8). This array will store the Stack's elements. We would like to create an array of type E to store the elements. However, the generics mechanism does not allow type parameters in array-creation expressions because the type parameter (in this case, E) is not available at runtime. To create an array with the appropriate type, line 22 in the oneargument constructor creates the array as an array of type Object and casts the reference returned by new to type E[]. Any object could be stored in an Object array, but the compiler's type-checking mechanism ensures that only objects of the array variable's declared type can be assigned to the array via any array-access expression that uses variable elements. Yet when this class is compiled using the - Xlint:unchecked option, e.g.,

javac -Xlint:unchecked Stack.java

the compiler issues the following warning message about line 22:

Stack.java:22: warning: [unchecked] unchecked cast found : java.lang.Object[]

required: E[]

elements = ( E[] ) new Object[ size ]; // create array

The reason for this message is that the compiler cannot ensure with 100% certainty that an array of type Object will never contain objects of types other than E. Assume that E represents type Integer, so that array elements should store Integer objects. It is possible to assign variable elements to a variable of type Object[], as in

Object[] objectArray = elements;

[Page 881]

Then any object can be placed into the array with an assignment statement like

objectArray[ 0 ] = "hello";

which places a String in an array that should contain only Integers, which would lead to runtime problems when the Stack is manipulated. As long as you do not perform statements like those shown here, your Stack will contain objects of only the correct element type.

[Page 882]

Method push (lines 2734) first determines whether an attempt is being made to push an element onto a full Stack. If so, lines 3031 throw a FullStackException. Class FullStackException is declared in Fig. 18.8. If the Stack is not full, line 33 increments the top counter and places the argument in that location

of array elements.

Figure 18.8. FullStackException class declaration.

1// Fig. 18.8: FullStackException.java

2// Indicates a stack is full.

3public class FullStackException extends RuntimeException

4{

5// no-argument constructor

6public FullStackException()

7{

8this( "Stack is full" );

9} // end no-argument FullStackException constructor

11// one-argument constructor

12public FullStackException( String exception )

13{

14super( exception );

15} // end one-argument FullStackException constructor

16} // end class FullStackException

Method pop (lines 3743) first determines whether an attempt is being made to pop an element from an empty Stack. If so, line 40 throws an EmptyStackException. Class EmptyStackException is declared in Fig. 18.9. Otherwise, line 42 returns the top element of the Stack, then postdecrements the top counter to indicate the position of the next top element.

Figure 18.9. EmptyStackException class declaration.

1// Fig. 18.9: EmptyStackException.java

2// Indicates a stack is full.

3public class EmptyStackException extends RuntimeException

4{

5// no-argument constructor

6public EmptyStackException()

7{

8this( "Stack is empty" );

9} // end no-argument EmptyStackException constructor

11// one-argument constructor

12public EmptyStackException( String exception )

13{

14super( exception );

15} // end one-argument EmptyStackException constructor

16} // end class EmptyStackException

Classes FullStackException (Fig. 18.8) and EmptyStackException (Fig. 18.9) each provide the conventional no-argument constructor and one-argument constructor of exception classes (as discussed in Section 13.11). The no-argument constructor sets the default error message, and the one-argument constructor sets a custom exception message.

[Page 883]

As with generic methods, when a generic class is compiled, the compiler performs erasure on the class's type parameters and replaces them with their upper bounds. For class Stack (Fig. 18.7), no upper bound is specified, so the default upper bound, Object, is used. The scope of a generic class's type parameter is the entire class. However, type parameters cannot be used in a class's static declarations.

Now, let's consider the test application (Fig. 18.10) that uses the Stack generic class. Lines 910 declare variables of type Stack< Double > (pronounced "Stack of Double") and Stack< Integer > (pronounced "Stack of Integer"). The types Double and Integer are known as the Stack's type arguments. They are

used by the compiler to replace the type parameters so that the compiler can perform type checking and insert cast operations as necessary. We'll discuss the cast operations in more detail shortly. Method testStack (called from main) instantiates objects doubleStack of size 5 (line 15) and integerStack of size 10 (line 16), then calls methods testPushDouble (lines 2544), testPopDouble (lines 4767), testPushInteger (lines 7089) and testPopInteger (lines 92112) to demonstrate the two Stacks in this example.

Figure 18.10. Generic class Stack test program.

(This item is displayed on pages 883 - 886 in the print version)

1

//

Fig. 18.10: StackTest.java

2

//

Stack generic class test program.

3

 

 

4public class StackTest

5{

6

private

double[] doubleElements

=

{ 1.

1,

2

.2

,

3

.3,

 

4.

4

, 5

.5

,

6.6

};

 

7

private

int[] integerElements =

{

1, 2

,

3,

4

,

5

, 6

,

7

,

8,

9

,

10,

11

};

8

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

9private Stack< Double > doubleStack; // stack stores Double objects

10private Stack< Integer > integerStack; // stack stores Integer objects

12// test Stack objects

13public void testStacks()

14

{

 

 

 

 

15

doubleStack = new Stack< Double >( 5

);

//

Stack of

Doubles

16

integerStack = new Stack< Integer >(

10

);

// Stack

of Integers

17

 

 

 

 

 

18testPushDouble(); // push double onto doubleStack

19testPopDouble(); // pop from doubleStack

20testPushInteger(); // push int onto intStack

21testPopInteger(); // pop from intStack

22} // end method testStacks

23

24// test push method with double stack

25public void testPushDouble()

26{

27// push elements onto stack

28try

29{

30System.out.println( "\nPushing elements onto doubleStack" );

32// push elements to Stack

33for ( double element : doubleElements )

34{

35

System.out.printf( "%.1 f

",

element

);

36

doubleStack.push( element

);

// push

onto doubleStack

37} // end for

38} // end try

39catch ( FullStackException fullStackException )

40{

41System.err.println();

42fullStackException.printStackTrace();

43} // end catch FullStackException

44} // end method testPushDouble

45

46// test pop method with double stack

47public void testPopDouble()

48{

49// pop elements from stack

50try

51{

52System.out.println( "\nPopping elements from doubleStack" );

53double popValue; // store element removed from stack

54

55// remove all elements from Stack

56while ( true )

57{

58

popValue =

doubleStack.pop(); // pop from doubleStack

59

System.out

.printf( "%.1 f ", popValue );

60} // end while

61} // end try

62catch( EmptyStackException emptyStackException )

63{

64System.err.println();

65emptyStackException.printStackTrace();

66} // end catch EmptyStackException

67} // end method testPopDouble

68

69// test push method with integer stack

70public void testPushInteger()

71{

72// push elements to stack

73try

74{

75System.out.println( "\nPushing elements onto intStack" );

77// push elements to Stack

78for ( int element : integerElements )

79{

80

System.out.printf(

"%d ", element );

81

integerStack.push(

element ); // push onto integerStack

82} // end for

83} // end try

84catch ( FullStackException fullStackException )

85{

86System.err.println();

87fullStackException.printStackTrace();

88} // end catch FullStackException

89} // end method testPushInteger

90

91// test pop method with integer stack

92public void testPopInteger()

93{

94// pop elements from stack

95try

96{

97System.out.println( "\nPopping elements from intStack" );

98int popValue; // store element removed from stack

99

100// remove all elements from Stack

101while ( true )

102{

103

popValue =

integerStack.pop(); // pop

from intStack

104

System.out

.printf( "%d ", popValue );

 

105} // end while

106} // end try

107catch( EmptyStackException emptyStackException )

108{

109System.err.println();

110emptyStackException.printStackTrace();

111} // end catch EmptyStackException

112} // end method testPopInteger

113

114public static void main( String args[] )

115{

116StackTest application = new StackTest();

117application.testStacks();

118} // end main

119} // end class StackTest

Pushing elements onto doubleStack 1.1 2.2 3.3 4.4 5.5 6.6

FullStackException: Stack is full, cannot push 6.6 at Stack.push(Stack.java:30)

at StackTest.testPushDouble(StackTest.java:36) at StackTest.testStacks(StackTest.java:18)

at StackTest.main(StackTest.java:117)

Popping elements from doubleStack 5.5 4.4 3.3 2.2 1.1

EmptyStackException: Stack is empty, cannot pop at Stack.pop(Stack.java:40)

at StackTest.testPopDouble(StackTest.java:58) at StackTest.testStacks(StackTest.java:19)

 

 

 

 

 

at

StackTest.main(StackTest.java:117)

Pushing

 

elements

onto

integerStack

1

2

3

4

 

5

6

7

8

9

10

11

FullStackException: Stack is full, cannot push 11

 

 

 

 

 

at

Stack.push(Stack.java:30)

 

 

 

 

 

at

StackTest.testPushInteger(StackTest.java:81)

 

 

 

 

 

at

StackTest.testStacks(StackTest.java:20)

 

 

 

 

 

at

StackTest.main(StackTest.java:117)

Popping

 

elements

from

integerStack

10

 

9

8

7

6

5

4

3

2

1

 

EmptyStackException: Stack is empty, cannot pop

 

 

 

 

 

at

Stack.pop(Stack.java:40)

 

 

 

 

 

at

StackTest.testPopInteger(StackTest.java:103)

 

 

 

 

 

at

StackTest.testStacks(StackTest.java:21)

 

 

 

 

 

at

StackTest.main(StackTest.java:117)

 

 

 

 

 

 

 

 

 

 

 

 

Method testPushDouble (lines 2544) invokes method push to place the double values 1.1, 2.2, 3.3, 4.4 and 5.5 stored in array doubleElements onto doubleStack. The for loop terminates when the test program attempts to push a sixth value onto doubleStack (which is full, because doubleStack can store only five elements). In this case, the method throws a FullStackException (Fig. 18.8) to indicate that the Stack is full. Lines 3943 catch this exception and print the stack trace information. The stack trace indicates the exception that occurred and shows that Stack method push generated the exception at lines 3031 of the file Stack.java (Fig. 18.7). The trace also shows that method push was called by StackTest method testPushDouble at line 36 of StackTest.java, that method testPushDouble was called from method testStacks at line 18 of StackTest.java and that method testStacks was called from method main at line 117 of StackTest.java. This information enables you to determine the methods that were on the method-call stack at the time that the exception occurred. Because the program catches the exception, the Java runtime environment considers the exception to have been handled and the program can continue executing. Note that autoboxing occurs in line 36 when the program tries to push a primitive double value onto the doubleStack, which stores only Double objects.

[Page 886]

Method testPopDouble (lines 4767) invokes Stack method pop in an infinite while loop to remove all the values from the stack. Note in the output that the values indeed pop off in last-in-first-out order (this, of course, is the defining characteristic of stacks). The while loop (lines 5761) continues until the stack is empty (i.e., until an EmptyStackException occurs), which causes the program to proceed to the catch block (lines 6266) and handle the exception, so the program can continue executing. When the test program attempts to pop a sixth value, the doubleStack is empty, so the pop tHRows an EmptyStackException. Auto-unboxing occurs in line 58 when the program assigns the Double object popped from the stack to a double primitive variable. Recall from Section 18.4 that the compiler inserts cast operations to ensure that the proper types are returned from generic methods. After erasure, Stack method pop returns type Object. However, the client code in method testPopDouble expects to receive a double when method pop returns. So the compiler inserts a Double cast, as in

popValue = ( Double ) doubleStack.pop();

to ensure that a reference of the appropriate type is returned, auto-unboxed and assigned to popValue.

[Page 887]

Method testPushInteger (lines 7089) invokes Stack method push to place values onto integerStack until it is full. Method testPopInteger (lines 92112) invokes Stack method pop to remove values from integerStack until it is empty. Once again, note that the values pop off in last-in-first-out order. During the erasure process, the compiler recognizes that the client code in method testPopInteger expects to receive an int when method pop returns. So the compiler inserts an Integer cast, as in

popValue = ( Integer ) integerStack.pop();