If you understand java generics in one article, you may not understand it, after all, it is too comprehensive

I. Introduction

Background and role of Java generics

Java generics are a feature of the Java programming language. The purpose of introducing generics is to enhance the type safety and reusability of code. Before there was no generic type, collection classes in Java (such as ArrayList, HashMap, etc.) could only store objects of type Object, which required mandatory type conversion when using collections, which was prone to type errors.

Background on Generics: Prior to Java 5 versions, Java's types were static, determined at compile time, and type information was erased at runtime. In this case, the compiler cannot verify the element type of the collection, so it may cause a runtime type error. To solve this problem, Java introduced the generic mechanism.

The role of generics :

  1. Type safety: Generics enable type errors to be detected at compile time, avoiding type conversion exceptions at runtime.
  2. Code reuse: By using generics, you can write general-purpose code that is applicable to many different types of data, improving the flexibility and reusability of the code.
  3. API design: Generics make API design clearer and more consistent. Generic interfaces, classes, and methods can be defined, and more flexible parameter types and return value types can be provided.
  4. Enhanced collection classes: Generics make collection classes more type-safe and concise, eliminating the need for explicit type conversion.

When using generics, you can define classes, interfaces, methods, and variables with generic parameters, and specify the specific type of generics by using specific type arguments. For example, you can define a generic class ArrayList, where T represents a type parameter, and you can specify a specific type when creating an ArrayList object, such as ArrayList represents an ArrayList that stores integers.

Basic concepts and benefits of generics

basic concept:
  • Type parameters: In generics, type parameters are used to represent an unknown type. Type parameters can be denoted by arbitrary identifiers, usually using a single uppercase letter as a convention, such as T, E, Ketc.
  • Actual type parameters: When using generics, you need to assign specific types to type parameters. These specific types are called actual type parameters. For example, when creating an instance of a generic class, you can create an object that stores integers by Integerpassing the type parameter as the actual type parameter .T
benefit:
  • Type safety: Generics provide stricter type checking, and type errors can be found at compile time. By specifying specific type parameters, incompatible type operations can be caught during compilation, avoiding type conversion errors and related exceptions at runtime.
  • Code reusability: Generics allow us to write common code logic that can operate on multiple types without having to write corresponding code for each type. This reduces code duplication and improves code maintainability and readability.
  • Efficiency: Generic types perform type erasure at compile time, converting generic types to their boundary types (usually Object types). This means that there is no need to preserve generic type information at runtime, thereby avoiding additional overhead and improving program performance.

2. Generic types and methods

generic class

Syntax and usage of defining generic classes

In many programming languages, such as Java and C#, a generic class is a special type of class that can accept different types of parameters for instantiation. Generic classes provide the benefits of code reuse and type safety because they can be used with various data types without writing a separate class for each type.

Following is the syntax for defining a generic class:

public class GenericClass<T> {
    
    
    // 类成员和方法定义
}

In the above example, GenericClassis the name of a generic class, <T>denoting the type parameter, which Tcan be replaced by any legal identifier denoting the actual type.

To use a generic class, instantiate it by specifying the actual type. For example, assuming we have a MyClassgeneric class called , we can use it as follows:

GenericClass<Integer> myInstance = new GenericClass<Integer>();

In the above example, we instantiated the GenericClassgeneric class with an integer type. In this way, myInstanceit will be an object that can only store integer types.

After instantiating a generic class, you can use the members and methods defined in that class, just like ordinary classes. The difference is that members or methods in a generic class can take type parameters Tand will be type checked and processed according to the actual type.

If you need to use multiple type parameters in a generic class, you can separate them by commas:

public class MultiGenericClass<T, U> {
    
    
    // 类成员和方法定义
}

The above example defines a generic class with two type parameters MultiGenericClass.

To summarize, the syntax for defining a generic class is to use <T>or other type parameters after the class name, and use those type parameters in the class. The generic class can then be instantiated by specifying the actual type, and the members and methods defined in the generic class can be used.

Qualification of type parameters and use of wildcards

Restrictions on type parameters :

Type parameter qualification allows us to constrain the type parameters of a generic class or method to ensure that only certain types or types satisfying certain conditions can be used.

In Java, extendstype parameters can be qualified using the keyword. There are two ways of qualifying type parameters:

  1. Single Bound : Specifies that the type parameter must be a subclass of a class or interface.

    public class MyClass<T extends SomeClass> {
          
          
        // 类成员和方法定义
    }
    

    In the example above, the type parameter Tmust be SomeClassa subclass of the class or a type that implements SomeClassthe interface.

  2. Multiple Bounds : Specifies that the type parameter must be a subclass of more than one class or interface, and only one (if any).

    public class MyClass<T extends ClassA & InterfaceB & InterfaceC> {
          
          
        // 类成员和方法定义
    }
    

    In the example above, the type parameter Tmust be ClassAa subclass of the class and also implement InterfaceBthe and InterfaceCinterfaces.

