Java高手的30k之路|面试宝典|精通泛型

泛型

知识点

在Java高级开发中,掌握泛型(Generics)是非常重要的,它是Java语言中的一项重要特性,提供了编译时类型安全检查机制,使得代码更加灵活和可重用。以下是Java高级开发需要掌握的泛型知识点:

  1. 泛型基础

    • 理解泛型的定义和基本语法,包括泛型类、泛型方法、泛型接口。
    • 泛型用法示例:class MyClass<T> {}public <T> void myMethod(T t) {}
  2. 泛型通配符

    • 理解 <?><? extends T><? super T> 等通配符的含义和使用场景。
    • 了解上界通配符和下界通配符在泛型方法和泛型类中的应用。
  3. 泛型和继承关系

    • 理解泛型在继承和子类化中的表现,包括泛型类的继承、通配符的上下界限定。
  4. 类型擦除

    • 理解Java泛型的类型擦除机制,即在运行时泛型类型信息被擦除为原始类型。
    • 泛型类和泛型方法在编译后如何处理为非泛型形式的字节码。
  5. 泛型和集合

    • 熟悉Java集合框架中泛型的应用,如 List<T>Map<K, V> 等。
    • 掌握使用泛型提高集合类型安全性的方法。
  6. 泛型方法

    • 理解泛型方法的定义和使用,以及与泛型类的区别。
    • 了解泛型方法如何在静态方法和实例方法中应用。
  7. 泛型的好处

    • 掌握泛型的优势,如提高代码的类型安全性、避免类型转换错误、增加代码的可读性和重用性等。
  8. 泛型和反射

    • 理解泛型和反射的结合使用,如何通过反射获取泛型信息。
  9. 泛型约束

    • 熟悉泛型的类型约束,如泛型的边界限定(extends 和 super 关键字)。
  10. 泛型和异常

    • 了解泛型和异常处理的结合使用,如何处理泛型异常。

使用

1. 泛型类和泛型接口

限定类型可以在定义泛型类和泛型接口时使用。

// 限定类型 T 必须是 Number 的子类
public class NumberBox<T extends Number> {
    private T content;

    public void setContent(T content) {
        this.content = content;
    }

    public T getContent() {
        return content;
    }
}

// 泛型接口示例
public interface NumberProcessor<T extends Number> {
    void process(T number);
}

2. 泛型方法

在定义泛型方法时,可以使用限定类型来约束方法的类型参数。

public class Utility {
    
    // 泛型方法,限定类型 T 必须是 Number 的子类
    public static <T extends Number> void printNumber(T number) {
        System.out.println("Number: " + number);
    }
}

3. 泛型通配符

限定类型也可以在泛型通配符中使用,以表示方法参数或返回值的类型约束。

import java.util.List;

public class WildcardExample {

    // 通配符上限,list 中的元素必须是 Number 的子类
    public static void printNumbers(List<? extends Number> list) {
        for (Number number : list) {
            System.out.println(number);
        }
    }

    // 通配符下限,list 中的元素必须是 Integer 的父类
    public static void addIntegers(List<? super Integer> list) {
        list.add(1);
        list.add(2);
    }
}

4. 多重限定

如果类型参数需要同时满足多个接口,可以使用 & 符号进行多重限定。

// T 必须同时实现 Comparable<T> 和 Serializable 接口
public class MultiBound<T extends Comparable<T> & Serializable> {
    private T content;

    public void setContent(T content) {
        this.content = content;
    }

    public T getContent() {
        return content;
    }
}

5. 示例业务场景

下面通过一些具体的业务场景示例来说明限定类型的使用。

示例 1: 数据处理器

假设我们有一个数据处理器,需要处理不同类型的数值数据,我们可以使用泛型类来实现。

public class DataProcessor<T extends Number> {
    private T data;

    public DataProcessor(T data) {
        this.data = data;
    }

    public void process() {
        System.out.println("Processing data: " + data);
    }

