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

2024-06-17 19:20

本文主要是介绍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); // Integerprint(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

这篇关于Java高手的30k之路|面试宝典|精通泛型的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1070328

相关文章

Java实现将HTML文件与字符串转换为图片

《Java实现将HTML文件与字符串转换为图片》在Java开发中,我们经常会遇到将HTML内容转换为图片的需求,本文小编就来和大家详细讲讲如何使用FreeSpire.DocforJava库来实现这一功... 目录前言核心实现:html 转图片完整代码场景 1:转换本地 HTML 文件为图片场景 2:转换 H

Java使用jar命令配置服务器端口的完整指南

《Java使用jar命令配置服务器端口的完整指南》本文将详细介绍如何使用java-jar命令启动应用,并重点讲解如何配置服务器端口,同时提供一个实用的Web工具来简化这一过程,希望对大家有所帮助... 目录1. Java Jar文件简介1.1 什么是Jar文件1.2 创建可执行Jar文件2. 使用java

SpringBoot实现不同接口指定上传文件大小的具体步骤

《SpringBoot实现不同接口指定上传文件大小的具体步骤》:本文主要介绍在SpringBoot中通过自定义注解、AOP拦截和配置文件实现不同接口上传文件大小限制的方法,强调需设置全局阈值远大于... 目录一  springboot实现不同接口指定文件大小1.1 思路说明1.2 工程启动说明二 具体实施2

Java实现在Word文档中添加文本水印和图片水印的操作指南

《Java实现在Word文档中添加文本水印和图片水印的操作指南》在当今数字时代,文档的自动化处理与安全防护变得尤为重要,无论是为了保护版权、推广品牌,还是为了在文档中加入特定的标识,为Word文档添加... 目录引言Spire.Doc for Java:高效Word文档处理的利器代码实战:使用Java为Wo

SpringBoot日志级别与日志分组详解

《SpringBoot日志级别与日志分组详解》文章介绍了日志级别(ALL至OFF)及其作用,说明SpringBoot默认日志级别为INFO,可通过application.properties调整全局或... 目录日志级别1、级别内容2、调整日志级别调整默认日志级别调整指定类的日志级别项目开发过程中,利用日志

Java中的抽象类与abstract 关键字使用详解

《Java中的抽象类与abstract关键字使用详解》:本文主要介绍Java中的抽象类与abstract关键字使用详解,本文通过实例代码给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧... 目录一、抽象类的概念二、使用 abstract2.1 修饰类 => 抽象类2.2 修饰方法 => 抽象方法,没有

SpringBoot 多环境开发实战(从配置、管理与控制)

《SpringBoot多环境开发实战(从配置、管理与控制)》本文详解SpringBoot多环境配置,涵盖单文件YAML、多文件模式、MavenProfile分组及激活策略,通过优先级控制灵活切换环境... 目录一、多环境开发基础(单文件 YAML 版)(一)配置原理与优势(二)实操示例二、多环境开发多文件版

Spring 中的切面与事务结合使用完整示例

《Spring中的切面与事务结合使用完整示例》本文给大家介绍Spring中的切面与事务结合使用完整示例,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考... 目录 一、前置知识:Spring AOP 与 事务的关系 事务本质上就是一个“切面”二、核心组件三、完

Java实现远程执行Shell指令

《Java实现远程执行Shell指令》文章介绍使用JSch在SpringBoot项目中实现远程Shell操作,涵盖环境配置、依赖引入及工具类编写,详解分号和双与号执行多指令的区别... 目录软硬件环境说明编写执行Shell指令的工具类总结jsch(Java Secure Channel)是SSH2的一个纯J

JavaScript中比较两个数组是否有相同元素(交集)的三种常用方法

《JavaScript中比较两个数组是否有相同元素(交集)的三种常用方法》:本文主要介绍JavaScript中比较两个数组是否有相同元素(交集)的三种常用方法,每种方法结合实例代码给大家介绍的非常... 目录引言:为什么"相等"判断如此重要?方法1:使用some()+includes()(适合小数组)方法2