Through the qualification of type parameters, the type can be controlled and constrained more precisely in a generic class or method to improve the type safety and flexibility of the code.

Use of wildcards :

A wildcard is a special type parameter used to represent an unknown or indeterminate type in a generic class or method. There are two wildcards that can be used:

  1. Unbounded Wildcard : Use a question ?mark to indicate that it can match any type.

    public void myMethod(List<?> myList) {
          
          
        // 方法实现
    }
    

    In the example above, myMethodthe method accepts one parameter of type List, but the element type of the list is unknown and could be any type.

  2. Bounded Wildcard : Use extendsand a concrete class or interface to limit the range of types that a wildcard can match.

    public void myMethod(List<? extends SomeClass> myList) {
          
          
        // 方法实现
    }
    

    In the example above, myMethodthe method accepts one parameter of type List, but the element type of the list must be SomeClassof class or a subclass of it.

By using wildcards, it is possible to write more general generic code, allowing arguments of various types to be handled. It provides more flexibility, especially when you don't care about the concrete type or need to operate on multiple types.

It should be noted that when wildcards are used, the operation of adding elements to generic objects with wildcards cannot be performed, because the specific type represented by the wildcard cannot be determined. But the operation of reading elements can be performed. If you need to support both add and read operations, you can use qualified wildcards to solve this problem.

Instantiating generic classes and type inference

In Java, a generic class is a class that can be parameterized on a type. By using generics, we can write more general and reusable code while improving type safety. When instantiating a generic class, we need to specify concrete type parameters.

Following is the general syntax for instantiating a generic class:

ClassName<DataType> objectName = new ClassName<>();

In the above syntax, ClassNameis the name of the generic class and DataTypeis a placeholder for the actual type parameter. By substituting the appropriate type with DataType, we can create an object of a specific type. For example, if we have a generic class Box<T>where Tis the generic type parameter, we can instantiate it as follows:

Box<Integer> integerBox = new Box<>();

In this example, we Treplaced the generic type parameter with Integer, and then created an Boxinteger object of type .

Type inference, on the other hand, refers to the process by which the compiler automatically infers generic type parameters based on contextual information. In some cases, we can omit the generic type parameters and let the compiler infer them automatically. This simplifies the code and makes it more readable.

Here is an example showing the usage of type inference:

Box<Integer> integerBox = new Box<>();  // 类型推断

List<String> stringList = new ArrayList<>();  // 类型推断

In these examples, we did not specify the generic type parameter explicitly, but used <>the operator. The compiler will infer the correct type parameters from the variable declaration and initialization value.

Note that type inference is only available in Java 7 and later. In older versions of Java, generic type parameters had to be specified explicitly.

generic method

Syntax and usage of defining generic methods

A generic method is a method that has parameters of a generic type. By using generic methods, we can use type parameters at the method level, enabling methods to handle different types of data, and improving code flexibility and reusability.

Following is the general syntax for defining a generic method:

public <T> ReturnType methodName(T parameter) {
    
    
    // 方法体
}

In the syntax above, <T>a placeholder for a type parameter, which can be any identifier (usually a single uppercase letter). TCan be used inside method parameters, return types, and method bodies. ReturnTypeIs the return type of the method, which can be a concrete type or a generic type.

Here is a simple example showing how to define and use generic methods:

public <T> void printArray(T[] array) {
    
    
    for (T element : array) {
    
    
        System.out.println(element);
    }
}

// 调用泛型方法
Integer[] intArray = {
    
     1, 2, 3, 4, 5 };
printArray(intArray);

String[] stringArray = {
    
     "Hello", "World" };
printArray(stringArray);

In the example above, we defined a printArraygeneric method called . It takes a generic array as an argument and prints out each element in the array. We can use this method to print arrays of different types such as integer arrays and string arrays.

It is important to note that a generic method can exist independently of the generic class and can be defined and used in any class. They provide more flexibility, allowing us to genericize specific methods, not just entire classes.

Calling generic methods and type inference