    public T getData() {
        return data;
    }
}

public class Main {
    public static void main(String[] args) {
        DataProcessor<Integer> intProcessor = new DataProcessor<>(100);
        intProcessor.process();

        DataProcessor<Double> doubleProcessor = new DataProcessor<>(99.99);
        doubleProcessor.process();
    }
}
示例 2: 数字打印工具

我们可以使用泛型方法来创建一个打印数字的工具方法。

public class PrintUtil {
    
    public static <T extends Number> void print(T number) {
        System.out.println("Number: " + number);
    }
    
    public static void main(String[] args) {
        print(123); // Integer
        print(45.67); // Double
    }
}
示例 3: 集合操作工具

使用泛型通配符来创建一个工具类,用于操作集合中的元素。

import java.util.List;
import java.util.ArrayList;

public class CollectionUtil {
    
    public static void printNumbers(List<? extends Number> list) {
        for (Number number : list) {
            System.out.println("Number: " + number);
        }
    }

    public static void addNumbers(List<? super Integer> list) {
        list.add(10);
        list.add(20);
    }

    public static void main(String[] args) {
        List<Integer> intList = new ArrayList<>();
        addNumbers(intList);
        printNumbers(intList);
    }
}

结合具体场景的最佳实践

当然可以。以下是结合具体业务场景的Java泛型最佳实践讲解:

1. 确保类型安全

业务场景:一个电商系统需要存储和处理不同类型的订单(如书籍订单和电子产品订单)。

示例

class Order { /*...*/ }
class BookOrder extends Order { /*...*/ }
class ElectronicsOrder extends Order { /*...*/ }

List<Order> orders = new ArrayList<>();
orders.add(new BookOrder());
orders.add(new ElectronicsOrder());
// orders.add(new String()); // 编译时会报错,防止插入非订单类型

通过使用泛型,确保集合中只存储订单对象,防止错误类型的插入。

2. 使用通配符提高代码灵活性

业务场景:需要处理一个包含各种类型商品的库存。

上界通配符(Producer-Extends)

public void printInventory(List<? extends Product> inventory) {
    for (Product p : inventory) {
        System.out.println(p.getName());
    }
}

List<Book> books = new ArrayList<>();
List<Electronic> electronics = new ArrayList<>();
printInventory(books);
printInventory(electronics);

通过上界通配符,可以处理各种类型的商品清单。

下界通配符(Consumer-Super)

public void addElectronics(List<? super Electronics> inventory) {
    inventory.add(new Electronics());
    inventory.add(new Smartphone()); // Smartphone extends Electronics,注意这里可以添加三种类:父类,自己,子类(多态)
}

List<Product> products = new ArrayList<>();
addElectronics(products);

通过下界通配符,可以向商品列表中添加各种电子产品。

3. 避免使用原始类型

业务场景:一个社交媒体应用需要存储用户评论。

示例

List<Comment> comments = new ArrayList<>();
comments.add(new Comment("Great post!"));
// comments.add(new User()); // 编译时会报错,防止插入非评论类型

通过使用泛型,确保集合中只包含评论对象。

4. 使用泛型方法提高代码重用性

业务场景:一个文件处理系统,需要打印不同类型的文件内容。

示例

public <T> void printFiles(T[] files) {
    for (T file : files) {
        System.out.println(file.toString());
    }
}

Document[] documents = {new Document("Doc1"), new Document("Doc2")};
Image[] images = {new Image("Image1"), new Image("Image2")};
printFiles(documents);
printFiles(images);

通过使用泛型方法,可以打印任何类型的文件内容,提高代码重用性。

5. 使用有界类型参数进行约束

业务场景:一个排行榜系统,需要找到分数最高的用户。

示例

public <T extends Comparable<T>> T findTopScorer(T[] scores) {
    T top = scores[0];
    for (T score : scores) {
        if (score.compareTo(top) > 0) {
            top = score;
        }
    }
    return top;
}

