Introduction
Generics enable types (classes and interfaces) to be parameters when defining classes, interfaces, and methods. It enables you to re-use the same code. In this section, we will cover Generic Classes, Generic Interfaces, Generic Methods, and Bounded Type Parameters.
Type Parameter Naming Conventions
By convention, type parameter names are single, uppercase letters. The most commonly used type parameter names are:
E - Element (used extensively by the Java Collections Framework) K - Key N - Number T - Type V - Value S,U,V etc. - 2nd, 3rd, 4th types
Generic Classes
Generic classes are very useful if you need to use different classes as the type parameter. You can create a generic class with a generic data type T. For example, the Food class has a generic type variable declared after the name of the class. It means that you can create Food instances using different data types
public class Food<T> {
private T ingredients;
public T getIngredients(){
return ingredients;
}
public void setIngredients(T ingredients) {
this.ingredients = ingredients;
}
}
Let’s create a food instance with String data type
Food<String> food = new Food<>();
food.setIngredients("protein: 200 gr; carb: 300, fat: 75");
Maybe you changed your mind and decided to create another class that holds the ingredients
class Ingredients {
private String name;
private int calories;
Ingredients(String name, int calories) {
this.name = name;
this.calories = calories;
}
}
Then, you can create a food instance using Ingredients data type
Ingredients ingredients = new Ingredients("protein", 200);
Food<Ingredients> ingredientsFood = new Food<>();
ingredientsFood.setIngredients(ingredients);
Let’s look at the next example. Why don’t we create a food instance with the Map data type? Don’t worry if you don’t understand the syntax. We will cover the Map interface in the next section
Food<Map<String, Integer>> foodAsMap = new Food<>();
Map<String, Integer> foods = new HashMap<String,Integer>();
foods.put("protein", 200);
foods.put("carb", 300);
foods.put("fat", 75);
foodAsMap.setIngredients(foods);
Generic classes can have two type parameters as well. In this example, we defined FoodMultipleType generic class that has two parameters. K for a map key, V for a map value
public class FoodMultipleType<K, V> implements FoodPair<K, V> {
private K name;
private V calories;
public FoodMultipleType(K name, V value) {
this.name = name;
this.calories = value;
}
@Override
public K getName() { return name; }
@Override
public V getCalories() { return calories; }
}
To use this class, we can implement the following:
FoodPair<String, Integer> tuna = new FoodMultipleType<String, Integer>("tuna", 205);
FoodPair<String, Integer> brownRice = new FoodMultipleType<String, Integer>("brown rice", 214);
Generic Interfaces
An interface can also declare a formal type parameter. For example, the List interface uses a generic type.
public interface List<E> extends Collection<E> {
int size();
boolean isEmpty();
boolean contains(Object var1);
...
}
You can also define your own interface. Let’s create a Diet interface. There are three approaches to implement the Diet interface.
public interface Diet<T> {
void printDietList(T t);
}
Approach 1: Specify the generic type in the class
The following concrete class says that it deals only with LowCarbHighProtein class
class LowCarbDiet implements Diet<LowCarbHighProtein> {
@Override
public void printDietList(LowCarbHighProtein t) {
//do something with t
}
}
Approach 2: Create a generic class
The following concrete class allows the caller to specify the type of the generic
class LowCarbAbstractDiet<U> implements Diet<U> {
@Override
public void printDietList(U u) { // do something}
}
Approach 3: Not use generics at all
This is the old way of writing code. It generates a compiler warning about LowCarbDietSimple being a raw type, but it compiles
class LowCarbDietSimple implements Diet {
@Override
public void printDietList(Object o) {}
}
Generic Methods
Generic methods are methods that introduce their own type parameters. The following generic method compares the two FoodMultipleType objects
public static <K, V> boolean compare(FoodMultipleType<K, V> p1, FoodMultipleType<K, V> p2) {
return p1.getName().equals(p2.getName()) &&
p1.getCalories().equals(p2.getCalories());
}
Let’s see how we use the compare method
FoodPair<String, Integer> tuna = new FoodMultipleType<String,Integer>("tuna",205);
FoodMultipleType<String, Integer> brownRice = new FoodMultipleType<String,Integer>("brown rice",214);
Util.<String,Integer>compare((FoodMultipleType<String, Integer>) tuna,brownRice);
Bounded Type Parameters
We sometimes need to restrict the type arguments for the methods. Bounded type parameters serve for that purpose.
Unbounded
The unbounded wildcard type is specified using the wildcard character (?), for example, List<?>. This is called a list of unknown type. This method prints a list of any type.
private static void unboud(List<?> list) {
for (Object elem : list)
System.out.print(elem + " ");
System.out.println();
}
Lower Bound
A lower bounded wildcard is expressed using List<? super ClassName>, such as List<? super Integer>. This method works on lists of Integer and the supertypes of Integer, such as Integer, Number, and Object
private static void lowerBound(List<? super Integer> list) {
for (int i = 0; i < 10 ; i++) {
list.add(i);
}
System.out.println(list);
}
Upper Bound
An upper bounded wildcard is specified using List<? extends ClassName>, List<? extends Number>. This method works on lists of Number and the subtypes of Number, such as Integer, Double, and Float.
private static double upperBound(List<? extends Number> list) {
double s = 0.0;
for (Number n : list)
s += n.doubleValue();
return s;
}
Let’s look at how these methods are called. Note that we can pass Integer and Double list to the upperBound method. On the other hand, lowerBound method only accepts Integer and its super classes. The undound method doesn’t have a restriction.
List<Integer> intList = Arrays.<Integer>asList(1, 2, 3);
System.out.println(upperBound(intList)); // Output: 6.0
List<Double> doubleList = Arrays.<Double>asList(4.3, 7.1, 2.4);
System.out.println(upperBound(doubleList)); // Output: 13.799999999999999
lowerBound(intList); // Output: 0 1 2
//lowerBound(doubleList); // compile error
unboud(intList); // Output: 1 2 3
unboud(doubleList); // Output: 4.3 7.1 2.4
Conclusion
In this section, we covered Generic Classes, Generic Interfaces, Generic Methods, and Bounded Type Parameters. You can find the source code on GitHub.