还不懂Java的泛型?只用这一篇文章,保证你面试对答如流

简介: 还不懂Java的泛型?只用这一篇文章,保证你面试对答如流

最近技术交流群里,有朋友问:Object和泛型T有啥区别。回答完问题,不禁在想,面试在即,还有那么多朋友不了泛型?是时候给大家整理一篇泛型相关的文章了,一篇文章全面搞定泛型,让大家再也不愁面试或实践中泛型相关的问题了。

什么是泛型

泛型是在JDK 5时就引入的新特性,也就是“参数化类型”,通俗来讲就是将原来的具体类型通过参数化来定义,使用或调用时再传入具体的类型(类型实参)。

泛型的本质是为了参数化类型(在不创建新类型的前提下,通过泛型指定的不同类型来控制形参具体的类型)。在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

为什么使用泛型

未使用泛型时,可以通过Object来实现参数的“任意化”,但这样做的缺点就是需要显式的强制类型转换,这就需要开发者知道实际的类型。

而强制类型转换是会出现错误的,比如Object将实际类型为String,强转成Integer。编译期是不会提示错误的,而在运行时就会抛出异常,很明显的安全隐患。

Java通过引入泛型机制,将上述的隐患提前到编译期进行检查,开发人员既可明确的知道实际类型,又可以通过编译期的检查提示错误,从而提升代码的安全性和健壮性。

使用泛型前后的对比

拿一个经典的例子来演示一下未使用泛型会出现的问题。

List list = new ArrayList();
list.add(1);
list.add("zhuan2quan");
list.add("程序新视界");
for (int i = 0; i < list.size(); i++) {
    String value = (String) list.get(i);
    System.out.println("value=" + value);
}

上述代码在编译器并不会报任何错误,但当执行时会抛出如下异常:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

那么,是否可以在编译器就解决这个问题,而不是在运行期抛出异常呢?泛型应运而生。上述代码通过泛型来写之后,变成如下形式:

List<String> list = new ArrayList<>();
list.add(1);
list.add("zhuan2quan");
list.add("程序新视界");
for (String value : list) {
    System.out.println("value=" + value);
}

可以看出,代码变得更加清爽简单,而且list.add(1)这行代码在IDE中直接会提示错误信息:

Required type: String
Provided: int

提示错误信息便是泛型对向List中添加的数据产生了约束,只能是String类型。

泛型中通配符

在使用泛型时经常会看到T、E、K、V这些通配符,它们代表着什么含义呢?

本质上它们都是通配符,并没有什么区别,换成A-Z之间的任何字母都可以。不过在开发者之间倒是有些不成文的约定:

  • T (type) 表示具体的一个java类型;
  • K V (key value) 分别代表java键值中的Key Value;
  • E (element) 代表Element;

为什么Java的泛型是假泛型

为了做到向下兼容,Java中的泛型仅仅是一个语法糖,并不是C++那样的真泛型。

还是上面的例子,在直接向泛型为String的List中添加int类型会提示错误:

List<String> list = new ArrayList<>();
list.add(1);

针对上述代码,我们采用反射间接地调用add方法:

@Test
public void test3() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    List<Integer> list = new ArrayList<>();
    list.add(1);
    Method add = list.getClass().getMethod("add", Object.class);
    add.invoke(list,"程序新视界");
    System.out.println(list);
    System.out.println(list.get(1));
}

执行上述代码,我们发现程序并没有抛出异常,正常打印出入:

[1, 程序新视界]
程序新视界

原本只能装入Integer的List,成功装入了一个String类型的值。由此可见,所谓的泛型确实是假泛型。

同时,我们还可以通过字节码来证明。拿上面使用了泛型的实例代码,通过javap -c命令来看看字节码:

Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #6                  // String zhuan2quan
      11: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      16: pop
      17: aload_1
      18: ldc           #7                  // String 程序新视界
      20: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      25: pop
      26: aload_1
      27: invokeinterface #18,  1           // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
      32: astore_2
      33: aload_2
      34: invokeinterface #19,  1           // InterfaceMethod java/util/Iterator.hasNext:()Z
      39: ifeq          80

从字节码中可以看出,List.add方法本质上就是一个Object。再次证明,Java的泛型仅仅在编译期有效,在运行期则会被擦除,也就是说所有的泛型参数类型在编译后都会被清除掉。这就是我们经常说的类型擦除。

因此,也可以说:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。

泛型的定义与使用

泛型有三类,分别为:泛型类、泛型接口、泛型方法。

在学习这三种类型的泛型使用场景之前,我们需要明确一个基本准则,那就是泛型的声明通常都是通过<>配合大写字母来定义的,比如<T>。只不过不同类型,声明的位置不同,使用的方式也有所不同。