Integer[] intScores = {85, 92, 88};
Double[] doubleScores = {85.5, 92.3, 88.9};
System.out.println(findTopScorer(intScores)); // 输出92
System.out.println(findTopScorer(doubleScores)); // 输出92.3

通过使用有界类型参数,可以确保数组中的元素可以比较,从而找到最高分数。

6. 避免使用泛型类型的静态成员

业务场景:一个用户管理系统,需要存储用户信息。

示例

public class UserManager<T> {
    private T user;
    // private static T instance; // 编译错误,避免使用泛型类型的静态成员
}

通过避免使用泛型类型的静态成员,防止类型擦除带来的问题。

7. 使用类型令牌解决类型擦除问题

业务场景:一个对象工厂,需要根据类型创建对象实例。

示例

public <T> T createInstance(Class<T> clazz) throws Exception {
    return clazz.getDeclaredConstructor().newInstance();
}

User user = createInstance(User.class);

通过使用类型令牌,可以在运行时获取泛型类型信息,创建对象实例。

8. 避免在泛型类中使用泛型数组

业务场景:一个订单管理系统,需要存储不同类型的订单。

示例

public class OrderManager<T> {
    private T[] orders; // 编译错误,避免使用泛型数组
    private List<T> orderList = new ArrayList<>();
}

通过避免使用泛型数组,防止类型擦除带来的问题。

9. 谨慎使用泛型和异常

业务场景:一个数据处理系统,需要处理不同类型的数据。

示例

// 错误示例
public <T extends Exception> void processData() throws T {
    // 不能抛出或捕获泛型异常类型
}

避免抛出或捕获泛型异常类型。

10. 使用PECS原则

业务场景:一个物流系统,需要处理和添加不同类型的货物。

示例

// Producer-Extends
public void processCargo(List<? extends Cargo> cargoList) {
    for (Cargo cargo : cargoList) {
        System.out.println(cargo.getDetails());
    }
}

// Consumer-Super
public void addCargo(List<? super PerishableCargo> cargoList) {
    cargoList.add(new PerishableCargo());
    cargoList.add(new FreshCargo()); // FreshCargo extends PerishableCargo
}

List<Cargo> cargos = new ArrayList<>();
addCargo(cargos);

通过PECS原则,可以处理和添加不同类型的货物。

泛型的实现原理及其在运行时的表现

1. 泛型的本质

Java 泛型在编译时提供类型检查和类型安全,允许开发人员编写更灵活且类型安全的代码。然而,在运行时,Java 泛型会被类型擦除(Type Erasure),这意味着所有的类型参数都会被擦除,并替换为它们的限定类型(如果没有指定,则替换为 Object)。

2. 类型擦除(Type Erasure)

类型擦除是 Java 泛型的核心机制。在编译时,编译器会移除泛型类型信息并插入必要的类型转换,以确保类型安全。在运行时,泛型类型信息不存在,所有泛型类型都被替换为原始类型。

例如,以下泛型类:

public class Box<T> {
    private T content;

    public void setContent(T content) {
        this.content = content;
    }

    public T getContent() {
        return content;
    }
}

在编译后,类型参数 T 会被替换为 Object

public class Box {
    private Object content;

    public void setContent(Object content) {
        this.content = content;
    }

    public Object getContent() {
        return content;
    }
}

编译器在插入和取出元素时会生成适当的类型转换代码,以确保类型安全。

3. 限定类型

如果泛型类型参数有上限限制,如 T extends Number,类型擦除后会替换为限定类型,而不是 Object

例如:

public class NumberBox<T extends Number> {
    private T content;

    public void setContent(T content) {
        this.content = content;
    }

    public T getContent() {
        return content;
    }
}

在编译后,类型参数 T 会被替换为 Number

public class NumberBox {
    private Number content;

    public void setContent(Number content) {
        this.content = content;
    }