When calling generic methods, we need to pay attention to several key points:

  1. Explicitly specify type parameters: If the type parameters of the generic method are not automatically inferred by the compiler, we need to explicitly specify the type parameters. You can use angle brackets (<>) before the method name and provide specific type parameters.

    // 显式指定类型参数为String
    String result = myGenericMethod.<String>genericMethod(argument);
    
  2. Automatic type inference: In some cases, the Java compiler can automatically infer the type parameters of the generic method, making the code more concise and readable. Explicitly specifying the type parameter can be omitted.

    // 自动类型推断,根据参数类型推断类型参数为Integer
    Integer result = myGenericMethod.genericMethod(argument);
    

    The compiler infers type parameters from the type and context information of the method parameters. This type inference is very useful for simplifying code and improving readability.

  3. Wildcard type parameters: In some cases, we may want a generic method to accept parameters of an unspecified type. In this case, wildcards can be used as type parameters.

    • Unbounded wildcard: It is represented by a question mark (?), and any type of parameter can be accepted.

      // 泛型方法接受任意类型的参数
      void myGenericMethod(List<?> list) {
              
              
          // 方法体
      }
      
    • Bounded wildcard: Use the extends keyword to specify the upper bound or use the super keyword to specify the lower bound, which limits the range of parameter types accepted by the generic method.

      // 泛型方法接受 Number 及其子类的参数
      void myGenericMethod(List<? extends Number> list) {
              
              
          // 方法体
      }
      
      // 泛型方法接受 Integer 及其父类的参数
      void myGenericMethod(List<? super Integer> list) {
              
              
          // 方法体
      }
      

It should be noted that when calling a generic method, the compiler will perform type checking according to the passed parameter type and context. A compilation error will be generated if the types do not match.

3. Generic interfaces and wildcards

generic interface

Syntax and usage of defining generic interfaces

A generic interface is an interface with generic type parameters. By using generic interfaces, we can use type parameters at the interface level, enabling implementation classes to handle different types of data, and improving code flexibility and reusability.

Following is the general syntax for defining a generic interface:

public interface InterfaceName<T> {
    
    
    // 接口方法和常量声明
}

In the syntax above, <T>a placeholder for a type parameter, which can be any identifier (usually a single uppercase letter). TCan be used in interface methods, constants and inner classes.

Here is a simple example showing how to define and use a generic interface:

public interface Box<T> {
    
    
    void add(T item);
    T get();
}

// 实现泛型接口
public class IntegerBox implements Box<Integer> {
    
    
    private Integer item;

    public void add(Integer item) {
    
    
        this.item = item;
    }

    public Integer get() {
    
    
        return item;
    }
}

// 使用泛型接口
Box<Integer> box = new IntegerBox();
box.add(10);
Integer value = box.get();

In the example above, we defined a Boxgeneric interface called . It contains a addmethod and a getmethod, which are used to add and get data of generic type respectively. Then, we implement a concrete class of this generic interface IntegerBoxand specify the concrete type parameter in it Integer.

Finally, we created an Box<Integer>object of type using the generic interface, addadding integer values ​​via the method, and getgetting integer values ​​via the method.

It should be noted that when implementing a generic interface, you can choose to specify the type parameters specifically, or you can continue to use generics.

How to implement a generic interface
  1. Concrete type parameter implementation: Explicitly specify concrete type parameters in the implementing class. This will allow the implementing class to only handle certain types of data.

    public class IntegerBox implements Box<Integer> {
          
          
        private Integer item;
    
        public void add(Integer item) {
          
          
            this.item = item;
        }
    
        public Integer get() {
          
          
            return item;
        }
    }
    

    In the above example, IntegerBoxthe class implements the generic interface Box<Integer>and explicitly specifies the type parameters as Integer. Therefore, IntegerBoxthe class can only handle integer type data.

  2. Preserve generic type parameters: Continue to use generic type parameters in the implementing class. This will allow the implementing class to have the same type parameters as the generic interface, maintaining flexibility.

    public class GenericBox<T> implements Box<T> {
          
          
        private T item;
    
        public void add(T item) {
          
          
            this.item = item;
        }
    
        public T get() {
          
          
            return item;
        }
    }
    

    In the above example, GenericBox<T>the class implements the generic interface Box<T>and holds the type parameters T. This means that GenericBoxthe class can handle any type of data with greater flexibility.

Using one of the above two ways, you can choose the way to implement the generic interface according to your needs. It depends on whether the implementing class needs to be bound to a specific type or to be flexible when working with the data.

In addition, no matter which method you use to implement the generic interface, you need to ensure that the method signatures in the implementing class exactly match the methods defined in the generic interface. This includes the method name, parameter list, and return type.

wildcard

The concept of upper bound wildcards and lower bound wildcards

Upper Bounded Wildcard

