java泛型看这一篇就够了
Java泛型
泛型的使用
定义泛型的接口和类
泛型就是允许在定义类、接口、方法时使用类型形参,这个类型形参(泛型)将在声明变量、创建对象、调用方法时动态地指定。
定义泛型的接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21//定义接口指定泛型形参,该形参名为E
public interface List<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);
}对于
List<E>
来说,如果E形参传入String类型实参,则产生了一个新的类型:List<String>
类型,可以把List<E>
中所有 E 的地方都换成String。虽然程序只定义了一个List<E>
接口,但是实际使用时可以产生无数多个List接口。**List<String>
绝对不会被替换成ListString
,系统没有进行源码复制,二进制代码中也没有,哪里都没有。**定义泛型的类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30public class Apple<T> {
//使用T类型定义实例变量
private T info;
public Apple() {
}
//使用T类型来定义构造器
public Apple(T info) {
this.info = info;
}
public T getInfo() {
return info;
}
public void setInfo(T info) {
this.info = 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<>(3.14);
System.out.println(a2.getInfo());
}
}上面程序在使用时就可以生成
Apple<String>,Apple<Double>
等多个形式的逻辑子类(物理上并不存在)泛型类生成子类的注意事项
首先,当我们继承这些泛型类时,父类不能包含泛型形参。也就是说下面的书写方式是错误的。
public class A extends Apple<T>{}
这是错误的写法,以下几种写法是正确的。1
2
3public class A extends Apple<String>{}
public class B extends Apple{}
public class c<T> extends Apple<T>{}当使用第一种的时候,父类中所有带有 T 的方法都会变成 String,当子类需要重写父类的方法时候要尤为注意。
如果是第二种的话,父类中带T的方法都会变成Object。
并不存在泛型类
1
2
3
4
5//分别创建List<String>对象和List<Integer>对象
List<String> l1 = new ArrayList<>();
List<Integer> l2 = new ArrayList<>();
//调用getClass方法来比较两个类是否相等
System.out.println(l1.getClass()==l2.getClass());运行上面的泛型类,可以发现输出True。不管为泛型形参传入哪一个种类型实参,对于Java来说,他们都是同一个类,内存中也只占用一块内存空间,因此在静态方法、静态初始化块、静态变量(类相关)的声明和初始化中不允许使用泛型形参。
类型通配符
感受一下一种神奇的错误
1 |
|
java早期设计中,允许Integer[]数组赋值给Number[]变量存在缺陷,因此在设计泛型的时候进行了改进,不再允许List
1 |
|
你可能会希望这个参数能够传入所有类型的list,但是很遗憾。只有List
所以这就需要能表示各种泛型List的父类,类型通配符是一个问号(?),也就是说写成如下形式是正确的。
1 |
|
设定类型通配符的上限
当直接使用List<?>这种形式时,表示这个List集合可以是任何泛型List的父类。有时候我们不希望它是所有泛型的父类,因为好像没啥用。有时候我们想限定它是一种类的子类,这样我们就可以用父类的方法了。
1 |
|
这个程序重点关注List<? extends Shape> shapes
注意一定要这么写,这个参数才能接受Shape的两个实现类作为其参数。假如你写成List<Shape> shapes
则只能接受Shape这个抽象类的实例对象。不过? extends Shape
,它表示Shape未知的子类,程序无法确定这个类型是什么,所以无法将任何对象添加到这种集合中。
总之这种指定通配符上限的集合,只能从集合中取元素(取出的元素总是上线的类型或其子类),不能向集合中添加元素,而且我们还可以为泛型形参设置上限
1 |
|
对于更广泛的泛型类来说,指定通配符上限就是为了支持类型型变。 比如 Foo 是 Bar 的子类,这样 A< Foo> 就 相当于 A<? extends Bar> 的子类, 可以将 A< Foo> 赋值给 A<? extends Bar> 类型 的 变量,这种型变方式被称为协变。
对于协变的泛型而言,它只能调用泛型类型作为返回值类型的方法( 编译器会将该方法返回值当 成通配符上限的类型;而不能调用泛型类型作为参数的方法。 口诀 是:协变只出不进!
设定类型通配符的下限
与上限刚好相反,你限定这个参数是该类的父类或是与其相同。假设自己实现一个工具方法:实现将src集合中的元素赋值到dest集合的功能,因为dest集合可以保存src集合中的所有元素,所以dest集合可以保存src集合中的所有元素,所以dest集合元素的类型应该是src集合元素类型的父类。
1 |
|
Java 也允许指定通配符的下限,通配符的下限用<? super 类型> 的方式来指定,通配符下限的作用与通配符上限的作用恰好相反。 指定通配符的下限就是为了支持类型 型变。 比如 Foo 是 Bar 的子类, 当程序需要一个 A<? super Foo> 变量时,程序可以将 A< Bar>、 A< Object> 赋值给 A<? super Foo> 类型的变量,这种型变方式被称为逆变。
泛型方法
泛型方法的定义
泛型方法的定义体与之前的差不多,大多数时候都可以用泛型方法来代替类型通配符。
1 |
|
不过泛型方法允许泛型形参被用来表示方法的一个或多个参数之间的类型依赖关系,就像如上的方法中,限定了两个参数之间的关系,如果我们定义的方法没有这样类型的依赖关系,就不应该用泛型方法。
类型通配符和泛型方法显著的区别:类型通配符既可以在方法签名中定义形参的类型,也可以用来定义变量的类型;但是泛型方法中的泛型形参必须在对应的方法中显式声明。
菱形语法与泛型构造器
Java 也允许在构造器签名中声明泛型形参,这样就产生了所谓的泛型构造器。一旦定义了泛型构造器, 接下来 在调用构造器时, 就不仅可以让 Java 根据数据参数的类型来“ 推断” 泛型形参的类型,而且程序员也可以显式地为构造器中的泛型形参指定实际的类型。
1 |
|
泛型方法重载
1 |
|
不要写出令人迷惑的代码:上述代码列表存在一定的区别,但是这区别不是很明确。假如两个参数类型都是Collection
1 |
|
擦除和转换
擦除:把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有<>之间的类型信息都会丢失。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34class BoBo<T extends Number>{
T size;
public BoBo() {}
public BoBo(T size) {
this.size = size;
}
public T getSize() {
return size;
}
public void setSize(T size) {
this.size = size;
}
}
public class ErasureTest {
public static void main(String[] args) {
BoBo<Integer> a = new BoBo<>();
//a的getSize()方法返回Integer
Integer as = a.getSize();
//把 a对象赋值给其他变量会丢失尖括号里的信息
BoBo b = a;
//b只知道size的类型是Number
Number size = b.getSize();
//下面代码将引起错误
Integer size1 = b.getSize();
}
}转换:你可以直接把一个List对象赋值给一个
List<String>
。编译器会提示你未经检查的转换,有可能会引发运行时异常ClassCastException1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import java.util.ArrayList;
import java.util.List;
public class ErasureTest2 {
public static void main(String[] args) {
List<Integer> li = new ArrayList<>();
li.add(6);
li.add(9);
List list = li;
//下面的代码引起“未经检查的转换警告”
List<String> ls = list;
//但只要访问ls里的元素,下面代码将引起运行时异常ClassCastException
System.out.println(ls.get(0));
}
}
泛型与数组
Java泛型有一个很重要的设计原则—如果一段代码在编译时没有提出未经严查的转换警告,则程度在运行时不会引发ClassCastException
正是因为这个原因:所以数组元素的类型不能包含泛型变量或泛型形参。假如是允许的话,那么下面的代码一定会引发ClassCastException
1 |
|
如果改成以下形式:
1 |
|
这样的话List<String>[] lsa = new ArrayList[10];
会有编译警告未经检查的转换,即编译器并不保证这段代码是类型安全的。但正因为编译器给出了警告,所以完全可能出现这种异常,与此类似如下代码也是不允许的。
1 |
|
由于类型变量在运行时并不存在,而编译器无法确定实际类型是什么,因此编译器会报错。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!