    public Number getContent() {
        return content;
    }
}

4. 泛型方法

泛型方法在运行时也会经历类型擦除,其类型参数在运行时被替换为限定类型或 Object

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

在编译后,类型参数 T 被替换为 Object

public static void printArray(Object[] array) {
    for (Object element : array) {
        System.out.println(element);
    }
}

5. 泛型数组

Java 不允许创建泛型类型的数组,因为在运行时泛型类型被擦除,数组的运行时类型需要具体的类型信息。

以下代码是非法的:

List<String>[] listArray = new List<String>[10]; // 编译错误

解决方法是使用通配符或 Object

List<?>[] listArray = new List<?>[10];

6. 泛型与反射

由于类型擦除,在使用反射时,无法获取泛型类型参数的具体类型信息。例如:

List<String> list = new ArrayList<>();
Type type = list.getClass().getGenericSuperclass();
System.out.println(type); // 输出 java.util.AbstractList<E>

上面的代码只能得到泛型类型参数 E,而不是具体的 String 类型。

为什么输出 java.util.AbstractList<E>

ArrayList 继承自 AbstractList,而 AbstractList 又是AbstractCollection 的子类。具体的继承关系如下:

  • ArrayList<E> extends AbstractList<E>
  • AbstractList<E> extends AbstractCollection<E>

当你调用 list.getClass().getGenericSuperclass() 时,getGenericSuperclass() 方法返回的是直接父类的类型,即 AbstractList<E>。这个类型包含泛型信息,但因为泛型类型参数 E 是在编译时擦除的,实际运行时显示的是泛型类型 E

小结

Java 泛型通过类型擦除实现,在编译时确保类型安全,但在运行时移除类型信息。类型擦除机制使得 Java 泛型在运行时没有性能开销,并且与非泛型代码兼容。然而,这也导致在运行时无法获取具体的泛型类型信息,需要通过其他方式(如反射)来处理泛型类型。

类型擦除的例外

在 Java 中,泛型类型信息确实会在编译期间被擦除,这就是所谓的类型擦除(type erasure)。类型擦除的基本概念是:泛型参数类型在编译时被替换为它们的非泛型上界(通常是 Object,除非有特定的边界),并在必要时插入类型转换。类型擦除的结果是,在运行时,泛型参数的实际类型信息是不可用的。然而,有一些情况下,类型信息是可以保留的,主要依赖于编译器在生成 class 文件时的额外信息。以下是几种情况:

  • 字段:类的字段声明为泛型时,泛型信息会存储在 class 文件的字段信息部分。
  • 方法参数和返回类型:方法参数或返回类型使用泛型时,泛型信息会存储在 class 文件的方法签名部分。
  • 类和接口:类或接口本身声明为泛型时,泛型信息会存储在 class 文件的类型信息部分。

1. 字段的泛型类型

当一个类的字段声明为泛型类型时,编译器会将泛型类型的信息存储在 class 文件的字段信息部分。这使得在运行时,通过反射 API 可以获取到这些泛型类型的信息。比如:

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;

public class GenericField {
    
    private List<String> stringList;
    
    public static void main(String[] args) throws NoSuchFieldException {
        Field field = GenericField.class.getDeclaredField("stringList");
        Type genericFieldType = field.getGenericType();
        
        if (genericFieldType instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) genericFieldType;
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
            for (Type type : actualTypeArguments) {
                System.out.println(type);
            }
        }
    }
}

在这个示例中,stringList 字段的泛型类型信息在编译期被存储在 class 文件中,虽然在运行时无法直接使用泛型类型,但可以通过反射 API 获取到这些信息。输出结果为 class java.lang.String

2. 方法的泛型参数类型

类似地,当方法的参数或返回类型使用泛型时,编译器会在 class 文件中存储这些泛型信息。在运行时,可以通过反射获取这些信息。比如:

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;

public class GenericMethod {
    
