- 1 泛型类
- 2 泛型方法
- 3 通配符
- 3.1 使用类型通配符
- 3.2 限定类型通配符的上限
- 3.3 设定通配符下限
- 3.4 泛型方法和类型通配符的区别
- 3.5 泛型方法与方法重载
- 3.6 泛型方法与反射
- 4 Java 7的“菱形”语法与泛型构造器
- 5 Java 8改进的类型推断
1 泛型类
泛型程序设计意味着编写的代码可以被很多不同类型的对象所重用
- 泛型(Generic):Java的参数化类型,允许程序在创建集合时指定集合元素的类型,防止不加限制的情形下,引发
ClassCastException
异常 - 创建方法:在集合接口、类后增加尖括号
<>
,尖括号里放一个或多个数据类型,即表明这个集合接口、集合类只能保存特定类型的对象 - “菱形”语法:在构造器后不需要带完整的泛型信息,只要给出一对尖括号
<>
即可
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());
}
}
- 泛型的实质:允许在定义接口、类时声明类型形参,类型形参在整个接口、类体内可当成类型使用,几乎所有可使用普通类型的地方都可以使用这种类型形参
- 从泛型类派生子类:当使用这些接口、父类时不能再包含类型形参
- public class A extends Apple{ } 这是错误的使用方法
- public class A extends Apple{ }正确
- public class A extends Apple{ } 会出现警告:使用了未经检查或不安全的操作
- 注意:在JVM中并不存在泛型类,不管泛型的实际类型参数是什么,他们在运行时总有同样的类(
class
),同时在静态方法、静态初始化块或者是静态变量的声明和初始化中不允许使用类型形参,且instanceof
运算符后不能使用泛型类
2 泛型方法
- 泛型方法(Generic Method):在方法定义时定义一个或多个类型形参,语法格式如下:
修饰符 <T,S> 返回值类型 方法名(形参列表){
//方法体...
}
// 声明一个泛型方法,该泛型方法中带一个T类型形参,
static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
for (T o : a) {
c.add(o);
}
}
public static void main(String[] args) {
Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<>();
// 下面代码中T代表Object类型
fromArrayToCollection(oa, co);
String[] sa = new String[100];
Collection<String> cs = new ArrayList<>();
// 下面代码中T代表String类型
fromArrayToCollection(sa, cs);
// 下面代码中T代表Object类型
fromArrayToCollection(sa, co);
// 下面代码中T代表String类型,但na是一个Number数组,因为Number既不是String类型,也不是它的子类,所以出现编译错误
fromArrayToCollection(na, cs);
}
- 泛型方法可以定义在普通类中,也可以定义在泛型类中,方法中的泛型参数无须显式传入实际类型参数
3 通配符
3.1 使用类型通配符
- 类型通配符是一个问号(
?
),将一个问号作为类型实参传给List
集合,写作:List<?
>,这个问号被称为通配符,它的元素类型可以匹配任何类型,带通配符的List仅表示它是各种泛型类的父类,并不能把元素加入到其中,因为程序无法确定集合中元素的类型,所以不能添加,例外的是null
可以添加,另外,在使用get()
方法返回的元素是未知类型
public void test(List<?> c){
for(int i = 0; i < c.size(); i++){
System.out.println(c.get(i));
}
}
3.2 限定类型通配符的上限
- 有时,类或方法需要对类型变量加以约束,可以通过对类型变量T设置限定实现这一点:
public static <T extends Comparable> T min(T[]);
-
<T extends BoundingType>
表示T应该是绑定类型的子类型,T和绑定类型可以是类或接口,这里的extends
已经不是继承的意思,而是绑定的意思 - 一个类型变量或通配符可以有多个限定:
<T extends BoundingType1 & BoundingType2>
,限定类型用&
分隔,用逗号来分隔类型变量,限定可以有多个接口,但是最多只能有一个类,且有类的时候必须是限定列表的第一个 - 类型擦除就是将带有泛型的对象赋值给一个没有泛型的变量时,丢失类型信息;
3.3 设定通配符下限
- 设定通配符的下限:
<? Super Type>
表示必须是Type
本身或是Type
的父类,主要用于方法拷贝中
3.4 泛型方法和类型通配符的区别
- 大多数的时候都可以使用泛型方法来代替类型通配符;
- 也可以同时使用泛型方法和类型通配符;
- 使用通配符比使用泛型方法更加准确和清晰;
- 类型通配符可以在方法中声明签名中定义形参的类型,也可以用于定义变量的类型,但泛型方法中的类型形参必须在对应方法中显式声明
3.5 泛型方法与方法重载
- 由于可以设定通配符的上限和下限,可以重载,但是容易出现问题,不推荐使用
3.6 泛型方法与反射
- 通过在反射中使用泛型,可以避免使用反射生成的对象需要强制类型转换
- 使用反射来获取泛型信息
public class GenericTest {
private Map<String , Integer> score;
public static void main(String[] args) throws Exception {
Class<GenericTest> clazz = GenericTest.class;
Field f = clazz.getDeclaredField("score");
// 直接使用getType()取出的类型只对普通类型的成员变量有效
Class<?> a = f.getType();
// 下面将看到仅输出java.util.Map
System.out.println("score的类型是:" + a);
// 获得成员变量f的泛型类型
Type gType = f.getGenericType();
// 如果gType类型是ParameterizedType对象
if(gType instanceof ParameterizedType) {
// 强制类型转换
ParameterizedType pType = (ParameterizedType)gType;
// 获取原始类型
Type rType = pType.getRawType();
System.out.println("原始类型是:" + rType);
// 取得泛型类型的泛型参数
Type[] tArgs = pType.getActualTypeArguments();
System.out.println("泛型信息是:");
for (int i = 0; i < tArgs.length; i++) {
System.out.println("第" + i + "个泛型类型是:" + tArgs[i]);
}
}
else {
System.out.println("获取泛型类型出错!");
}
}
}
4 Java 7的“菱形”语法与泛型构造器
- 泛型构造器:
public <T> 类名(T t);
- 使用:
new 类名(值);
new <String> 类名(值);
- 注意:显式指定了泛型构造器中声明的类型形参的实际类型,则不可以使用“菱形”语法。
class Foo {
public <T> Foo(T t) {
System.out.println(t);
}
}
public class GenericConstructor {
public static void main(String[] args) {
// 泛型构造器中的T参数为String。
new Foo("疯狂Java讲义");
// 泛型构造器中的T参数为Integer。
new Foo(200);
// 显式指定泛型构造器中的T参数为String,
// 传给Foo构造器的实参也是String对象,完全正确。
new <String> Foo("疯狂Android讲义");
// 显式指定泛型构造器中的T参数为String,
// 但传给Foo构造器的实参是Double对象,下面代码出错
new <String> Foo(12.3);
}
}
class MyClass<E> {
public <T> MyClass(T t) {
System.out.println("t参数的值为:" + t);
}
}
public class GenericDiamondTest {
public static void main(String[] args) {
// MyClass类声明中的E形参是String类型。
// 泛型构造器中声明的T形参是Integer类型
MyClass<String> mc1 = new MyClass<>(5);
// 显式指定泛型构造器中声明的T形参是Integer类型,
MyClass<String> mc2 = new <Integer> MyClass<String>(5);
// MyClass类声明中的E形参是String类型。
// 如果显式指定泛型构造器中声明的T形参是Integer类型
// 此时就不能使用"菱形"语法,下面代码是错的。
// MyClass<String> mc3 = new <Integer> MyClass<>(5);
}
}
5 Java 8改进的类型推断
- 可通过调用方法的上下文来推断类型参数的目标类型;
- 可在方法调用链中,将推断得到的类型参数传递到最后一个方法;
- 不是万能的,少用
class MyUtil<E> {
public static <Z> MyUtil<Z> nil() {
return null;
}
public static <Z> MyUtil<Z> cons(Z head, MyUtil<Z> tail) {
return null;
}
E head() {
return null;
}
}
public class InferenceTest {
public static void main(String[] args) {
// 可以通过方法赋值的目标参数来推断类型参数为String
MyUtil<String> ls = MyUtil.nil();
// 无需使用下面语句在调用nil()方法时指定类型参数的类型
MyUtil<String> mu = MyUtil.<String>nil();
// 可调用cons方法所需的参数类型来推断类型参数为Integer
MyUtil.cons(42, MyUtil.nil());
// 无需使用下面语句在调用nil()方法时指定类型参数的类型
MyUtil.cons(42, MyUtil.<Integer>nil());
// 希望系统能推断出调用nil()方法类型参数为String类型,
// 但实际上Java 8依然推断不出来,所以下面代码报错
//String s = MyUtil.nil().head();
String s = MyUtil.<String>nil().head();
}
}