What is Generic Programming in Java

Chapter 10: Generics

Programming in Java: Introduction



This book is licensed under a Creative Commons license.


10.1 General

Generic programming

In Java 5, Sun introduced Generics - so-called parameterized data types. Accordingly, this happened relatively late. In all previous versions of Java 1.0 to 1.4 you cannot use everything that you will get to know in this chapter.

At first glance, generics complicate source code, so that Sun initially did not include these language properties in Java. They are the key to a programming paradigm called generic programming. The first Java versions therefore did not support generic programming. One of the reasons for this was that other programming languages ​​such as C ++ knew that generic programming places higher demands on software developers and is therefore not that easy to learn and use. In the previous versions of Java 5, it was therefore possible to program object-oriented, but not generically, while in C ++ both programming paradigms have been mixed for a long time.

Sun decided to support generic programming as of Java 5, because at that time a sufficient number of developers were familiar with Java and only had to learn generics when switching to the new version. In addition, Java came under pressure from C #, which supported generic programming from version 2.0. If Java did not want to fall behind, generic programming, which is somewhat complicated in practice, but quite useful, had to be supported.

In this chapter you will learn what is basically meant by generic programming and how you can use it in Java with generics. Java 5 or higher is required to be able to translate and execute the examples in this chapter.


10.2 Generic classes

Container as a prime example

To understand the advantages of generic programming, let's look at an example below of how it could be developed in Java 1.4 without generic programming.

