泛型
什么是泛型
泛型提出的背景:集合类容器在设计阶段/声明阶段不能确定该容器实际存放什么类型的对象。但对该对象的操作管理是确定的,因此此时把元素类型设计成一个参数,这个类型参数叫做泛型。由此可知,泛型最早是为集合设立。
泛型概念:允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型。这个参数将在使用时(例如,继承或实现接口,用这个类声明变量、创建对象)确定(即传入实际的类型参数,也成为实型参数)。
由此可见,泛型的使用也就分为三类:对接口、对类、对函数。
为什么使用泛型
如果不使用泛型,当使用集合时可能会出现类型不安全的问题,使得在进行类型转换的时候出现ClassCastException
的异常,实例见如下代码(以ArrayList
为例,其他容器类型同理):
ArrayList list = new ArrayList(); //不使用泛型
list.add(79);
list.add(79);
list.add(729);
list.add("hello"); // 出现类型不安全问题
但如果在声明的时候增加了泛型就能避免上述问题(注意泛型的类型不能是基本数据类型)
ArrayList<Integer> list = new ArrayList<Integer>();
一旦在声明的时候使用了泛型,在集合类或接口中凡是定义类或接口时,内部结构使(比如:方法、构造器、属性)用到泛型的位置都会限定泛型类型为Integer
,由此规范性和安全性也就得到了保障。
怎么使用泛型
使用已定义的泛型
如ArrayList
集合,Map
集合,直接在实例化集合对象时传递参数即可。
自定义泛型
泛型类(接口)
定义泛型类时,相当于给该类添加了一个参数,要求外界创建该类对象时给定该参数一个类型,以供该类在实例化时能准确定义成员变量与成员方法
- 定义一个普通的带泛型的类
public class Order<T> {
String name;
int orderId;
// 类的内部就可以使用类的泛型
T orderT;
public Order() { // 空参构造器不需要加<>,但实例化的时候需要
}
public Order(String name, int orderId, T orderT) {
this.name = name;
this.orderId = orderId;
this.orderT = orderT;
}
public T getName(T orderT){
return orderT;
}
public static void main(String[] args) {
Order order = new Order(); // 如果没指明类的泛型,则认为泛型就是Object
Order<Integer> integerOrder = new Order<>();
}
}
换句话说,这里的泛型就相当于一个全局约束规范,凡是该类中涉及泛型的内容,都需要遵守该规范(使用泛型类型)
- 带继承的泛型类
此时情况相对复杂一些,当子类后面没有<>时,例如Son1
和Son2
,其就一定不是泛型类,只有子类后面也有<>时才为泛型类入Son3
和Son4
。
class Father<T1,T2>{
}
//子类不保留父类的泛型
// 1)没有类型——擦出类型(转换为Object)
class Son1 extends Father{ //等价于class Son extends Father<Object,Object>{}
}
// 2)有具体类型
class Son2 extends Father{ //等价于class Son extends Father<Object,Object>{}
}
//子类保留父类泛型
// 1)全部保留
class Son3<T1,T2> extends Father<T1,T2>{
}
// 2)部分保留
class Son4<T2> extends Father<Integer,T2>{
}
⭐️特别提示:类中带泛型的方法不能够是静态方法,这是因为在调用该方法时并没有实例化对象,不知道泛型类型。
泛型方法
泛型方法:在方法中出现了泛型结构,泛型参数与类的泛型参数没有任何关系。
首先需要明确一点,泛型方法和带泛型的方法不是一个概念。后者的泛型对象由类定义时确定,而前者的泛型是自己的的泛型。也就是说,即使在实例化对象时给定了泛型类型,带泛型的方法可以得到该泛型类型,而泛型方法仍然不能,除非给其再次指定泛型类型。
以上述Order类为例,public T getName()
是带泛型的方法,但不是泛型方法,但如果再在Order
类中添加如下方法
public class Order{
// ...
public <E> List<E> copyFromArrayToList(E[] arr){
// 如果再List<E> 前不加<E>,编译器会认为存在一个 E 类,加了后编译器就知道该方法是泛型方法了
ArrayList<E> list = new ArrayList<>();
for(E e:arr){
list.add(e)
}
return list;
}
// ...
public static void main(String[] args) {
Order<Integer> integerOrder = new Order<>();
Integer[] arr = new Integer[]{1,2,3,4};
List<Integer> list = order.copyFromArrayToList(arr)
//这时上面的泛型方法就会根据传入的数组类型,将其泛型设置为Integer
}
}
⭐️ 特别提示:泛型方法可以使用静态方法,因为泛型方法没有使用类的泛型,不需要类实例化提供泛型参数。
⚠️写在最后(一些注意事项)
-
每一个泛型类对象都是一个单独个体,多个不同泛型参数类型的对象互为并列结构。即使同一个泛型类互为父子的类型,两者也不不能完成父子关系。换句话说,虽然 类A 是 类B 的父类,但是G<A>和G<B> 而这不具备子父类关系,二者是并列关系。见如下代码
List<Object> list1 = null; List<String> list2 = new ArrayList<String>(); list1 = list2; //错误
反之,A<G>是B<G>的父类
List<String> list1 = null; ArrayList<String> list2 = null; list1 = list2; //正确
通配符的使用
普通通配符
首先考虑这样一个需求:我们希望构造一个方法,能够对所有的List
集合实现赋值,不管List
集合中存放的是什么内容。这是就可以考虑使用通配符?
,请看如下代码:
public class Order{
//. Order类的其他代码
public static void main(String[] args) throws Exception {
List list1 = new ArrayList();
list1.add(1221);
list1.add("tome");
Order.print(list1);//这个时候就可以输出输出 1221和tome
List<Integer> list2 = new ArrayList();
list2.add(12);
List<String> list3 = new ArrayList();
list3.add("tom");
Order.print(list2);// 12
Order.print(list3);//tom
}
public static void print(List<?> list){
Iterator<?> iterator = list.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
可以发现,使用通配符就相当于给G<A>和G<B>类构造了一个公共父类G<?>,定义类G<?>可以使用的位置G<A>和G<B>都可以使用。同时之前会出现类型转换异常的方法在这里也得以消除。当然这也就有违背泛型使用的初衷,会造成类型不安全问题。
有限制条件的通配符
这里主要考虑的是如下两种通配符的使用
? extends 类A
:G<? extends A>
可以作为G<A>
和G<B>
的父类,其中B是A的子类? super 类A
:G<? super A>
可以作为G<A>
和G<B>
的父类,其中B是A的父类
上述内容换句话说就是如下两种情况:
-
当容器的泛型是
? extends 类A
,该类可以通配使用所有A的子类(包括A自身),通配范围(-∞,A]。 -
当泛型是
? super 类A
,该类可以通配使用所有A的父类(包括A自身),通配范围[A,+∞)。说明:可以预见,泛型是
?
时,通配范围是(-∞,+∞)
请看如下代码:
public void test(){
List<? extends Person> list1 = null;
List<? super Person> list1 = null;
List<Student> list3 = null;
List<Student> list4 = null;
List<Student> list5 = null;
list1 = list3; //编译通过
list1 = list4; //编译通过
list1 = list5; //编译不通过
list2 = list3; //编译不通过
list2 = list4; //编译通过
list2 = list5; //编译通过
}