泛型类

泛型类的语法形式:

class name<T1, T2, ..., Tn> { /* ... */ }

泛型类的声明和非泛型类的声明类似,只是在类名后面添加了类型参数声明部分。由尖括号(<>)分隔的类型参数部分跟在类名后面。它指定类型参数(也称为类型变量)T1,T2,...和 Tn。一般将泛型中的类名称为原型,而将<>指定的参数称为类型参数。

使用示例:

// T为任意标识,比如用T、E、K、V等表示泛型
public class Foo<T> {
    // 泛化的成员变量,T的类型由外部指定
    private T info;
    // 构造方法类型为T,T的类型由外部指定
    public Foo(T info){
        this.info = info;
    }
    // 方法返回值类型为T,T的类型由外部指定
    public T getInfo() {
        return info;
    }
    public static void main(String[] args) {
        // 实例化泛型类时,必须指定T的具体类型,这里为String。
        // 传入的实参类型需与泛型的类型参数类型相同,这里为String。
        Foo<String> foo = new Foo<>("程序新视界");
        System.out.println(foo.getInfo());
    }
}

当然,上述示例中在使用泛型类时也可以不指定实际类型,语法上支持,那么此时与未定义泛型一样,不推荐这种方式。

Foo foo11 = new Foo(1);

比如上述写法,也是可行的,但时区了定义泛型的意义了。

泛型接口

泛型接口的声明与泛型类一致,泛型接口语法形式:

public interface Context<T> {
    T getContext();
}

泛型接口有两种实现方式:子类明确声明泛型类型和子类不明确声明泛型类型。

先看子类明确声明泛型类型的示例:

// 实现泛型接口时已传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
public class TomcatContext implements Context<String> {
    @Override
    public String getContext() {
        return "Tomcat";
    }
}

子类不明确声明泛型类型:

// 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
public class SpringContext<T> implements Context<T>{
    @Override
    public T getContext() {
        return null;
    }
}

当然,还有一种情况,就是我们把定义为泛型的类像前面讲的一样当做普通类使用。

上面的示例中泛型参数都是一个,当然也可以指定两个或多个:

public interface GenericInterfaceSeveralTypes< T, R > {
    R performAction( final T action );
}

多个泛型参数可以用逗号(,)进行分割。

泛型方法

泛型类是在实例化类时指明泛型的具体类型;泛型方法是在调用方法时指明泛型的具体类型。泛型方法可以是普通方法、静态方法、抽象方法、final修饰的方法以及构造方法。

泛型方法语法形式如下:

public <T> T func(T obj) {}

尖括号内为类型参数列表,位于方法返回值T或void关键字之前。尖括号内定义的T,可以用在方法的任何地方,比如参数、方法内和返回值。

protected abstract<T, R> R performAction( final T action );
static<T, R> R performActionOn( final Collection< T > action ) {
    final R result = ...;
    // Implementation here
    return result;
}

上述实例中可以看出泛型方法同样可以定义多个泛型类型。

再看一个示例代码:

public class GenericsMethodDemo1 {
    //1、public与返回值中间<T>,声明此方法的泛型类型。
    //2、只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
    //3、<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
    //4、T可以为任意标识,如T、E、K、V等。
    public static <T> T printClass(T obj) {
        System.out.println(obj);
        return obj;
    }
    public static void main(String[] args) {
        printClass("abc");
        printClass(123);
    }
}

需要注意的是,泛型方法与类是否是泛型无关。另外,静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。

上述示例中如果GenericsMethodDemo1定义为GenericsMethodDemo1<T>,则printClass方法是无法直接使用到类上的T的,只能像上面代码那样访问自身定义的T。

泛型方法与普通方法区别

下面,我们对比一下泛型方法和非泛型方法的区别:

// 方法一
public T getKey(){
    return key;
}
// 方法二
public <T> T showKeyName(T t){
    return t;
}

其中方法一虽然使用了T这个泛型声明,但它用的是泛型类中定义的变量,因此这个方法并不是泛型方法。而像方法二中通过两个尖括号声明了T,这个才是真正的泛型方法。

对于方法二,还有一种情况,那就是类中也声明了T,那么该方法参数的T指的只是此方法的T,而并不是类的T。

泛型方法与可变参数
@SafeVarargs
public final <T> void print(T... args){
 for(T t : args){
  System.out.println("t=" + t);
 }
}
public static void main(String[] args) {
 GenericDemo2 demo2 = new GenericDemo2();
 demo2.print("abc",123);
}

print方法打印出可变参数args中的结果,而且可变参数可以传递不同的具体类型。

打印结果:

t=abc
t=123

关于泛型方法总结一下就是:如果能使用泛型方法尽量使用泛型方法,这样能将泛型所需到最需要的范围内。如果使用泛型类,则整个类都进行了泛化处理。