import java.applet. *; import java.awt. *; import java.util. *; public class MyApplet extends Applet {private ArrayList Names = new ArrayList (); private Choice NamesChoice = new Choice (); public void init () {Names.add ("Anton"); Names.add ("Boris"); Names.add ("Caesar"); for (int i = 0; i A container of the type is used in the applet above. This class is called a container because an object of the type can hold several other objects. In the above example, multiple strings are added by repeated calls from the container.

The class represents an array that grows dynamically. While you have to specify how many elements the array should consist of when defining arrays, you can add new elements to a container of the type at any time - you just have to call them up. You can use the methods and to determine the number of elements currently stored and to access individual elements via an index. An object of the type therefore behaves very similarly to an array.

If you look at the source code above, you will notice that the return value of the method is cast to one in the loop. This is absolutely necessary because objects of the type can store objects of any data type - not just strings, but really everything. The method does not expect parameters of the type, but of the type. This is basically correct, because otherwise there would have to be a correspondingly adapted class for each data type in Java. Because internally stores type references, this class can be used to store objects of any data type.

A casting as in the example above works. And all Java programs up to version 1.4 looked like above. The problem with castings, however, is that we as developers seem to know a little better than the Java runtime environment. We tell the Java runtime environment that when elements are accessed, a conversion of the internally used data type should take place. The Java runtime has no choice but to trust that we are right. Because if we were to perform a casting for a different data type, an exception would be thrown. Castings are therefore seen as problematic because they easily lead to programming errors. These can be avoided with the help of generic programming.

import java.applet. *; import java.awt. *; import java.util. *; public class MyApplet extends Applet {private ArrayList Names = new ArrayList (); private Choice NamesChoice = new Choice (); public void init () {Names.add ("Anton"); Names.add ("Boris"); Names.add ("Caesar"); for (int i = 0; i In the example above, the generic class is used as it has existed since Java 5. As you can see, exactly the same class is being accessed as before.

A parameter must be passed to the generic class in angle brackets. This parameter must be of a data type. For example, in the example above, it is indicated that the list will store items of type. A generic class therefore does not store objects of any data type - that is, objects of the type - but only objects of the type specified as a parameter in angle brackets.

The use of the generic class means that casting is no longer necessary. Because the Java compiler now knows that only strings are stored in, it is also clear that only strings are returned. If we tried to cast the return value of to an incompatible data type, the Java compiler would complain and refuse to translate the code. The faulty casting would therefore be discovered during compilation and not just at runtime.

Generic programming makes it possible to adapt data types to other data types. Containers such as are considered a prime example of generic programming, since containers, by definition, hold objects. If containers can be told what data type the corresponding objects have, the Java compiler can use this information to our advantage and check for compilation whether our code is correct. For example, thanks to the generic class, it is impossible to accidentally save an object that is not a string with - something that would be possible in the previous example and would only be discovered at runtime if the casting failed. Generic programming is therefore an aid to writing more secure code because it can be checked for correctness at an earlier point in time - namely at compilation and not just at runtime.


10.3 Generic interfaces

and the extended loop

Generic programming is not only supported by classes, but also by interfaces. The parameterized interfaces include, among other things. This interface is implemented by numerous containers, including the class. A special variant of the loop is based on this interface, which has only existed since Java 5, and which has therefore also only existed since Java 5.

import java.applet. *; import java.awt. *; import java.util. *; public class MyApplet extends Applet {private ArrayList Names = new ArrayList (); private Choice NamesChoice = new Choice (); public void init () {Names.add ("Anton"); Names.add ("Boris"); Names.add ("Caesar"); for (String s: Names) NamesChoice.add (s); add (NamesChoice); }}

The loop used in the example above iterates over all elements in the list. In each loop pass, the corresponding element is made available in the variable and can thus be added. That works because the interface implements.

If you look up the documentation for the interface, you will see that it only defines one method. This method returns a so-called iterator that can be used to iterate over elements in a container. It is important that the returned iterator is also a parameterized data type. If the class is instantiated with the data type as in the example above, the interface is also instantiated, which in turn also instantiates the iterator that is returned by. The data type is passed on so that it is clear at all points in the code that strings are being used. This is important, otherwise the loop would not work as above. For example, if you remove the parameter and use the non-generic version of, as was necessary up to Java 1.4, the compiler will report an error. After all, a variable in the loop is supposed to store elements from the container - the non-generic class, however, only stores objects of the type. You can't even work with castings at this point because you don't have access to the code that the compiler creates in the background for the loop used above, for example to call.

What is the definition of the class roughly?

package java.util; import java.lang. *; public class ArrayList implements Iterable {public Iterator iterator () {} public boolean add (T t) {} public T get (int index) {}}

To make a class generic, a pair of angle brackets is placed after the class name. Inside those angle brackets is a parameter, commonly called T - T for type. This T represents the placeholder for the data type that will be specified later in angle brackets when the class is used. This process is also called instantiation.

The parameter T can now appear in different places - wherever the use of a data type makes sense. For example, the T in angle brackets can be passed on to an interface such as. Since this is a generic interface, a pair of pointed brackets must be specified after anyhow. The parameter T can now be set in these angle brackets as shown above: The data type with which the class is later instantiated when used is accordingly passed on to the interface. This in turn means that T must also be passed on to the class - the data type of the return value from.

Generic classes are therefore incomplete code that uses placeholders in numerous places. Only when the generic class is accessed and a data type is specified as a parameter in angle brackets does the class become a complete type. You can think of this type in such a way that the data type specified as a parameter appears in all the places where the placeholder T is in the source code. In this way it is possible to define containers as once and then adapt them for any data type so that secure code can be written.


10.4 Restrictions on Wildcards

Upper and lower limits

When you define a generic class, you can specify the data types with which the class can be instantiated. For example, consider the following class, which offers a method for determining the number of elements in a container.

import java.util. *; public class Length {T t; public Length (T t) {this.t = t; } public int length () {return t.size (); }}

In the angle brackets after the class name, there is no longer just one parameter T specified. There is a type restriction after T, which states that data types used for instantiation must be children of.

At first glance, this restriction seems arbitrary. However, if you take a closer look at the class, you notice that the variable is accessed in the method to invoke a method. Since the data type of the variable is the placeholder T and therefore depends on the data type used for instantiation, the compiler must somehow be able to ensure that the corresponding data type defines a method at all. For example, if the generic class were to be instantiated with the data type, there would be a problem because it has no method.

For generic classes, the compiler checks whether appropriate method calls are permitted for a placeholder. If you call methods that you know are not offered by any data type, you have to restrict the placeholder accordingly. Otherwise, a developer could get the idea of ​​instantiating their generic class with the data type, for example - and what should happen if a method is called that does not even offer? To prevent such a problem from occurring in the first place, the compiler carries out a corresponding check to determine whether the placeholder is correctly restricted.

import java.applet. *; import java.awt. *; import java.util. *; public class MyApplet extends Applet {private ArrayList Names = new ArrayList (); private Length > Length = new Length > (Names); public void paint (Graphics g) {Names.add ("Anton"); Names.add ("Boris"); Names.add ("Caesar"); g.drawString (Integer.toString (Length.length ()), 0, 10); }}

In the example code above, you can see how the generic class is instantiated with a container of type. You can also see that it is not a problem if the corresponding data type is itself generic and, as also expects a data type as a parameter.

In addition to the restriction with the keyword, which specifies an upper limit for data types, there can also be a restriction with the keyword. In this case, a lower limit is specified. All data types may then be used for instantiation that are the parent classes of the data type specified after the restriction.


10.5 Wildcards

Instantiations depending on other data types

In this chapter, a data type is passed as a parameter to generic classes in angle brackets in order to instantiate them. However, it is also possible to instantiate with a wildcard - expressed as a question mark in Java code. Take a look at the following example.

import java.applet. *; import java.awt. *; import java.util. *; public class MyApplet extends Applet {private ArrayList Names = new ArrayList (); private ArrayList Ages = new ArrayList (); private int length (ArrayList list) {return list.size (); } public void paint (Graphics g) {Names.add ("Anton"); Names.add ("Boris"); Names.add ("Caesar"); Ages.add (25); Ages.add (32); Ages.add (41); g.drawString (Integer.toString (length (Names)), 0, 10); g.drawString (Integer.toString (length (Ages)), 0, 20); }}

The parameter of the method has the data type. The generic class is therefore not instantiated with a data type such as or, but with a wildcard. This means that the reference variable can refer to lists of the type, regardless of the data type with which the respective lists are instantiated. So the method accepts both as parameters.

However, the above example could also be developed using a generic method.

import java.applet. *; import java.awt. *; import java.util. *; public class MyApplet extends Applet {private ArrayList Names = new ArrayList (); private ArrayList Ages = new ArrayList (); private int length (ArrayList list) {return list.size (); } public void paint (Graphics g) {Names.add ("Anton"); Names.add ("Boris"); Names.add ("Caesar"); Ages.add (25); Ages.add (32); Ages.add (41); g.drawString (Integer.toString (length (Names)), 0, 10); g.drawString (Integer.toString (length (Ages)), 0, 20); }}

As you can see, methods can also be generic - not just classes and interfaces.

The difference between this and the previous example is that wildcards are used to instantiate. The code of a generic class is completed in order to create a corresponding variable. Take a look at the following example, in which the variable is not defined as a parameter of a function, but within.

import java.applet. *; import java.awt. *; import java.util. *; public class MyApplet extends Applet {private ArrayList Names = new ArrayList (); private ArrayList Ages = new ArrayList (); public void paint (Graphics g) {ArrayList list; Names.add ("Anton"); Names.add ("Boris"); Names.add ("Caesar"); list = Names; g.drawString (Names.get (0), 0, 10); Ages.add (25); Ages.add (32); Ages.add (41); list = Names; g.drawString (Integer.toString (Ages.get (0)), 0, 20); }}

is a reference variable that can refer to objects of the type that are instantiated with any data type. However, the non-generic variant of could just as easily be used here. Wildcards only make real sense if you make restrictions as in the previous section.

import java.applet. *; import java.awt. *; import java.util. *; public class MyApplet extends Applet {private ArrayList Names = new ArrayList (); public void paint (Graphics g) {ArrayList list; Names.add ("Anton"); Names.add ("Boris"); Names.add ("Caesar"); list = Names; g.drawString (Names.get (0), 0, 10); }}

In the code example above, you can now only refer to lists of the type that are instantiated with or a child class. This places a restriction that cannot be made without generics.

Wildcards are particularly useful when developing generic classes. Because placeholders are used in generic classes, the question arises as to which data types should be used to define internal variables in generic classes. Since wildcards can also depend on placeholders in generic classes, variables can be defined internally in such a way that their data types depend on the data types with which generic classes are later instantiated. For example, a wildcard is used in the definition of the method in the interface, as you can see from the documentation.


10.6 Exercises

Practice creates masters

You can purchase the solutions to all of the exercises in this book as a ZIP file.

  1. Develop a Java application and implement the generic interface for the class in which you define the static method. Create two objects of the type of this class and compare them by calling the method given by. Output the result of the comparison to the standard output. It is sufficient if you only check the reference variables for equality with.

  2. Develop a Java application to which any number of integers can be passed as command line parameters when called. Store all numbers in a generic container of type. Then output the number of values ​​saved in the container on the standard output. Test your application by passing some numbers several times as command line parameters.

  3. Develop a Java application that asks the user for name and age. A user should be able to make any number of entries until he presses Enter. The data should be stored in a generic container of the type. Then the average age of all saved persons should be calculated and displayed on the standard output.

  4. Extend your solution to exercise 3 so that the ages entered are output on the standard output in descending order. Use the method of the class to solve this problem.


Copyright © 2001-2010 Boris Schäling