在Java的集合中我们运行程序时会出现下方警告代码
$ javac CollectionDemo.java -encoding utf8
注: CollectionDemo.java使用了未经检查或不安全的操作。
注: 有关详细信息, 请使用 -Xlint:unchecked 重新编译。
这是因为集合中可能会被存放类型不同的元素,这在编译时不会出错,但运行时可能会出现错误!可以在代码中使用注解@SuppressWarnings("unchecked")
压制警告,但这终究不是一个好的方法,Java中可以使用泛型来解决这一问题!
在引入泛型之前,泛型程序设计都是用继承来实现的,必须要强制类型转换,并且可能会引发一些编译时发现不了的潜在问题。泛型可以很好的解决这些问题。
泛型类
首先看一下泛型类如何定义与使用:
代码示例
class Pair<T, U> {
private T first;
private U second;
Pair() {
first = null;
second = null;
}
Pair(T first, U second) {
this.first = first;
this.second = second;
}
public void setFirst(T newValue) {
first = newValue;
}
public void setSecond(U newValue) {
second = newValue;
}
public T getFirst() {
return first;
}
public U getSecond() {
return second;
}
public static <T> T getSo(T some) {
return (T)some.toString();
}
}
public class NewGer {
public static void main(String[] args) {
Pair<String, Integer> pair = new Pair<String, Integer>("java", 123);
Pair<Integer, Integer> pair1 = new Pair<Integer, Integer>(123, 456);
System.out.println(pair.getFirst());
System.out.println(pair.getSecond());
System.out.println(pair1.getClass() == pair.getClass());
}
}
输出结果:
java
123
true
代码说明
以下数字表示满足要求的代码所在行
- 泛型可以有多个类型变量以满足不同需求 1
- 类型变量可以指定方法的返回类型及域和局部变量的类型 2、3、19、22
- 使用具体的类型替换类型变量可以实例化泛型类型 32、33
- 范型的类型变量只能是类或接口,不能是简单类型(比如int、double等) 32、33
- Java中无论泛型的类型参数传入哪一种类型实参,其运行的都是同一块内存空间中的同一个类 32、33->36
另外还有一些应该注意的问题:
- 静态方法、静态初始化块、静态变量的声明与初始化都不允许使用泛型类型形参
- Java中不存在真正的泛型类,因此instanceof运算符对泛型类不可用
- 即使类型参数是子父类(子父接口)的关系,其泛型也不是子父类(子父接口)之间的关系
import java.util.*;
public class NewClass<T> {
// 1
static T info;
T age;
void foo(T msg) {}
public static void bar(T msg) {}
// 2
Collection<String> cs = new ArrayList<>();
if (cs instanceof List<String>) {}
// 3
public void test(List<Object> c) {}
public static void main(String[] args) {
List<String> strList = new ArrayList<>();
test(strList);
}
}
泛型方法
普通泛型方法
class ArrayAlg {
public static <T> T getMid(T... a) {
return a[a.length / 2];
}
}
public class ArrAnalog {
public static void main(String[] args) {
String mid = ArrayAlg.<String>getMid("hello", "java", "test");
//等价于String mid = ArrayAlg.getMid("hello", "java", "test");
System.out.println(mid);
}
}
说明
- 泛型方法可以放在泛型类中,也可以放在普通类中
- 泛型方法的类型变量要放在修饰符后面
- 编译器可以根据实参推断出泛型方法类型,
<String>
可以省略。因此第9行代码等价于第10行代码 - 实参元素类型必须属于同一类,否则运行时会报错
- T…代表着可变长度的类型为T的数组
- 如果不需要返回值,请将第2行代码写成
public static <T> void getMid(T... a) {
泛型构造器
class Test {
public Test() {
}
public <T> Test(T t) {
System.out.println(t);
}
}
public class NewGericDemo {
public static void main(String[] args) {
new <String>Test("hello");
new Test(123);
//new <Integer>Test("hello");
}
}
代码说明:
- 泛型构造器的类型实参必须与构造器的类型变量相同,否则会引发异常
类型变量
类型变量习惯上使用大写字母且较短:
- E:表示集合元素类型
- K和V:表示关键字与值的类型
- T(或者U、S):表示任意类型
- ?:表示通配符,可以是任意类型的元素
public static <T extends Comparable & Serializable> T func(List<? extends T> list)
- 无论类型参数是接口还是类,限定时都使用
extends
关键字,因为这样更加贴近子类的概念 - 一个类型变量或通配符可以同时限定多个接口或者类,使用
&
连接 - 通配符的限定T称为是类型通配符的上限
设定类型通配符的上限
如果将列表src的元素全部传给dest列表,那么src的元素类型必须是dest元素类型的子类才能满足要求,这就要用到了元素类型的上限限定了。
代码示例
import java.util.*;
public class NewGericDemo {
public static <T> void copy(List<T> dest, List<? extends T> src) {
//列表src的元素属性必须是dest元素属性的子类,或子接口
for (int i = 0; i < src.size() ; i++ ) {
dest.add(src.get(i));
}
}
public static void main(String[] args) {
List<Number> list1 = new ArrayList<>();
List<Double> list2 = new ArrayList<>();
list2.add(1.5);
list2.add(2.2);
list2.add(3.3);
copy(list1, list2);
System.out.println(list1);
}
}
代码说明:
列表src的元素属性必须是dest元素属性的子类,或子接口;dest中初始时不能存在与src元素类型不同的元素。
设定类型通配符的下限
上方代码还可以写成这样:
import java.util.*;
public class NewGericDemo {
public static <T> void copy(List<? super T> dest, List<T> src) {
for (int i = 0; i < src.size() ; i++ ) {
dest.add(src.get(i));
}
}
public static void main(String[] args) {
List<Number> list1 = new ArrayList<>();
List<Double> list2 = new ArrayList<>();
list2.add(1.5);
list2.add(2.2);
list2.add(3.3);
copy(list1, list2);
System.out.println(list1);
}
}
两者都能实现将src元素复制到dest中,此处使用的是设置类型通配符的下限,dest元素类型是src元素类型的父类或父接口,这和设置类型通配符的上限意思相同!
泛型代码
类型擦除与转换
因为JVM中没有泛型类型对象,因此泛型代码只会存活在编译时期,.class文件中只有相应的原始类型。原始类型就是删除类型参数后的泛型类型名。比如上方的Pair类去除泛型后成为下方代码
class Pair {
private Object first;
private Object second;
public Pair(Object first, Object second) {
this.first = first;
this.second = second;
}
public Object getFirst() {
return first;
}
public Object getSecond() {
return second;
}
public void setFirst(Object newValue) {
first = newValue;
}
public void setSecond(Object newValue) {
second = newValue;
}
}
说明
泛型转换成原始类型时使用第一个限定的类型变量来替换泛型的类型变量,如果没有显式的限定就用Object
替换。
代码示例
public class Interval<T extends Comparable & Serializable> implements Serializable{
private T lower;
private T upper;
public Interval(T first,T second){
}
}
等价于
public class Interval implements Serializable{
private Comparable lower;
private Comparable upper;
public Interval(Comparable first,Comparable second){
}
}
另外,因为原始类型替换了泛型类型变量,所以应该使用原始类型来操作结果元素,而不是泛型的变量类型。
代码示例
class Num<T extends Number> {
private T num;
public Num() {
}
public Num(T num) {
this.num = num;
}
public void setNum(T num) {
this.num = num;
}
public T getNum() {
return this.num;
}
}
public class NewGericDemo1 {
public static void main(String[] args) {
Num<Integer> num = new Num<>(56);
Integer int1 = num.getNum();
Num num1 = new Num();
num1 = num;
System.out.println(int1);
Number int2 = num1.getNum();
// Integer int2 = num1.getNum();
System.out.println(int2);
}
}
上方代码中将泛型类转赋值给了普通类,调用元素时,应该使用Number。注释掉的元素将会导致异常:无法将Number类型转换成Integer类型。