Java generics: generic classes, generic interfaces, and generic methods

Java generics (1): generic classes, generic interfaces, and generic methods
Java generics (2): unbounded wildcards, upper wildcards, and lower wildcards
Java Generics (3): Type Erase

Preface

Generalization is a very important feature in our development, especially in scenarios involving collections, polymorphisms or custom classes. But most people, including me, don't have a deep understanding of generics. Most of the time, they only use them when using lists, maps, and other collections. In other cases, they don't use them at all or they don't use them. They can't do it well when coding and designing, so it's time to comb out the knowledge of generics.

1. What are generics

With classical collections as an example, before introducing generics, we define a set object that can store any type of data, that is, Object. When acquiring an element, most of the time we need to force the object to a certain data type before we can call its related methods or get some attribute values, that is, we need to know the type of each element explicitly, which makes it very easy to trigger a ClassCastException. The scary thing is that there are no signs of code problems during compilation, and exceptions are raised only when the runtime program executes here and triggers some logic, such as:

List list = new ArrayList();
list.add("1");
list.add(2);
list.add(Collections.emptyList());

You can see that any type, that is, Object type or its subclass type, can be stored in a list. When we take elements, we usually need to cast them:

String str;
for (Object obj : list) {
	str = (String) obj;
	System.out.println(str.length);
}

When we finished writing this program, the IDE did not prompt us for any problems, but ClassCastException was thrown when the program was executed because the second and third elements in the list were not String types.
Since it is a transition error, we should have no problem using the instance of keyword to determine the type of ownership. The improved code is as follows:

for (Object obj : list) {
    if (obj instanceof String) {
        System.out.println((String) obj);
    } else if (obj instanceof Integer) {
        System.out.println((Integer) obj);
    } else if (obj instanceof List) {
        System.out.println((List) obj);
    }
}

The problem is that if we add a new data type (such as a floating point number), the program will add a corresponding type judgment. In daily development, programs are likely to be available to the outside world, and we don't know what types the caller will pass, so how many types should we check to make the program robust, 10, 100 or 1000? By adding so much judgment, the code loses readability and maintainability. Even when the caller passes in a custom type, it is even more impossible to solve the problem by this means.

From Java 5 onwards, we can solve this problem using Generic, which provides a compile-time type safety check mechanism that allows you to determine whether a type matches during compilation, without waiting for runtime, and without having to manually cast elements when they are acquired.

2. Use of generics

_Generics are essentially parameterized types, in which all operation types are specified as one parameter and can be passed through the entire class or method.

generic class

_Knowing the name, a generic class defines a generic identity on a class, which is similar to the creation of a normal class, but has more <> to store the generic identity.

Usage method

public class Class name <Generic Identity,Generic Identity 2,...> {
   private Generic Identity Variable Name;
}

A generic identifier can be any string, typically using only one uppercase letter. Usually common are E, T, K, V, of course can also be defined as A,B,C,D, etc., the number of generic identities can also be any.
A generic defined in a generic class can be understood as a formal parameter of a type, used as a member variable, or as an input to a method or as a return value type of a method, such as:

public class TestGeneric<T> {
	// Be a member variable
	private T t;
	
	// Include method
	public TestGeneric(T t) {
    	this.t = t;
	}
	
  	// Do Method Return Value Type
	public T getT() {
        return t;
    }
}

_Creating generic class objects is similar to creating ordinary objects, but there are many <> to declare the actual generic type, which can be interpreted as an argument, i.e. the member variable, method, or return value type in the currently created object is converted from T to the passed type.

Class name<data type> Object Name = new Class name<data type>();

_Examples of commonly used Lists and ArrayList s are:

List<String> list = new ArrayList<String>();

_The above wording is rather verbose, and two generic actual data types have been defined before and after. After Java7, the generic type on the back can be written-free, and the program recognizes it automatically, leaving the back empty <>.

List<String> list = new ArrayList<>(); 

_Defaults to Object if no generic type is specified.
_mentioned in the introduction to generics, two benefits of generics are that you can check the data type at compile time and you can get it without having to force a type conversion, as you would normally do.

List<String> list = new ArrayList<>();
list.add("1");
// Compilation error, only String type can be stored
list.add(2);
// Compilation error, only String type can be stored
list.add(Collections.emptyList());

// String types can be obtained directly without forcing a transition
String str = list.get(0);

Generic Class Inheritance

  • Subclasses are generic
    _When a child class is also a generic class and also wants to specify the generic type of the parent class, the generic types of the two classes must be identical.
public class TestList<T> extends ArrayList<T> {
}

class Test {
    public static void main(String[] args) {
        List<String> list = new TestList<>();
        list.add("1");
    }
}

_Compile errors if generic types are inconsistent

// Cannot resolve symbol 'E'
public class TestList<T> extends ArrayList<E> {
}

