Review Java generics, take you more in-depth understanding of it, better use it!

Time:2021-3-3

1. Introduction

Java generics are introduced in jdk5.0 to reduce errors and add an extra abstraction layer to the types.
This article will briefly introduce generics in Java, the goals behind generics, and how to use generics to improve the quality of code.

2. Why use generics?

Imagine a scenario where we want to create a list to store in JavaInteger; the code might look like this:

List list = new LinkedList();
list.add(new Integer(1)); 
Integer i = list.iterator().next();

Surprisingly, the compiler prompts for the last line. It does not know what type of data is returned. Therefore, the compiler prompts for an explicit conversion:

Integer i = (Integer) list.iterator.next();

There is no convention to guarantee that the return type of the list isInteger.The defined list can contain any object. We only know that we retrieve the list by checking the context. When viewing a type, it can only guarantee that it is aObjectTherefore, an explicit conversion is needed to ensure that the type is safe.

This transformation can be noisy, and we know exactly what the data types in this list areinteger. The conversion also makes our code messy. If the programmer makes an error in the explicit conversion, it may result in throwing andType dependent runtime error

It’s easier if programmers can express their intention to use a particular type, and the compiler can ensure that the type is correct.

This is the core idea behind generics.

We modify the first line of the previous code segment to read:

List<Integer> list = new LinkedList<>();

By adding a diamond operator containing types < >, we narrow the specialization of this list toIntegerType, which specifies the type to be saved in the list. The compiler can enforce this type at compile time.

In smaller programs, this looks like a trivial addition. But in larger programs, this can add significant robustness and make the program easier to read.

3. Generic method

Generic methods are methods written with a single method declaration and can be called with different types of parameters. The compiler will ensure that the correct type is used. Here are some properties of generic methods:

  • A generic method has a type parameter (diamond operator wrapping type) before the return type of the method declaration
  • Type parameters can be bounded (boundaries will be explained later in this article)
  • Generic methods can have different type parameters separated by commas in the method signature
  • The body of a generic method is the same as a normal method

An example of defining a generic method to convert an array to a list:

public <T> List<T> fromArrayToList(T[] a) {   
    return Arrays.stream(a).collect(Collectors.toList());
}

In the previous example, the <T>Indicates that the method will handle generic typesT. This is necessary even if the method returns void.
As mentioned above, a method can handle multiple generic types, in which case all generic types must be added to the method declaration, for example, if we want to modify the above method to handle typesTAnd typeGIt should be written as follows:

public static <T, G> List<G> fromArrayToList(T[] a, Function<T, G> mapperFunction) {
    return Arrays.stream(a)
      .map(mapperFunction)
      .collect(Collectors.toList());
}

We are passing a function that will haveTThe array of type elements is converted to containGA list of type elements. For example, theIntegerConvert to itsStringExpression form:

@Test
public void givenArrayOfIntegers_thanListOfStringReturnedOK() {
    Integer[] intArray = {1, 2, 3, 4, 5};
    List<String> stringList
      = Generics.fromArrayToList(intArray, Object::toString);
 
    assertThat(stringList, hasItems("1", "2", "3", "4", "5"));
}

Oracle recommends using uppercase letters for generic types and choosing more descriptive letters for formal types, such as in Java collections,TFor type,KRepresents the key,VRepresents a value.

3.1. Generic boundary

As mentioned earlier, type parameters can be bounded. Being bounded means “limiting.” we can limit the types that methods can accept.

For example, you can specify that a method accepts a type and all its subclasses (upper bound) or a type and all its superclasses (lower bound).

To declare an upper bound type, we use the keyword after the typeextends, followed by the upper limit to use. For example:

public <T extends Number> List<T> fromArrayToList(T[] a) {
    ...
}

Keywords are used hereextendsRepresentation typeTThe upper limit of the extension class, or the upper limit of the implementation interface.

3.2. Multiple boundaries

A type can also have multiple upper bounds, as follows:

<T extends Number & Comparable>

IfTOne of the types of extensions is the class (that is, theNumber)Must be placed first in the boundary list. Otherwise, a compile time error will result.