The upper bound wildcard is used to restrict the generic type parameter to be the specified type or a subclass of the specified type. Use extendsthe keyword to specify an upper bound.

grammar:

<? extends Type>

For example, suppose we have a generic method printListthat takes a list and prints the elements of the list. But we hope that the method can only accept a list of Number type or its subclasses, which can be achieved by using the upper bound wildcard:

public static void printList(List<? extends Number> list) {
    
    
    for (Number element : list) {
    
    
        System.out.println(element);
    }
}

// 调用示例
List<Integer> integerList = Arrays.asList(1, 2, 3);
printList(integerList); // 可以正常调用

List<String> stringList = Arrays.asList("Hello", "World");
printList(stringList); // 编译错误,String 不是 Number 的子类

In the above example, printListthe method <? extends Number>defines an upper-bounded wildcard using to indicate that the method accepts a list of type Number or its subclasses. Therefore, we can pass a list of type Integer as a parameter, but we cannot pass a list of type String.

Lower Bounded Wildcard

The lower bound wildcard is used to restrict the generic type parameter to be of the specified type or a superclass of the specified type. Use superthe keyword to specify the lower bound.

grammar:

<? super Type>

For example, suppose we have a generic method addToListthat takes a list and an element to add to the list. But we hope that this method can only accept elements of Object type or its parent class, which can be achieved by using the lower bound wildcard:

public static void addToList(List<? super Object> list, Object element) {
    
    
    list.add(element);
}

// 调用示例
List<Object> objectList = new ArrayList<>();
addToList(objectList, "Hello");
addToList(objectList, 42);

List<String> stringList = new ArrayList<>();
addToList(stringList, "World"); // 编译错误,String 不是 Object 的父类

In the above example, addToListthe method <? super Object>defines a lower-bounded wildcard using , which means that the method accepts a list of Object type or its superclass, and can add elements of any type to the list. Therefore, we can add strings and integers to objectListbut not strings stringList.

It should be noted that upper-bound wildcards and lower-bound wildcards are mainly used to flexibly handle generic type parameters, so that different types of data can be handled in generic code. They provide greater flexibility and reusability.

Scenarios for using wildcards in generic methods and generic interfaces

Scenarios for using wildcards in generic methods:

  1. Read operation: When the method only needs to obtain the value from the generic parameter, the upper bound wildcard can be used ? extends Tto indicate that the method is applicable to any T type or its subclasses.

    public static <T> void printList(List<? extends T> list) {
          
          
        for (T element : list) {
          
          
            System.out.println(element);
        }
    }
    
    // 调用示例
    List<Integer> integerList = Arrays.asList(1, 2, 3);
    printList(integerList); // 可以正常调用
    
    List<String> stringList = Arrays.asList("Hello", "World");
    printList(stringList); // 可以正常调用
    
  2. Write operation: When a method needs to write a value to a generic parameter, a lower bound wildcard can be used ? super Tto indicate that the method is applicable to any T type or its superclass.

    public static <T> void addToList(List<? super T> list, T element) {
          
          
        list.add(element);
    }
    
    // 调用示例
    List<Object> objectList = new ArrayList<>();
    addToList(objectList, "Hello");
    addToList(objectList, 42);
    
    List<Number> numberList = new ArrayList<>();
    addToList(numberList, 3.14);
    addToList(numberList, 123);
    

Scenarios for using wildcards in generic interfaces:

  1. Define a flexible container: When defining a container class, it is hoped that the container can store any type of data, and unlimited wildcards can be used <?>.

    public interface Container<E> {
          
          
        void add(E element);
        E get();
    }
    
    // 实现示例
    public class AnyContainer implements Container<?> {
          
          
        private Object element;
    
        public void add(Object element) {
          
          
            this.element = element;
        }
    
        public Object get() {
          
          
            return element;
        }
    }
    
  2. Limiting the range of types: When you want a generic interface to only handle types within a certain range, you can use upper or lower bound wildcards.

    public interface Box<T extends Number> {
          
          
        void addItem(T item);
        T getItem();
    }
    
    // 实现示例
    public class NumberBox<T extends Number> implements Box<T> {
          
          
        private T item;
    
        public void addItem(T item) {
          
          
            this.item = item;
        }
    
        public T getItem() {
          
          
            return item;
        }
    }
    
    public class IntegerBox implements Box<Integer> {
          
          
        private Integer item;
    
        public void addItem(Integer item) {
          
          
            this.item = item;
        }
    
        public Integer getItem() {
          
          
            return item;
        }
    }
    

