Chapter: Java The Complete Reference - The Java Language - Generics

Study Material, Lecturing Notes, Assignment, Reference, Wiki description explanation, brief detail

A Simple Generics Example

Let’s begin with a simple example of a generic class. The following program defines two classes. The first is the generic class Gen, and the second is GenDemo, which uses Gen.

A Simple Generics Example

 

Let’s begin with a simple example of a generic class. The following program defines two classes. The first is the generic class Gen, and the second is GenDemo, which uses Gen.

 

     //A simple generic class.

 

     //Here, T is a type parameter that

 

     //will be replaced by a real type

 

     //when an object of type Gen is created.

 

     class Gen<T> {

 

T ob; // declare an object of type T

 

     //Pass the constructor a reference to

 

     //an object of type T.

 

Gen(T o) { ob = o;

 

}

 

     //Return ob.

     T getob() {

 

return ob;

 

}

 

//Show type of T.

void showType() { System.out.println("Type of T is " +

 

ob.getClass().getName());

 

}

 

}

 

// Demonstrate the generic class.

class GenDemo {

 

public static void main(String args[]) {

 

     //Create a Gen reference for Integers.

     Gen<Integer> iOb;

 

     //Create a Gen<Integer> object and assign its

 

     //reference to iOb. Notice the use of autoboxing

 

     //to encapsulate the value 88 within an Integer object.

 

     iOb = new Gen<Integer>(88);

 

     //Show the type of data used by iOb.

 

iOb.showType();

 

     //Get the value in iOb. Notice that

 

     //no cast is needed.

 

int v = iOb.getob();

System.out.println("value: " + v);

 

System.out.println();

 

// Create a Gen object for Strings.

 

Gen<String> strOb = new Gen<String> ("Generics Test");

 

     //Show the type of data used by strOb.

     strOb.showType();

 

     //Get the value of strOb. Again, notice that no cast is needed.

 

String str = strOb.getob(); System.out.println("value: " + str);

 

}

 

}

The output produced by the program is shown here:

 

Type of T is java.lang.Integer value: 88

 

Type of T is java.lang.String value: Generics Test

 

Let’s examine this program carefully.

 

First, notice how Gen is declared by the following line:

