超详细Java泛型解析,由浅入深带你认识和使用泛型
一、前言
泛型是JDK5中引入的新特性。在 Java 编程中,泛型(Generics)是一个非常强大的特性,它能够让代码更加通用、安全和灵活。
泛型的主要作用是在类、接口和方法中使用参数化类型,从而使代码可以处理不同类型的数据,而不需要重复编写相同的逻辑。
二、泛型引入
为了更好地理解泛型,我们先抛出一个问题:没有泛型的时候,我们的类、接口和方法是怎样存储数据的呢?我们以常用的集合为例。
2.1 没有泛型的时候,集合是如何储存数据的呢?
- 如果我们没有给集合指定类型,默认所有数据类型都是Object类型,此时可以往集合添加任意的数据类型。(实际上这是一种多态)
2.2 这样就会带来一个坏处
- 我们在获取数据的时候无法使用它的特有行为。例如下图,我们没有定义集合的泛型,往集合里面传递一个字符串对象,我们想遍历元素,再用字符串特有的length方法计算字符串长度,但是发生报错。
- 原因是:我们遍历出来的对象实际上是一个Object对象,使用到了多态,在调用方法时编译看左边,运行看右边。但是在编译的时候没有在Object类中发现有length方法,所以无法调用。
- (这也就是多态的弊端:不能访问子类特有的功能,只能访问其重写的父类功能)
2.3 那么我们进行类型转换不就行了吗?
我们可以看到,经过强制类型转换后确实可以调用子类中的特有功能。
但是,这个问题远远没有解决,如果我们添加的是不同类型的元素呢?
运行之后发现报了类型转换异常的错误,因为第二次遍历得到的是一个整数类型的数据,第三次遍历得到的是自定义类型的数据,无法强制转换为String类型。
由此我们发现,当存入不同类型数据的时候,我们的问题仍然得不到解决,不同类型的数据仍然无法一一调用其特有的功能。
2.4 那么怎样能够在添加数据的时候让类型统一?
这个时候我们就要站在java源代码开发者的角度去思考:
用户使用这个对象时候都会想要传入什么类型的数据?我应该怎样满足?
- 在源码中定义一个StringArrayList、IntegerArraylist类...,在其add方法指定添加的类型?
那肯定不行呀,这也太麻烦了!每一种类型都要写一种集合,这代码量也太大了,开发者的学习成本也会大大增加!
这个时候,我们就要使用到泛型了。
三、泛型详解
3.1 泛型的好处?
- 统一了数据类型
- 把运行时期的问题提前到了编译时期
- 理解:如运行时对象想调用特有的方法无法调用问题,当我们使用了泛型后,在类和方法中指定了传入的数据类型,这个问题在编译时期就已经解决了。
- 避免了类型的强制转换
我们再回到上面的问题,我们给ArrayList集合定义一个泛型后,传入String类型,即可统一集合的数据。而且,如果想要创建一个存储Integer类型元素的集合,也可以通过该传入相应的泛型类型去创建。
3.2 泛型的定义格式
-
<类型>: 指定一种类型的格式.尖括号里面可以任意书写,一般只写一个字母.例如: <E> <T>
-
<类型1,类型2…>: 指定多种类型的格式,多种类型之间用逗号隔开.例如: <E,T> <K,V>
3.3 泛型的创建与使用
3.3.1 泛型类
使用场景:
当一个类中,某个变量的数据类型不确定时,就可以定义带有泛型的类
我们举个例子:
javabea类:
测试类:
在这个例子中我们可以发现,在类定义的泛型,类内的变量都可以用其来指定变量的类型
在这里有一个细节需要注意:
当我们在类的类名后定义一个泛型,其也可以被接口的泛型类型接收
3.3.2 泛型方法
使用场景:
当一个方法中,某个变量的数据类型不确定时,就可以定义带有泛型的方法
注意:泛型类型<类型>要写在方法修饰符的后面
举个例子:
拓展:如果我们需要传入多个值怎么办(3个以上),一个一个定义岂不是很麻烦,所以我们使用可变参数来完成
3.3.3 泛型接口
使用场景:
当一个接口中,某个变量的数据类型不确定时,就可以定义带有泛型的接口
那我们如何使用一个带有泛型的接口呢?
方法一:实现类给出具体类型
方法二:实现类延续泛型,创建对象时再确定
方法二示例图
3.3.4 泛型定义的一些细节
- 泛型中不能写基本数据类型,如果需要用到如int等基本数据类型,使用其包装类Integer等
- 指定泛型具体类型后,传递数据时,可以传入该类类型或者其子类类型
- 通过前面的Object类型集合的问题剖析也可以很好理解(字符串、整数、Student类都是其子类类型)
- 如果不写泛型,类型默认是Object
这里我们有一个值得注意的点是:泛型不具备继承性,但是数据具备继承性
这句话应该怎么理解呢?
在下图中我们发现我们定义了一个method方法,传入一个泛型类型为Ye的集合参数,当我们调用method方法,传入Ye的子类Fu和Zi的时候产生报错!这说明泛型不具备继承性
但是我们为同一个集合传入数据时,是可以传入其子类数据的,前面的Object的讲述大家已经很熟悉。这说明数据具备继承性
拓展:Java中的泛型实际上是一种伪泛型
3.4 泛型通配符
3.4.1 引入
在下图方法中,根据上述的学习可知:泛型里写什么类型,那么它就只能传递什么类型的数据
比如我想传递Ye类型的数据则泛型写的是Ye
但是这又引发了一个问题:此时它可以接收任意类型的数据,如果要传递一个Student类型也是可以的。
但是如果我虽然不确定类型,而且上述说了,泛型不具备继承性,定义了ArrayList<Ye>就只能传入Ye类型的泛型值。
但是希望传递Ye继承体系的Ye、Fu、Zi类型该怎么处理呢?有没有一种折中的可能?
这里我们就需要引入泛型通配符了。
3.4.2 通配符
? extends E:表示可以传递E或者E所有的子类类型
?super E:表示可以传递E或者E所有的父类类型
应用场景
- 如果我们在定义类、方法、接口的时候,如果类型不确定,就可以定义泛型类、泛型方法、泛型接口。
- 如果类型不确定,但是能知道以后只能传递某个继承体系中的,就可以泛型的通配符
泛型的通配符关键点:可以限定类型的范围。
四、泛型的局限性
尽管泛型很强大,但它们也有一些限制:
-
类型擦除:Java 泛型在编译后会被类型擦除,这意味着在运行时无法获取泛型的具体类型信息。比如
List<String>
和List<Integer>
在运行时实际上是相同的类型。 -
不能使用基本类型:泛型只能使用引用类型,不能直接使用基本数据类型(如
int
、char
等)。不过,你可以使用它们的包装类(如Integer
、Character
)来解决这个问题。 -
静态上下文中不能使用泛型类型参数:因为泛型在静态上下文中不可用,不能在静态方法或静态字段中使用泛型类型参数。
class MyClass<T> { // 成员方法可以使用泛型 public void instanceMethod(T param) { System.out.println(param); } // 静态方法无法使用泛型T public static void staticMethod(T param) { // 错误:静态上下文不能引用实例的泛型类型 System.out.println(param); } }
五、总结
泛型是 Java 中非常有用的特性,它能够让你的代码更加灵活和安全。通过泛型,程序员可以编写出更通用的类、接口和方法,减少代码重复,并且避免不必要的类型转换错误。在编写代码时,理解和正确使用泛型将极大地提升代码的质量和可维护性。
希望这篇博文能够让你对 Java 泛型有一个清晰的理解,帮助你在项目中更好地使用泛型!