In the above scenarios, the purpose of using wildcards is to provide greater flexibility and reusability. Wildcards allow us to handle multiple types of data in generic methods and interfaces without binding to concrete types. This makes the code more general, extensible, and applicable to a wider range of scenarios.

4. Generics and collection framework

A detailed introduction to the generic collection framework

The generic collection framework is a set of container classes provided in Java for storing and manipulating data, and they support generic type parameters. java.utilThe generic collection framework provides rich implementations under the package in the JDK , including lists (List), collections (Set), maps (Map), and so on.

Core interface:

  1. List interface: represents an ordered repeatable collection. Allows access to elements by index and can contain repeated elements. Common implementation classes are ArrayList, LinkedList and Vector.

  2. Set interface: Represents an unordered collection that does not allow duplicate elements. The uniqueness of the element is guaranteed. Common implementation classes are HashSet, TreeSet and LinkedHashSet.

  3. Queue interface: represents a first-in-first-out (FIFO) queue. Common implementation classes are LinkedList and PriorityQueue.

  4. Map interface: represents a mapping table of key-value pairs. Each key is unique and can be used to get the associated value. Common implementation classes are HashMap, TreeMap and LinkedHashMap.

Advantages of generics:

The main advantage of the generic collection framework is that it provides type safety and compile-time type checking. By specifying generic type parameters, we can catch many type errors at compile time and avoid type conversion exceptions at runtime. Generics also provide better code readability and maintainability because they explicitly specify the type of elements stored in the container.

Example usage:

Here are some example usages of common generic collection frameworks:

// 创建一个泛型列表,并添加元素
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
stringList.add("World");

// 使用迭代器遍历列表
for (String element : stringList) {
    
    
    System.out.println(element);
}

// 创建一个泛型集合,并添加元素
Set<Integer> integerSet = new HashSet<>();
integerSet.add(1);
integerSet.add(2);
integerSet.add(3);

// 判断集合是否包含特定元素
boolean containsTwo = integerSet.contains(2);
System.out.println(containsTwo);  // 输出: true

// 创建一个键值对映射表,并添加元素
Map<String, Integer> stringToIntegerMap = new HashMap<>();
stringToIntegerMap.put("One", 1);
stringToIntegerMap.put("Two", 2);
stringToIntegerMap.put("Three", 3);

// 根据键获取值
int value = stringToIntegerMap.get("Two");
System.out.println(value);  // 输出: 2

By using the generic collection framework, we can easily create and manipulate collections of different types and get the benefits of type safety and checking at compile time.

5. Type erasure and bridge methods

The principle and impact of type erasure

Generic type erasure (Type Erasure) is one of the implementations of generics in Java. It is a mechanism for converting generic types to non-generic types during compilation. In generic type erasure, generic type parameters are erased to their upper bound or Object type, and type checking occurs primarily at compile time rather than run time.

The principle of generic type erasure:

  1. Type erasure: During compilation, all generic type parameters are replaced by their upper bound or Object type. For example, List<String>after compilation it becomes List<Object>.

  2. Conversion after type erasure: Due to type erasure, the original generic type information is not available at runtime. Therefore, when using generic types, necessary conversions are performed to ensure type safety.

    • Upcasting: If a generic type parameter is a subclass, it is cast to its upper bound type. For example, List<String>is converted to List<Object>.

    • Downcasting: If we need to obtain specific type parameters from a generic type, we need to perform type conversion. But this may cause a runtime type exception (ClassCastException).

Effects of generic type erasure:

  1. Compatibility: Generic type erasure ensures compatibility with original non-generic code. This means that code that uses generic types can be interacted with legacy code that does not use generics.

  2. Unable to get concrete type parameters: Details of generic type parameters cannot be obtained at runtime due to type erasure. For example, there is no way to tell at runtime List<String>whether a List object is a List object or a List object List<Integer>.

  3. Type Safety: Type erasure causes generics to lose type checking at runtime. The compiler can only perform type checking at compile time. If there is a type mismatch, a ClassCastException may occur at runtime.

  4. Restrict reflection operations: Through the reflection mechanism, the limitation of generic type erasure can be bypassed, and information of generic types can be obtained at runtime. However, the use of reflection is complex and has low performance, so frequent use is not recommended.

Example impact:

The following example illustrates the effect of generic type erasure:

// 定义一个泛型类
public class GenericClass<T> {
    
    
    private T value;

    public void setValue(T value) {
    
    
        this.value = value;
    }

    public T getValue() {
    
    
        return value;
    }
}

// 使用泛型类
GenericClass<String> stringGeneric = new GenericClass<>();
stringGeneric.setValue("Hello");
String value = stringGeneric.getValue();