4. Use wildcards

Using wildcards in java with question marks“They are used to refer to an unknown type. Wildcards are particularly useful when using generics and can be used as parameter types, but the first thing to consider is an important comment.

As we all know, object is the supertype of all Java classes, but the collection of objects is not the supertype of any collection.(maybe it’s a bit of a detour. Let’s have a good taste.)

For example,List<Object>no List<String> The supertype of theList<Object>Assign a variable of type toList<String>Variables of type will cause compiler errors.

This is to prevent possible conflicts when adding heterogeneous types to the same collection.

The same rules apply to any collection of types and their subtypes. Look at this example

public static void paintAllBuildings(List<Building> buildings) {
    buildings.forEach(Building::paint);
}

If we imagine a subtypeBuilding, instanceHouseThen we can’t combine this method withHouseList, even ifHouseyesBuildingA subtype of. If you need to use this method with a type build and all its subtypes, bounded wildcards can do the following:

public static void paintAllBuildings(List<? extends Building> buildings) {
    ...
}

Now, this method can handle itBuildingType and all its subtypes. This is called the upper bound wildcard, where the typeBuildingIt’s the upper bound.

Wildcards can also be specified with lower bounds, where the unknown type must be a supertype of the specified type. have access tosuperKeyword followed by a specific type to specify the lower bound, for example,<? super T>Represents an unknown type, which isTThe superclass of (= t and all its superclasses).

5. Type erasure

Generics are added to Java to ensure type safety, and to ensure that generics do not cause overhead at runtime. The compiler applies atype erasureThe progress of the project.

Type erase removes all type parameters and replaces them with their bounds, or if type parameters are unbounded, withObject. Therefore, the compiled bytecode only contains ordinary classes, interfaces, and methods to ensure that no new types are generated. At compile timeObjectThe correct cast is also applied to the type.
Here is an example of type erasure:

public <T> List<T> genericMethod(List<T> list) {
    return list.stream().collect(Collectors.toList());
}

Use type erasure, unbounded typeTReplace withObject, as follows:

// for illustration
public List<Object> withErasure(List<Object> list) {
    return list.stream().collect(Collectors.toList());
}
 
// which in practice results in
public List withErasure(List list) {
    return list.stream().collect(Collectors.toList());
}

If the type is bounded, the type is replaced by a binding at compile time:

public <T extends Building> void genericMethod(T t) {
    ...
}

Changes after compilation:

public void genericMethod(Building t) {
    ...
}

6. Generic and raw data types

One limitation of generics in Java is that type parameters cannot be base types

For example, the following cannot be compiled:

List<int> list = new ArrayList<>();
list.add(17);

To understand why raw data types don’t work, just rememberGenerics are a compile time featureThis means that the type will be erased and all generic types will be implemented asObjectClass.
For example, let’s look at the top of the listaddmethod:

List<Integer> list = new ArrayList<>();
list.add(17);

addThe statement of the method is as follows:

boolean add(E e);

And will be compiled as:

boolean add(Object e);

Therefore, type parameters must be convertible toObjectBase types cannot be used as type parameters because they do not inherit from object
However, Java provides them with boxing types, as well as automatic boxing and automatic unpacking:

Integer a = 17;
int b = a;

So if we want to create a list that can hold integers, we can use the wrapper:

List<Integer> list = new ArrayList<>();
list.add(17);
int first = list.get(0);

The compiled code is equivalent to:

List list = new ArrayList<>();
list.add(Integer.valueOf(17));
int first = ((Integer) list.get(0)).intValue();

Future versions of Java may allow generics to use raw data types.ValhallaThe project aims to improve the way generics are handled. The idea is to achieve the goalJEP 218Generic specialization described in

7. Summary

Java generics is a powerful complement to the Java language, because it makes the work of programmers easier and less error prone. Generics enforce type correctness at compile time, and, most importantly, can implement generic algorithms without incurring any additional overhead on our applications.

If you think the article is good, remember the official account: big guy outside the pot.
Liu Yishou’s blog