_Analysis: As mentioned above, generics are essentially parameterized types, and generic T can be specified at the time of instantiation, such as TestList <String> list = new TestList <>();, That is, the actual type of generic type T is String, and the generic type in the parent class is also T, which is also String. However, if inconsistent, as in the example above, the generic type E in the parent class cannot be passed through parameterization, the actual type cannot be determined, and the compilation will fail.

  • Subclass is not generic
    _When a child class is not a generic class but wants to specify a parent generic class, it is necessary to specify the type of generic in the parent class explicitly.
public class TestList2 extends ArrayList<Integer> {
}

class Test2 {
    public static void main(String[] args) {
        TestList2 list = new TestList2();
        list.add(1234);
    }
}

_Analysis: Since subclasses are not generic classes, it is natural that generic types cannot be passed to parent classes, so you need to specify specific generic types when defining classes, otherwise you will compile errors. As shown in the example above, if you specify the generic type of ArrayList as Integer, then when you use TestList2, you can call the ArrayList <Integer> related methods.

generic interface

Usage method

_Generic interfaces are defined exactly as generic classes

public interface Interface name <Generic Identity 1, Generic Identity 2...> {
}

_The most typical example is the well-known framework, Mybatis-Plus, Mapper and IBaseService interfaces. The following image is taken from Gitee, the official website

Generic interface implementation class

  • Implementation class is generic
    _Inherits from generic classes. If the implementation class is generic and you want to specify a generic type for the parent interface, the two generic types must be identical.
public class TestList3<E> implements List<E> {
    
    @Override
    public int size() {
        return 0;
    }
    ...

_Compile errors if generic types are inconsistent

// Cannot resolve symbol 'E'
public class TestList3<T> implements List<E> {
}
  • Implementation class is not generic

If the implementation class is not a generic class and you want to specify a generic class of the parent interface, then you need to specify the specific type explicitly.

public class TestList4 implements List<Integer> {
    
    @Override
    public int size() {
        return 0;
    }
    ...
}

class Test4 {
    public static void main(String[] args) {
        TestList4 list = new TestList4();
        list.add(1234);
    }
}

generic method

A generic method is an extra generic identifier that is defined when a method is defined and passed in when it is used. It is important to note that a generic method is not a method with a generic identity. For example:

public class Box<T> {

	private List<T> list = new ArrayList<>();
	
	public void put(T t) {
  		list.add(t);
    }

    public T get(int index) {
    	if (index >= list.size()) {
			throw new IllegalArgumentException("Illegal index");
		}
		return list.get(index);
    }
}

_Where, although both put() and get() methods have generic identity T as their parameter or return value types, these two methods are not generic methods, but use the generic type defined in the generic class Box, which is passed through the generic identity T defined by the generic class, not defined in the method.

_Generic methods are not dependent on generic classes or generic interfaces, that is, generic methods can also be defined in generic classes.

Usage method

_Use <> to define the generic identity of the method before returning the value type.

public  <Generic Identity 1, Generic Identity 2...> Return value type method name(Parameter 1...) {
	...
}
public class TestUtil {

    public <T> T getFirst(List<T> list) {
        if (CollectionUtils.isEmpty(list)) {
            return null;
        }
        return list.get(0);
    }

    public static void main(String[] args) {
        TestUtil testUtil = new TestUtil();
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        Integer num = testUtil.getFirst(list);
        // Result 1
        System.out.println(num);

        List<String> list2 = new ArrayList<>();
        list2.add("a");
        list2.add("b");
        list2.add("c");
        String letter = testUtil.getFirst(list2);
        // The result is a
        System.out.println(letter);
    }
} 

_In the above example, TestUtil is not a generic class, but getFirst() is a generic method that uses <T> as the generic identifier, passing in parameters and return values.

Static generic methods

_If you want to define a static generic method, you need to add static before the generic identity, which can be changed to

public static <T> T getFirst(List<T> list) {
    if (CollectionUtils.isEmpty(list)) {
        return null;
    }
    return list.get(0);
}

Owning class is generic class/generic interface

_When this occurs, you can use both generic identities.

public class Test<T> {
   public <E> void test(T t, E e) {
   	// Do Something...
  }
}

_When a generic method exists in a generic class or generic interface and the generic identities of both are identical, the generic identities in the method are defined in the method, not in the generic class/generic interface.

public class TestGeneric<T> {
   public <T> T test(T t) {
       return t;
   } 
   public static void Main(String[] args) {
       TestGeneric<String> testGeneric = new TestGeneric<>();
       // Real Participation Return Value Types are not String s defined when generic class objects are created, but Integer
       Integer number = testGeneric.get(1);
   }
}
ยทยทยท

Posted by Oubipaws on Wed, 17 Nov 2021 09:01:28 -0800