// 编译后的泛型类型擦除
GenericClass stringGeneric = new GenericClass();
stringGeneric.setValue("Hello");
String value = (String) stringGeneric.getValue();  // 需要进行类型转换

// 运行时类型异常示例
GenericClass<String> stringGeneric = new GenericClass<>();
GenericClass<Integer> integerGeneric = new GenericClass<>();

System.out.println(stringGeneric.getClass() == integerGeneric.getClass());  // 输出: true

stringGeneric.setValue("Hello");

try {
    
    
    Integer value = integerGeneric.getValue();  // 运行时抛出 ClassCastException 异常
} catch (ClassCastException e) {
    
    
    System.out.println("ClassCastException: " + e.getMessage());
}

The concept and function of the bridge method

Generic Bridge Method (Generic Bridge Method) is a method automatically generated by the Java compiler in order to maintain the safety of generic types. Its role is to ensure type safety and compatibility when inheriting or implementing classes or interfaces with generic type parameters.

concept:

When a class or interface defines methods with generic type parameters, and the class or interface is inherited or implemented by subclasses or implementing classes, due to generic type erasure, the compiler needs to generate additional bridge methods to ensure that Type safety. These bridge methods have the same method signature, but use primitive types as parameter and return type to maintain compatibility with other non-generic methods in the inheritance hierarchy.

effect:

  1. Type safety: The main role of generic bridge methods is to maintain type safety. Access to incompatible types can be prevented at runtime by adding bridge methods. This avoids type errors that cannot be detected during compilation.

  2. Maintaining inheritance relationships: Generic bridge methods are also used to maintain inheritance relationships between generic classes or interfaces. They ensure that subclasses or implementing classes can correctly override generic methods of superclasses or interfaces with the correct type parameters.

Example:

Consider the following example:

public class MyList<T> {
    
    
    public void add(T element) {
    
    
        // 添加元素的逻辑
    }
}

// 子类继承泛型类,并覆盖泛型方法
public class StringList extends MyList<String> {
    
    
    @Override
    public void add(String element) {
    
    
        // 添加元素的逻辑
    }
}

In this example, the compiler generates a bridge method to ensure type safety and compatibility due to Java's generic type erasure mechanism. The above code is actually transformed by the compiler into the following:

public class MyList {
    
    
    public void add(Object element) {
    
    
        // 添加元素的逻辑
    }
}

public class StringList extends MyList {
    
    
    @Override
    public void add(Object element) {
    
    
        add((String) element);
    }

    public void add(String element) {
    
    
        // 添加元素的逻辑
    }
}

In this transformed code, StringListthe class contains a bridge method add(Object element)that calls the real generic method add(String element). This maintains type safety and is compatible with non-generic methods of parent classes.

By generating generic bridge methods, the Java compiler can maintain type safety and compatibility when inheriting and implementing generic types. These bridge methods provide better type checking and runtime type safety while converting internally and maintaining generic type erasure.

6. Limitations and Precautions of Generics

Type Safety and Runtime Exceptions in Generics

In generics, type safety refers to the checking of types by the compiler to ensure that the program does not make type errors at runtime. By using generics, you can catch many type errors at compile time and avoid type conversion exceptions at run time.

Advantages of type safety:

  1. Compile-time type checking: The Java compiler performs type checking on generics to ensure the type safety of the code. It can verify that generic type parameters match declared type parameters and reject incorrect type operations.

  2. Avoid casts: When using generics, manual casts are no longer necessary because the compiler can automatically insert the cast code.

  3. Improve code readability and maintainability: By using generics, you can explicitly specify the type of elements stored in the container, making the code more readable and understandable. It can also provide better code maintainability because the type information is explicit.

Implementation of type safety:

  1. Compile-time type checking: The compiler will perform type checking on generics to ensure that no type errors occur at compile time. If there is a type mismatch, the compiler reports an error and prevents the code from compiling.

  2. Type erasure mechanism: Generics in Java are implemented through type erasure, that is, generic types are erased to primitive types (such as Object) at compile time. Type erasure ensures compatibility with original non-generic code and maintains backwards compatibility.

  3. Bridge method: In order to maintain the inheritance relationship and type safety between generic classes and interfaces, the compiler generates bridge methods. Bridge methods are used to ensure correct type conversion and method invocation when inheriting or implementing a class or interface with generic type parameters.

Runtime exception:

