Java Review(三十一、泛型)
@
为什么要使用泛型
Java 集合有个缺点一一把一个对象"丢进"集合里之后,集合就会"忘记"这个对象的数据类型 ,当再次取出该对象时 , 该对象的编译类型就变成 了 Object 类型(其运行时类型没变) 。
Java 集合之所以被设计成这样,是因为集合 的 设计者不知道我们会用集合来保存什么类型的对象 ,所以他们把集合设计成能保存任何类型 的对象,只要求具有很好的通用性 。 但这样做带来如下两个问题 :
- 集合对元素类型没有任何限制,这样可能引发一些问题 。 例如,想创建一个只能保存 Dog 对象的集合,但程序也可以轻易地将 Cat 对象"丢"进去,所以可能引发异常 。
- 由于把对象"丢进"集合时 , 集合丢失了对象的状态信息,集合只知道它盛装的是 Object,因此取出集合元素后通常还需要进行强制类型转换 。 这种强制类型转换既增加了编程的复杂度 ,也可能引发ClassCastException异常。
使用泛型
从 Java 5 以后, Java 引入了"参数化类型 Cparameterized type )" 的概念 ,允许程序在创建集合时指定集合元素的类型 ,如List
public class GenericList
{
public static void main(String[] args)
{
// 创建一个只想保存字符串的List集合
List<String> strList = new ArrayList<String>(); // ①
strList.add("疯狂Java讲义");
strList.add("疯狂Android讲义");
// 下面代码将引起编译错误
// strList.add(5); // ②
strList.forEach(str -> System.out.println(str.length())); // ③
}
}
深入泛型
泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参(或叫泛型)将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参 ) 。
定义泛型接口、类
下面是 Java 5 改写后 List 接口、 Iterator 接口、 Map 的代码片段 。
// 定义接口时指定了 一个泛型形参,该形参名为 E
public interface List<E>{
// 在该接口里. E 可作为类型使用
// 下面方法可以使用 E 作为参数类型
void add (E x);
Iterator<E> iterator(); //①
// 定义接口时指定了一个泛型形参 ,该形参名为 E
}
public interface Iterator<E>{
//在该接口里 E 完全可以作为类型使用
E next() ;
boolean hasNext() ;
// 定义该接口时指定了两个泛型形参,其形参名为 K 、 v
}
public interface Map<K , V>{
// 在该接口里 K 、 V 完全可以作为类型使用
Set<K> keySet() //②
V put(K key, V value)
}
尖括号中的内容一一就是泛型的实质:允许在定义接口、类时声明泛型形参,泛型形参在整个接口、类体内可当成类型使用,几乎所有可使用普通类型的地方都可以使用这种泛型形参 。
可以为任何类、接口增加泛型声明(并不是只有集合类才可以使用泛型声明 ,虽然集合类是泛型的重要使用场所) 。 下面自定义一个 Apple 类,这个 Apple 类就可以包含一个泛型声明 。
// 定义Apple类时使用了泛型声明
public class Apple<T>
{
// 使用T类型定义实例变量
private T info;
public Apple(){}
// 下面方法中使用T类型来定义构造器
public Apple(T info)
{
this.info = info;
}
public void setInfo(T info)
{
this.info = info;
}
public T getInfo()
{
return this.info;
}
public static void main(String[] args)
{
// 由于传给T形参的是String,所以构造器参数只能是String
Apple<String> a1 = new Apple<>("苹果");
System.out.println(a1.getInfo());
// 由于传给T形参的是Double,所以构造器参数只能是Double或double
Apple<Double> a2 = new Apple<>(5.67);
System.out.println(a2.getInfo());
}
}
类型通配符
当使用一个泛型类时 (包括声明变量和创建对象两种情况) , 都应该为这个泛型类传入一个类型实参。如果没有传入类型实际参数 , 编译器就会提出泛型警告 。 假设现在需要定义一个方法 , 该方法里有一个集合形参,集合形参的元素类型是不确定的, 那应该怎样定义呢?
使用类型通配符
为了表示各种泛型 List 的父类,可以使用类型通配符,类型通配符是一个问号 ( ?) ,将一个问号作为类型实参传给 List 集合 , 写作: List<?> (意思是元素类型未知的 List ) 。 这个问号(?)被称为通配符,它的元素类型可以匹配任何类型 。
public void test(List<?> c){
for (int i = 0 ; i < c . size() ; i++ ){
System . out .println (c.get(i) );
}
}
在使用任何类型的 List 来调用它,程序依然可以访问集合 c 中的元素,其类型是 Object,这永远是安全的,因为不管 List 的真实类型是什么,它包含的都是Object 。
但这种带通配符的 List 仅表示它是各种泛型 List 的父类,并不能把元素加入到其中 。 例如,如下代码将会引起编译错误 :
List<?> c = new ArrayLi st<String> ();
//下丽程序引起编译错误
c . add(new Object()) ;
因为程序无法确定 c 集合中元素的类型,所以不能向其中添加对象 。 根据前面的 List
另 一方面 , 程序可以调用 get()方法来返回 List<?>集合指定索引处的元素,其返回值是一个未知类型,但可以肯定的是,它总是一个 Object 。 因此,把 get()的返回值赋值给一个 Object 类型的变量,或者放在任何希望是 Object 类型的地方都可以 。
设定类型通配符的上限
当直接使用 List这种形式时,即表明这个 List 集合可以是任何泛型 List 的父类 。 但还有一种特殊的情形,程序不希望这个 List是任何泛型 List 的父类,只希望它代表某一类泛型 List 的父类。
一个简单的绘图程序,下面先定义三个形状类 :
// 定义一个抽象类Shape
public abstract class Shape
{
public abstract void draw(Canvas c);
}
// 定义Shape的子类Circle
public class Circle extends Shape
{
// 实现画图方法,以打印字符串来模拟画图方法实现
public void draw(Canvas c)
{
System.out.println("在画布" + c + "上画一个圆");
}
}
// 定义Shape的子类Rectangle
public class Rectangle extends Shape
{
// 实现画图方法,以打印字符串来模拟画图方法实现
public void draw(Canvas c)
{
System.out.println("把一个矩形画在画布" + c + "上");
}
}
上面定义了 三个形状类,其中 Shape 是一个抽象父类 , 该抽象父类有两个子类 : Circle 和 Rectangle 。接下来定义一个 Canvas 类 , 该画布类可以画数量不等的形状 (Shape 子类的对象) 。
public class Canvas
{
// // 同时在画布上绘制多个形状
// public void drawAll(List<Shape> shapes)
// {
// for (Shape s : shapes)
// {
// s.draw(this);
// }
// }
// 同时在画布上绘制多个形状,使用被限制的泛型通配符
public void drawAll(List<? extends Shape> shapes)
{
for (Shape s : shapes)
{
s.draw(this);
}
}
public static void main(String[] args)
{
List<Circle> circleList = new ArrayList<Circle>();
Canvas c = new Canvas();
// 由于List<Circle>并不是List<Shape>的子类型,
// 所以下面代码引发编译错误
c.drawAll(circleList);
}
}
程序中使用了被限制的泛型通配符。
被限制的泛型通配符表示如下 :
//它表示泛型形参必须是 Shape 子类的 List
List<? extends Shape>
List<? extends Shape>是受限制通配符的例子,此处的问号 (?) 代表一个未知的类型,就像前面看到的通配符一样 。 但是此处的这个未知类型一定是 Shape 的子类型(也可以是 Shape 本身),因此可以把 Shape 称为这个通配符的上限 (upper bound) 。
类似地,由于程序无法确定这个受限制的通配符的具体类型,所以不能把 Shape 对象或其子类的对象加入这个泛型集合中 。 例如,下面代码就是错误的:
public void addRectangle(List<? extends Shape> shapes){
//下面代码引起编译错误
shapes .add(O , new Rectangle());
}
简而言之,这种指定通配符上限的集合,只能从集合中取元素(取出的元素总是上限的类型) ,不能向集合中添加元素(因为编译器没法确定集合元素实际是哪种子类型) 。
设定类型通配符的下限
除可以指定通配符的上限之外, Java 也允许指定通配符的下限,通配符的下限用<? super 类型>的方式来指定,通配符下限的作用与通配符上限的作用恰好相反 。
指定通配符的下限就是为了支持类型型变 。 比如 Foo 是 Bar 的子类,当程序需要一个 A<? super Bar>变量时,程序可以将 A<Foo> 、 A<Object>赋值给 A<? super Bar>类型的变量,这种型变方式被称为逆变。
对于逆变的泛型集合来说,编译器只知道集合元素是下限的父类型,但具体是哪种父类型则不确定。
因此,这种逆变的泛型集合能向其中添加元素(因为实际赋值的集合元素总是逆变声明的父类) ,从集合中取元素时只能被当成 Object 类型处理(编译器无法确定取出的到底是哪个父类的对象)。
假设实现一个工具方法:实现将 src 集合中的元素复制到 dest 集合的功能,因为 dest 集合可以保存 src 集合中的所有元素,所以 dest 集合元素的类型应该是 src 集合元素类型的父类。
对于上面的 copy()方法,可以这样理解两个集合参数之间的依赖关系;不管 src 集合元素的类型是什么,只要 dest 集合元素的类型与前者相同或者是前者的父类即可,此时通配符的下限就有了用武之地 。
下面程序采用通配符下限的方式来实现该 copy()方法 :
public class MyUtils
{
// 下面dest集合元素类型必须与src集合元素类型相同,或是其父类
public static <T> T copy(List<? super T> dest
, List<T> src)
{
T last = null;
for (T ele : src)
{
last = ele;
// 逆变的泛型集合添加元素是安全的
dest.add(ele);
}
return last;
}
public static void main(String[] args)
{
List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
li.add(5);
// 此处可准确的知道最后一个被复制的元素是Integer类型
// 与src集合元素的类型相同
Integer last = copy(ln , li); // ①
System.out.println(ln);
}
}
使用这种语句,就可以保证程序的①处调用后推断出最后一个被复制的元素类型是 Integer,而不是笼统的 Number 类型 。
实际上, Java 集合框架中的 TreeSet
//下面的 E 是定义 TreeSet 类时的泛型形参
TreeSet(Comparator<? super E> c)
通过这种带下限的通配符的语法 ,可以在创建 TreeSet 对象时灵活地选择合适的 Comparator 。 假定需要创建一个 TreeSet<String>集合,并传入一个可以比较 String 大小 的 Comparator , 这个 Comparator既可以是 Comparator
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)