    public <T> void printList(List<T> list) {
        // Method body
    }
    
    public static void main(String[] args) throws NoSuchMethodException {
        Method method = GenericMethod.class.getMethod("printList", List.class);
        Type[] genericParameterTypes = method.getGenericParameterTypes();
        
        for (Type type : genericParameterTypes) {
            if (type instanceof ParameterizedType) {
                ParameterizedType parameterizedType = (ParameterizedType) type;
                Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
                for (Type actualType : actualTypeArguments) {
                    System.out.println(actualType);
                }
            }
        }
    }
}

在这个示例中,printList 方法的参数 list 的泛型类型信息被存储在 class 文件中,可以在运行时通过反射获取。

总结

类型擦除确实会在运行时丢失泛型参数的具体类型信息,但是编译器会在 class 文件中存储一些必要的泛型信息,使得可以在运行时通过反射获取这些信息。因此,通过 ParameterizedType,可以在运行时获取泛型参数的类型信息,这在某些情况下非常有用。

ParameterizedType

ParameterizedType 是 Java 反射 API 中的一个接口,它表示一个带有实际类型参数的泛型类型。在实际开发中,我们经常会遇到需要在运行时获取泛型类型参数的情况,这时 ParameterizedType 就非常有用。

ParameterizedType 接口用于表示参数化类型。一个参数化类型是指带有实际类型参数的类型,比如 List<String>Map<String, Integer> 等。通过反射 API,可以在运行时获取这些参数化类型的实际类型参数。

常用方法

ParameterizedType 接口定义了一些方法,用于获取参数化类型的详细信息:

  • Type[] getActualTypeArguments():返回实际类型参数的数组。
  • Type getRawType():返回不带泛型参数的原始类型。
  • Type getOwnerType():返回这个类型的所有者类型,如果这个类型是一个内部类的话。

使用示例

下面通过一个具体的例子来说明如何使用 ParameterizedType 获取泛型类型参数。

示例 1:获取类的泛型类型参数
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;

public class GenericClass<T> {
    
    public static void main(String[] args) {
        // 使用匿名子类来保留泛型类型信息
        GenericClass<List<String>> instance = new GenericClass<List<String>>() {};
        Type superclass = instance.getClass().getGenericSuperclass();
        
        if (superclass instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) superclass;
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
            for (Type type : actualTypeArguments) {
                System.out.println(type);
            }
        }
    }
}

在这个示例中,通过创建 GenericClass 的匿名子类,我们保留了泛型类型信息。然后使用反射获取泛型类型参数,输出结果为 java.util.List<java.lang.String>

示例 2:获取方法的泛型类型参数
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;

public class GenericMethod {
    
    public <T> void printList(List<T> list) {
        // Method body
    }
    
    public static void main(String[] args) throws NoSuchMethodException {
        Method method = GenericMethod.class.getMethod("printList", List.class);
        Type[] genericParameterTypes = method.getGenericParameterTypes();
        
        for (Type type : genericParameterTypes) {
            if (type instanceof ParameterizedType) {
                ParameterizedType parameterizedType = (ParameterizedType) type;
                Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
                for (Type actualType : actualTypeArguments) {
                    System.out.println(actualType);
                }
            }
        }
    }
}

在这个示例中,我们通过反射获取了泛型方法 printList 的类型参数,输出结果为 T

示例 3:获取字段的泛型类型参数
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;

public class GenericField {
    
    private List<String> stringList;
    
    public static void main(String[] args) throws NoSuchFieldException {
        Field field = GenericField.class.getDeclaredField("stringList");
        Type genericFieldType = field.getGenericType();
        
        if (genericFieldType instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) genericFieldType;
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
            for (Type type : actualTypeArguments) {
                System.out.println(type);
            }
        }
    }
}

在这个示例中,我们通过反射获取了泛型字段 stringList 的类型参数,输出结果为 java.lang.String

  • 11
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值