Although generics enhance type safety, runtime exceptions can still occur in certain situations. These exceptions usually occur in the following situations:

  1. Information loss due to type erasure: Due to type erasure, details of generic type parameters cannot be obtained at runtime. Therefore, during type conversion, if the type does not match, it may cause a ClassCastException exception.

  2. Interacting with primitive types: If you use primitive types to interact with generic types, such as assigning a generic collection to an unparameterized collection, there may be no warnings at compile time, but a type error will result at runtime.

  3. Reflection operation: Through the reflection mechanism, the type safety of generics can be bypassed. When using reflection, extra care is required to avoid type errors and runtime exceptions.

Example:

List<String> stringList = new ArrayList<>();
stringList.add("Hello");
stringList.add("World");

// 编译时类型检查,不允许添加非 String 类型的元素
stringList.add(123);  // 编译错误

// 获取元素时不需要进行类型转换
String firstElement = stringList.get(0);

// 迭代器遍历时可以确保元素类型的安全性
for (String element : stringList) {
    
    
    System.out.println(element);
}

// 类型擦除引起的运行时异常示例
List<Integer> integerList = new ArrayList<>();
integerList.add(10);

List rawList = integerList;  // 原始类型与泛型类型交互

List<String> stringList = rawList;  // 编译通过,但在运行时会导致类型错误

String firstElement = stringList.get(0);  // 运行时抛出 ClassCastException 异常

In this example, primitive types rawListcan be assigned to and from generic types at compile time List<String>. But at runtime, when we try to get an element from stringListthe , it will cause a ClassCastException due to type erasure and what is actually stored is an integer type.

So while generics offer the benefits of type safety and compile-time type checking, type erasure and interactions with primitive types still need to be handled carefully to avoid possible runtime exceptions.

Limitations and Solutions for Generic Arrays

Generic arrays are arrays created with generic type parameters. However, there are some restrictions in Java that do not allow direct creation of arrays with generic type parameters. This is due to the type erasure mechanism of Java generics.

limit:

  1. Cannot create an array with a generic type parameter: In Java, you cannot directly create an array with a generic type parameter, e.g. List<String>[]or T[].

  2. Compiler warning: If you try to create a generic array, the compiler will issue a warning saying "Generic array creation may cause unchecked or unsafe operations".

problem causes:

The type erasure mechanism of generics is the main reason why generic arrays cannot be created directly. Generics are erased to primitive types at compile time, so there is no way to get concrete information about generic types at runtime. This leads to the inability to determine the exact type of the array.

solution:

While direct creation of arrays with generic type parameters is restricted, the problem with generic arrays can be handled by the following two solutions:

1. Use wildcards or arrays of primitive types:

?Instead of specific generic type parameters, wildcards ( ) or arrays of primitive types can be used . For example, arrays of type List<?>[]or can be created. Object[]Although this method will not get type safety, it can bypass compile-time restrictions.

List<?>[] arrayOfLists = new List<?>[5];
Object[] objects = new Object[5];

It should be noted that since the exact type of the array cannot be determined, explicit type conversion may be required when accessing array elements.

2. Use a collection or other data structure:

Instead of arrays, you can use collections (eg ArrayList, , LinkedListetc.) or other data structures to store generic type parameters. This avoids the limitations and problems of using generic arrays directly.

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

The benefit of using collections is that they provide more flexible operations and type safety without the limitations of generic arrays.

Compatibility issues with generics and reflection

There are some compatibility issues between generics and reflection, which are caused by the type erasure mechanism of Java generics and the characteristics of reflection.

1. Information loss caused by type erasure: Generics are implemented in Java through type erasure, that is, at runtime, generic type parameters will be erased to primitive types (such as Object). This means that when using reflection, the specific information of the generic type parameter cannot be obtained, only the original type.

Solution: You can use reflection operations to obtain metadata (such as names, modifiers, generic parameters, etc.) of generic classes, generic methods, or generic fields, but you cannot accurately obtain the specific types of generic type parameters. In some cases, the generic marker interface can be used in combination to pass type information, so that more type information can be obtained in reflection operations.

2. Limitations of generic arrays: Arrays with generic type parameters cannot be created directly. This is due to the type erasure mechanism, which cannot determine the concrete type of a generic type parameter at runtime.

Solution:? An array of concrete generic type parameters can be replaced by using wildcards ( ) or arrays of primitive types. However, explicit type conversions may be required when accessing array elements.

3. Reflective invocation of generic methods: Type safety needs to be paid attention to when invoking generic methods through reflection. Since reflection operations are performed dynamically at runtime, the compiler cannot perform static type checking and thus may cause type errors.

Solution: When using reflection to call a generic method, you can ensure type safety by passing the correct parameter type, and perform appropriate type conversion on the return value.