class Gen<T> {

 

Here, T is the name of a type parameter. This name is used as a placeholder for the actual type that will be passed to Gen when an object is created. Thus, T is used within Gen whenever the type parameter is needed. Notice that T is contained within < >. This syntax can be generalized. Whenever a type parameter is being declared, it is specified within angle brackets. Because Gen uses a type parameter, Gen is a generic class, which is also called a parameterized type.

 

Next, T is used to declare an object called ob, as shown here:

 

T ob; // declare an object of type T

 

As explained, T is a placeholder for the actual type that will be specified when a Gen object is created. Thus, ob will be an object of the type passed to T. For example, if type String is passed to T, then in that instance, ob will be of type String.

Now consider Gen’s constructor:

 

Gen(T o) { ob = o;

 

}

 

Notice that its parameter, o, is of type T. This means that the actual type of o is determined by the type passed to T when a Gen object is created. Also, because both the parameter o and the member variable ob are of type T, they will both be of the same actual type when a Gen object is created.

 

The type parameter T can also be used to specify the return type of a method, as is the case with the getob( ) method, shown here:

 

T getob() { return ob;

}

 

Because ob is also of type T, its type is compatible with the return type specified by getob( ). The showType( ) method displays the type of T by calling getName( ) on the Class object returned by the call to getClass( ) on ob. The getClass( ) method is defined by Object and is

thus a member of all class types. It returns a Class object that corresponds to the type of the class of the object on which it is called. Class defines the getName( ) method, which returns a string representation of the class name.

The GenDemo class demonstrates the generic Gen class. It first creates a version of Gen for integers, as shown here:

 

Gen<Integer> iOb;

 

Look closely at this declaration. First, notice that the type Integer is specified within the angle brackets after Gen. In this case, Integer is a type argument that is passed to Gen’s type parameter, T. This effectively creates a version of Gen in which all references to T are translated into references to Integer. Thus, for this declaration, ob is of type Integer, and the return type of getob( ) is of type Integer.

Before moving on, it’s necessary to state that the Java compiler does not actually create different versions of Gen, or of any other generic class. Although it’s helpful to think in these terms, it is not what actually happens. Instead, the compiler removes all generic type information, substituting the necessary casts, to make your code behave as if a specific version of Gen were created. Thus, there is really only one version of Gen that actually exists in your program. The process of removing generic type information is called erasure, and we will return to this topic later in this chapter.

 

The next line assigns to iOb a reference to an instance of an Integer version of the Gen class:

iOb = new Gen<Integer>(88);

 

Notice that when the Gen constructor is called, the type argument Integer is also specified. This is because the type of the object (in this case iOb) to which the reference is being assigned is of type Gen<Integer>. Thus, the reference returned by new must also be of type Gen<Integer>. If it isn’t, a compile-time error will result. For example, the following assignment will cause a compile-time error:

 

iOb = new Gen<Double>(88.0); // Error!

 

Because iOb is of type Gen<Integer>, it can’t be used to refer to an object of Gen<Double>. This type checking is one of the main benefits of generics because it ensures type safety.

 

As the comments in the program state, the assignment

 

iOb = new Gen<Integer>(88);

 

makes use of autoboxing to encapsulate the value 88, which is an int, into an Integer. This works because Gen<Integer> creates a constructor that takes an Integer argument. Because an Integer is expected, Java will automatically box 88 inside one. Of course, the assignment could also have been written explicitly, like this:

 

iOb = new Gen<Integer>(new Integer(88));

 

However, there would be no benefit to using this version.

 

The program then displays the type of ob within iOb, which is Integer. Next, the program obtains the value of ob by use of the following line:

 

int v = iOb.getob();

Because the return type of getob( ) is T, which was replaced by Integer when iOb was declared, the return type of getob( ) is also Integer, which unboxes into int when assigned to v (which is an int). Thus, there is no need to cast the return type of getob( ) to Integer.

Of course, it’s not necessary to use the auto-unboxing feature. The preceding line could have been written like this, too:

 

int v = iOb.getob().intValue();

 

However, the auto-unboxing feature makes the code more compact.

 

Next, GenDemo declares an object of type Gen<String>:

 

Gen<String> strOb = new Gen<String>("Generics Test");

 

Because the type argument is String, String is substituted for T inside Gen. This creates (conceptually) a String version of Gen, as the remaining lines in the program demonstrate.

 

Generics Work Only with Reference Types

 

When declaring an instance of a generic type, the type argument passed to the type parameter must be a reference type. You cannot use a primitive type, such as int or char. For example, with Gen, it is possible to pass any class type to T, but you cannot pass a primitive type to a type parameter. Therefore, the following declaration is illegal:

 

Gen<int> intOb = new Gen<int>(53);

// Error, can't use primitive type

 

Of course, not being able to specify a primitive type is not a serious restriction because you can use the type wrappers (as the preceding example did) to encapsulate a primitive type. Further, Java’s autoboxing and auto-unboxing mechanism makes the use of the type wrapper transparent.

 

Generic Types Differ Based on Their Type Arguments

 

A key point to understand about generic types is that a reference of one specific version of a generic type is not type compatible with another version of the same generic type. For example, assuming the program just shown, the following line of code is in error and will not compile:

 

 

iOb = strOb; // Wrong!

 

Even though both iOb and strOb are of type Gen<T>, they are references to different types because their type parameters differ. This is part of the way that generics add type safety and prevent errors.

 

How Generics Improve Type Safety

 

At this point, you might be asking yourself the following question: Given that the same functionality found in the generic Gen class can be achieved without generics, by simply specifying Object as the data type and employing the proper casts, what is the benefit of making Gen generic? The answer is that generics automatically ensure the type safety of all operations involving Gen. In the process, they eliminate the need for you to enter casts and to type-check code by hand.

To understand the benefits of generics, first consider the following program that creates a non-generic equivalent of Gen:

 