泛型通配符

类型通配符一般是使用?代替具体的类型实参(此处是类型实参,而不是类型形参)。当操作类型时不需要使用类型的具体功能时,只使用Object类中的功能,那么可以用?通配符来表未知类型。例如List<?>在逻辑上是List<String>、List<Integer>等所有List<具体类型实参>的父类。

/**
 * 在使用List<Number>作为形参的方法中,不能使用List<Ingeter>的实例传入,
 * 也就是说不能把List<Integer>看作为List<Number>的子类;
 */
public static void getNumberData(List<Number> data) {
    System.out.println("data :" + data.get(0));
}
/**
 * 在使用List<String>作为形参的方法中,不能使用List<Number>的实例传入;
 */
public static void getStringData(List<String> data) {
    System.out.println("data :" + data.get(0));
}
/**
 * 使用类型通配符可以表示同时是List<Integer>和List<Number>、List<String>的引用类型。
 * 类型通配符一般是使用?代替具体的类型实参,注意此处是类型实参;
 * 和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。
 */
public static void getData(List<?> data) {
    System.out.println("data :" + data.get(0));
}

上述三个方法中,getNumberData只能传递List<Number>类型的参数,getStringData只能传递List<String>类型的参数。如果它们都只使用了Object类的功能,则可以通过getData方法的形式进行声明,则同时支持各种类型。

上述这种类型的通配符也称作无界通配符,有两种应用场景:

  • 可以使用Object类中提供的功能来实现的方法。
  • 使用不依赖于类型参数的泛型类中的方法。

在getData中使用了?作为通配符,但在某些场景下,需要对泛型类型实参进行上下边界的限制。如:类型实参只准传入某种类型的父类或某种类型的子类。

上界通配符示例如下:

/**
 * 类型通配符上限通过形如List来定义,如此定义就是通配符泛型值接受Number及其下层子类类型。
 */
public static void getUperNumber(List<? extends Number> data) {
    System.out.println("data :" + data.get(0));
}

通过extends限制了通配符的上边界,也就是只接受Number及其子类类型。接口的实现和类的集成都可以通过extends来表示。

而这里的Number也可以替换为T,表示该通配符所代表的类型是T类型的子类。

public static void getData(List<? extends T> data) {
    System.out.println("data :" + data.get(0));
}

与上界通配符示对照也有下界通配符:

public static void getData(List<? super Integer> data) {
    System.out.println("data :" + data.get(0));
}

下界通配符表示该通配符所代表的类型是T类型的父类。

泛型的限制

原始类型(比如:int,long,byte等)无法用于泛型,在使用的过程中需要通过它们的包装类(比如:Integer, Long, Byte等)来替代。

final List< Long > longs = new ArrayList<>();
final Set< Integer > integers = new HashSet<>();

当然,在使用的过程中会涉及到自动拆箱和自动装箱的操作:

final List< Long > longs = new ArrayList<>();
longs.add( 0L ); // 'long' 包装为 'Long'
long value = longs.get( 0 ); // 'Long'解包'long'

泛型的类型推断

当引入泛型之后,每处用到泛型的地方都需要开发人员加入对应的泛型类型,比如:

final Map<String, Collection<String>> map =
    new HashMap<String, Collection<String>>();
for(final Map.Entry< String, Collection<String> > entry: map.entrySet()) {
}

为了解决上述问题,在Java7中引入了运算符<>,编译器可以推断出该运算符所代表的原始类型。

因此,Java7及以后,泛型对象的创建变为如下形式:

final Map< String, Collection<String>> map = new HashMap<>();

小结

本篇文章带大家从为什么使用泛型到如何在不同场景下使用泛型都进行了逐步的讲解。通过本篇文章的学习,基本上可以应对使用和面试过程中90%以后上的场景。如果对你有所帮助,顺手可以给个赞。

参考文章:

https://blog.csdn.net/s10461/article/details/53941091
https://www.cnblogs.com/jingmoxukong/p/12049160.html
https://blog.csdn.net/lxxiang1/article/details/81429987
https://www.javacodegeeks.com/2015/09/how-and-when-to-use-generics.html