4. Generic information limitation of Class object: For a specific generic type, the specific information of its generic type parameters cannot be obtained through the Class object. For example, for List<String>the type, the information that the generic type parameter is String cannot be obtained directly List.classfrom .

Solution: You can use third-party libraries such as the TypeToken class library to bypass this limitation. TypeToken can capture the specific information of generic type parameters through subclassing and anonymous inner classes.

7. Generic Programming Practices and Best Practices

Generic Programming Common Patterns and Techniques

1. Generic classes and interfaces: Defining generic classes or interfaces with type parameters allows code to work with different types of data. By using type parameters in a class or interface, a concrete type can be specified at instantiation time.

public class GenericClass<T> {
    
    
    private T value;
    
    public void setValue(T value) {
    
    
        this.value = value;
    }
    
    public T getValue() {
    
    
        return value;
    }
}

2. Generic method: define a generic method with type parameters, which allows the method to perform type inference based on the type of the parameter passed in when calling, and return the corresponding type.

public <T> T genericMethod(T value) {
    
    
    // 方法逻辑
    return value;
}

3. Wildcards: Use wildcards ( ?) to represent unknown types or limit type ranges, increasing code flexibility.

  • Unbounded wildcard: List<?>Indicates that any type of List can be stored.
  • Upper bound wildcard: List<? extends Number>Indicates that a List that can store Number and its subclasses.
  • Lower bound wildcard: List<? super Integer>Indicates that the List that can store Integer and its superclass.

4. Type limitations and constraints: Use type limitations and constraints to limit the scope of generic type parameters and provide more precise type information.

public <T extends Number> void processNumber(T number) {
    
    
    // 方法逻辑
}

5. Generics and inheritance relationships: Generic classes and interfaces can inherit and implement other generic classes and interfaces, and a richer generic hierarchy can be built through inheritance relationships.

public interface MyInterface<T> {
    
    
   // 接口定义
}

public class MyClass<T> implements MyInterface<T> {
    
    
   // 类定义
}

6. Generic arrays and collections: Using generic arrays and collections can handle different types of data collections, providing safer and more flexible data storage and manipulation methods.

List<String> stringList = new ArrayList<>();
stringList.add("Hello");
stringList.add("World");

String value = stringList.get(0);  // 获取元素,无需转换类型

7. Type inference: The diamond operator ( <>) introduced in Java 7 can automatically infer type parameters according to the context, making the code more concise.

Map<String, List<Integer>> map = new HashMap<>();  // 类型推断

Avoid Common Generics Mistakes and Pitfalls

1. Confusing primitive and generic types: When using generics, you should make sure to correctly distinguish between primitive and generic types. Primitive types have no type parameters and lose the benefits of generics.

How to Avoid: Declare classes, interfaces, and methods with generic type parameters, and specify the type parameters explicitly in your code.

2. Ignore type checking warnings: When using generics, the compiler may generate type checking warnings, which may lead to type safety issues if ignored.

Avoidance method:@SuppressWarnings Try to avoid directly ignoring type checking warnings, and you can solve or suppress warnings through reasonable type limitation, type conversion, or using annotations.

3. Create generic arrays: Generic arrays cannot be created directly because arrays in Java have a fixed type (covariance). Attempting to create a generic array may result in a compile-time error or a runtime exception.

How to avoid: You can use wildcards or arrays of primitive types instead of specific generic arrays. For example, use List<?>or List<Object>instead List<T>.

4. Generic type erasure: At runtime, generic type parameters will be erased to primitive types (such as Object), making it impossible to obtain specific information about generic type parameters.

How to avoid: The generic type erasure issue can be bypassed by passing a type token or using a third-party library such as TypeToken to get more type information.

5. Generics in a static context: Static fields, static methods, and static initialization blocks cannot refer to generic type parameters because they exist at class loading time and have nothing to do with instantiation.

How to avoid: If you need to use a generic type in a static context, you can declare the generic parameter as a local variable inside the static method.

6. Generics and variadic methods: Using the syntax in a generic method <T...>may cause a compilation error when calling a variadic method.

How to avoid: You can use bounded type wildcards ( T[]or List<T>) as parameter types, or use non-generic type parameters.

7. Boundary limitation of generic type parameters: When generic type parameters are bounded, pay attention to the reasonable use of these restrictions in the code and prevent type conversion errors.

How to avoid: Where appropriate, use boundary qualification to constrain generic type parameters, and perform corresponding operations and conversions in code based on the boundary type.

Guess you like

Origin blog.csdn.net/u012581020/article/details/131452314