    //NonGen is functionally equivalent to Gen

 

//but does not use generics.

 

class NonGen {

 

Object ob; // ob is now of type Object

 

     //Pass the constructor a reference to

 

     //an object of type Object

 

     NonGen(Object o) {

 

ob = o;

 

}

 

     //Return type Object.

 

Object getob() { return ob;

 

}

 

// Show type of ob.

void showType() {

 

System.out.println("Type of ob is " + ob.getClass().getName());

 

}

 

}

 

// Demonstrate the non-generic class.

class NonGenDemo {

 

public static void main(String args[]) { NonGen iOb;

 

     //Create NonGen Object and store

 

     //an Integer in it. Autoboxing still occurs.

 

     iOb = new NonGen(88);

 

     //Show the type of data used by iOb.

 

     iOb.showType();

 

     //Get the value of iOb.

 

  //This time, a cast is necessary.

 

int v = (Integer) iOb.getob();

System.out.println("value: " + v);

 

System.out.println();

 

     //Create another NonGen object and

 

     //store a String in it.

 

NonGen strOb = new NonGen("Non-Generics Test");

 

     //Show the type of data used by strOb.

     strOb.showType();

 

//Get the value of strOb.

String str = (String) strOb.getob();

 

System.out.println("value: " + str);

 

// This compiles, but is conceptually wrong!

iOb = strOb;

 

v = (Integer) iOb.getob(); // run-time error!

 

}

 

}

 

There are several things of interest in this version. First, notice that NonGen replaces all uses of T with Object. This makes NonGen able to store any type of object, as can the generic version. However, it also prevents the Java compiler from having any real knowledge about the type of data actually stored in NonGen, which is bad for two reasons. First, explicit casts must be employed to retrieve the stored data. Second, many kinds of type mismatch errors cannot be found until run time. Let’s look closely at each problem.

 

Notice this line:

 

int v = (Integer) iOb.getob();

 

Because the return type of getob( ) is Object, the cast to Integer is necessary to enable that value to be auto-unboxed and stored in v. If you remove the cast, the program will not compile. With the generic version, this cast was implicit. In the non-generic version, the cast must be explicit. This is not only an inconvenience, but also a potential source of error.

 

Now, consider the following sequence from near the end of the program:

 

// This compiles, but is conceptually wrong!

iOb = strOb;

 

v = (Integer) iOb.getob(); // run-time error!

 

Here, strOb is assigned to iOb. However, strOb refers to an object that contains a string, not an integer. This assignment is syntactically valid because all NonGen references are the same, and any NonGen reference can refer to any other NonGen object. However, the statement is semantically wrong, as the next line shows. Here, the return type of getob( ) is cast to Integer, and then an attempt is made to assign this value to v. The trouble is that iOb now refers to an object that stores a String, not an Integer. Unfortunately, without the use of generics, the Java compiler has no way to know this. Instead, a run-time exception occurs when the cast to Integer is attempted. As you know, it is extremely bad to have run-time exceptions occur in your code!

 

The preceding sequence can’t occur when generics are used. If this sequence were attempted in the generic version of the program, the compiler would catch it and report an error, thus preventing a serious bug that results in a run-time exception. The ability to create type-safe code in which type-mismatch errors are caught at compile time is a key advantage of generics. Although using Object references to create “generic” code has always been possible, that code was not type safe, and its misuse could result in run-time exceptions. Generics prevent this from occurring. In essence, through generics, run-time errors are converted into compile-time errors. This is a major advantage.


Study Material, Lecturing Notes, Assignment, Reference, Wiki description explanation, brief detail


Copyright © 2018-2020 BrainKart.com; All Rights Reserved. Developed by Therithal info, Chennai.