目录
相关文章
|
4月前
|
缓存 Java 关系型数据库
2025 年最新华为 Java 面试题及答案,全方位打造面试宝典
Java面试高频考点与实践指南(150字摘要) 本文系统梳理了Java面试核心考点,包括Java基础(数据类型、面向对象特性、常用类使用)、并发编程(线程机制、锁原理、并发容器)、JVM(内存模型、GC算法、类加载机制)、Spring框架(IoC/AOP、Bean生命周期、事务管理)、数据库(MySQL引擎、事务隔离、索引优化)及分布式(CAP理论、ID生成、Redis缓存)。同时提供华为级实战代码,涵盖Spring Cloud Alibaba微服务、Sentinel限流、Seata分布式事务,以及完整的D
202 2
|
4月前
|
存储 安全 Java
常见 JAVA 集合面试题整理 自用版持续更新
这是一份详尽的Java集合面试题总结,涵盖ArrayList与LinkedList、HashMap与HashTable、HashSet与TreeSet的区别,以及ConcurrentHashMap的实现原理。内容从底层数据结构、性能特点到应用场景逐一剖析,并提供代码示例便于理解。此外,还介绍了如何遍历HashMap和HashTable。无论是初学者还是进阶开发者,都能从中受益。代码资源可从[链接](https://pan.quark.cn/s/14fcf913bae6)获取。
222 3
|
3月前
|
缓存 Java API
Java 面试实操指南与最新技术结合的实战攻略
本指南涵盖Java 17+新特性、Spring Boot 3微服务、响应式编程、容器化部署与数据缓存实操,结合代码案例解析高频面试技术点,助你掌握最新Java技术栈,提升实战能力,轻松应对Java中高级岗位面试。
336 0
|
9天前
|
安全 Java
Java之泛型使用教程
Java之泛型使用教程
111 10
|
4月前
|
存储 安全 Java
2025 最新史上最全 Java 面试题独家整理带详细答案及解析
本文从Java基础、面向对象、多线程与并发等方面详细解析常见面试题及答案,并结合实际应用帮助理解。内容涵盖基本数据类型、自动装箱拆箱、String类区别,面向对象三大特性(封装、继承、多态),线程创建与安全问题解决方法,以及集合框架如ArrayList与LinkedList的对比和HashMap工作原理。适合准备面试或深入学习Java的开发者参考。附代码获取链接:[点此下载](https://pan.quark.cn/s/14fcf913bae6)。
1416 48
|
3月前
|
安全 Java API
在Java中识别泛型信息
以上步骤和示例代码展示了怎样在Java中获取泛型类、泛型方法和泛型字段的类型参数信息。这些方法利用Java的反射API来绕过类型擦除的限制并访问运行时的类型信息。这对于在运行时进行类型安全的操作是很有帮助的,比如在创建类型安全的集合或者其他复杂数据结构时处理泛型。注意,过度使用反射可能会导致代码难以理解和维护,因此应该在确有必要时才使用反射来获取泛型信息。
123 11
|
4月前
|
算法 架构师 Java
Java 开发岗及 java 架构师百度校招历年经典面试题汇总
以下是百度校招Java岗位面试题精选摘要(150字): Java开发岗重点关注集合类、并发和系统设计。HashMap线程安全可通过Collections.synchronizedMap()或ConcurrentHashMap实现,后者采用分段锁提升并发性能。负载均衡算法包括轮询、加权轮询和最少连接数,一致性哈希可均匀分布请求。Redis持久化有RDB(快照恢复快)和AOF(日志更安全)两种方式。架构师岗涉及JMM内存模型、happens-before原则和无锁数据结构(基于CAS)。
116 5
|
4月前
|
Java API 微服务
2025 年 Java 校招面试全攻略:从面试心得看 Java 岗位求职技巧
《2025年Java校招最新技术要点与实操指南》 本文梳理了2025年Java校招的核心技术栈,并提供了可直接运行的代码实例。重点技术包括: Java 17+新特性(Record类、Sealed类等) Spring Boot 3+WebFlux响应式编程 微服务架构与Spring Cloud组件 Docker容器化部署 Redis缓存集成 OpenAI API调用 通过实际代码演示了如何应用这些技术,如Java 17的Record类简化POJO、WebFlux构建响应式API、Docker容器化部署。
153 5
|
4月前
|
缓存 NoSQL Java
Java Redis 面试题集锦 常见高频面试题目及解析
本文总结了Redis在Java中的核心面试题,包括数据类型操作、单线程高性能原理、键过期策略及分布式锁实现等关键内容。通过Jedis代码示例展示了String、List等数据类型的操作方法,讲解了惰性删除和定期删除相结合的过期策略,并提供了Spring Boot配置Redis过期时间的方案。文章还探讨了缓存穿透、雪崩等问题解决方案,以及基于Redis的分布式锁实现,帮助开发者全面掌握Redis在Java应用中的实践要点。
201 6
|
4月前
|
安全 Java API
2025 年 Java 校招面试常见问题及详细答案汇总
本资料涵盖Java校招常见面试题,包括Java基础、并发编程、JVM、Spring框架、分布式与微服务等核心知识点,并提供详细解析与实操代码,助力2025校招备战。
200 1

热门文章

最新文章