Java 泛型
JDK 5 增加泛型支持在很大程度上都是为了让集合能记住其元素的数据类型。
Java 5 以后,Java 引入了 “参数化类型 (parameterized type)” 的概念, Java 的参数化类型被称为泛型 ( Generic )
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 7 泛型的“菱形”语法
------------------------------------------------------------------------------------------------------------------
从 Java 7 开始, Java 允许在构造器后不需要带完整的泛型信息,只要给出一对尖括号 (<>) 即可, Java 可以推断尖括号里应该是什么泛型信息。
例如:
List<String> strList = new ArrayList<>();
Map<String, Integer> scores = new HashMap<>();
*
*
*
2 深入泛型
--------------------------------------------------------------------------------------------------------------------
所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可以称为类型实参)。
Java 5 改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参。例如: List<String>, ArrayList<String>
// 定义接口时指定了一个类型形参,该形参名为 E
public interface List<E>
{
// 在该接口里, E 可以作为类型使用
void add(E x);
Iterator<E> iterator();
....
}
泛型的实质:允许在定义接口、类时声明类型形参,类型形参在整个接口、类体内可当成类型使用,几乎所有可使用普通类型的地方都可以使用这种类型形参。
包含泛型声明的类型可以在定义变量、创建对象时传入一个类型实参,从而可以动态地生成无数多个逻辑子类,但这种子类在物理上并不存在。
可以为任何类、接口增加泛型声明,并不是只有集合类才可以使用泛型声明,虽然集合类是泛型的重要使用场所。
// 定义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());
}
}
当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明,例如,为 Apple<T>类定义构造器,其构造器名依然是Apple,
而不是 Apple<T>, 调用该构造器时可以使用 Apple<T> 的形式,当然应该为 T 形参传入实际的类型参数。 Java 7 提供了菱形语法,允许省略 <> 中的类型实参。
从泛型类派生子类
----------------------------------------------------------------------------------
当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从该父类派生子类,需要指出的是,当使用这些接口、父类时不能再包含类型形参
下面代码是错的:
// 定义类 A 继承 Apple 类, Apple 类不能跟类型形参
public class A extends Apple<T>{}
定义类、接口、方法时可以声明类型形参,使用类、接口、方法时应该为类型形参传入实际的类型。
如果想从 Apple 类派生一个子类,则可以改为如下代码:
// 使用 Apple 类时为 T 形参传入 String 类型
public class A extends Apple<String>
使用类、接口时也可以不为类型形参传入实际的类型参数,即下面的代码也是正确的。
public class A extends Apple
并不存在泛型类:
不管为泛型的类型形参传入哪一种类型实参,对于 Java 来说,它们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此静态方法、静态初始化块或静态变量的声明和初始化中不允许使用类型形参。
由于系统中并不会真正生成泛型类,所以 instanceof 运算符后不能使用泛型类。
例如:
java.util.Collection<String> cs = new java.util.ArrayList<>();
//下面代码引起编译时错误: instanceof 运算符后不能使用泛型
if ( cs instanceof java.util.ArrayList<String>){....}
*
*
*
3 类型通配符
---------------------------------------------------------------------------------------------------------------
如果 Foo 是 Bar 的一个子类型 (子类或子接口),而 G 是具有泛型声明的类或接口, G<Foo> 并不是 G<Bar> 的子类型。
数组和泛型有所不同,假设 Foo 是 Bar 的一个子类型 (子类或者子接口),那么 Foo[] 依然是 Bar[] 的子类型。
使用类型通配符:
为了表示各种泛型 List 的父类, 可以使用通配符, 类型通配符是一个问号(?),将一个问号作为类型实参传给 List 集合,写作: List<?> ,意思是元素未知的 List。 这个问号 (?) 被称为通配符,它的元素类型可以匹配任何类型。
public void test ( List<?> c)
{
for (int i = 0; i < c.size(); i++)
{
System.out.println( c.get(i));
}
}
设定类型通配符的上限:
----------------------------------------------------------
//表示所有Shape泛型List的父类
List <? extends Shape>
此处的问号(?) 代表一个未知的类型,但此处的这个未知类型一定是 Shape 的子类型 (也可以是 Shape 本身), 因此可以把 Shape 称为这个通配符的上限 ( upper bound )
设定类型形参的上限:
----------------------------------------------------------
Java 泛型不仅允许在使用通配符形参时设定上限, 而且可以在定义类型形参时设定上限,用于表示传给该类型形参的实际类型要么是该上限类型,要么是该上限类型的子类型。
public class Apple<T extends Number>
{
T col;
public static void main(String [] args)
{
Apple<Integer> ai = new Apple<>();
Apple<Double> ad = new Apple<>();
// 下面的代码将引发编译异常,下面代码试图把 String 类型传给 T 形参
// 但 String 不是 Number 的子类型,所以编译错误
Apple<String> as = new Apple<>();
}
}
在一种更极端的情况下,程序需要为类型形参设定多个上限(至多有一个父类上限,可以有多个接口上限),表明该类型形参必须是其父类的子类(是父类本身也行),并且实现多个上限接口。
// 表明 T 类型必须是 Number 类或其子类,并必须实现 java.io.Serializable 接口
public class Apple<T extends Number & java.io.Serializable>
{
...
}
*
*
*
4 泛型方法
---------------------------------------------------------------------------------------------------
前面介绍了在定义类、接口时可以使用类型形参,在该类的方法定义和成员变量定义、接口的方法定义中,这些类型形参可以被当成普通类型来用。
在另外一些情况下,定义类、接口时没有使用类型形参,但定义方法时想自己定义类型形参,这也是可以的, Java 5 提供了对泛型方法的支持。
定义泛型方法 (Generic Method) :所谓泛型方法, 就是在声明方法时定义一个或多个类型形参。 格式如下:
修饰符 <T, S> 返回值类型 方法名(形参列表)
{
...
}
泛型方法的方法签名比普通方法的方法签名多了类型形参声明,类型形参声明以尖括号括起来,多个类型形参之间以逗号(,)隔开,所有的类型形参声明放在方法修饰符和方法返回值类型之间。
public class GenericMethodTest
{
// 声明一个泛型方法,该泛型方法中带一个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);
Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number> cn = new ArrayList<>();
// 下面代码中T代表Number类型
fromArrayToCollection(ia, cn);
// 下面代码中T代表Number类型
fromArrayToCollection(fa, cn);
// 下面代码中T代表Number类型
fromArrayToCollection(na, cn);
// 下面代码中T代表Object类型
fromArrayToCollection(na, co);
// 下面代码中T代表String类型,但na是一个Number数组,
// 因为Number既不是String类型,
// 也不是它的子类,所以出现编译错误
// fromArrayToCollection(na, cs);
}
}
上面程序定义了一个泛型方法,该方法中定义了一个 T 类型形参,这个 T 类型形参就可以在该方法内当成普通类型使用。
与接口、类声明中定义的类型形参不同的是,方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。
与类、接口中使用泛型参数不同的是,方法中的泛型参数无须显式传入实际类型参数,当程序调用 fromArrayToCollection() 方法时,无须在调用方法前传入 String、Object等类型,但系统
依然可以知道类型形参的数据类型,因为编译器根据实参推断类型形参的值,它通常推断出最直接的类型参数。
泛型方法和类型通配符的区别:
泛型方法允许类型形参被用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样的类型依赖关系,就不应该使用泛型方法。
如果某个方法中一个形参 (a) 的类型或返回值的类型依赖于另一个形参 (b) 的类型,则形参 (b) 的类型声明不应该使用通配符 ---- 因为形参 (a) 或返回值的类型依赖于该形参 (b) 的类型。
如果形参 (b) 的类型无法确定,程序就无法定义形参 (a) 的类型。在这种情况下,只能考虑使用在方法签名中声明类型形参 ---- 也就是泛型方法。
如果有需要,也可以同时使用泛型方法和通配符
public class Collections
{
public static <T> void copy(List<T> dest, List<? extends T> src){...}
}
类型通配符与泛型方法(在方法签名中显示声明类型形参)还有一个显著的区别: 类型通配符既可以在方法签名中定义形参的类型,也可以用于定义变量的类型;
但泛型方法中的类型形参必须在对应方法中显示声明。
Java 7 的 菱形 语法与泛型构造器
-------------------------------------------------------
正如泛型方法允许在方法签名中声明类型形参一样,Java 也允许在构造器签名中声明类型形参,这样就产生了所谓的泛型构造器。
一旦定义了泛型构造器,接下来在调用构造器时,就不仅可以让 Java 根据数据参数的类型来 “推断” 类型参数的类型,而且程序员也可以显式地为构造器中的类型形参指定实际的类型。
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);
}
}
前面介绍过 Java 7 新增的 菱形 语法,它允许调用构造器时在构造器后使用一对尖括号来代表泛型信息,但如果程序显式指定了泛型构造器中声明的类型形参的实际类型,则不可以使用 菱形语法。
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);
}
}
设定通配符下限:
--------------------------------------------------------------------------------
<? super Type> : 这个通配符表示它必须是 Type 本身,或是 Type 的父类。
public class MyUtils
{
// 下面dest集合元素类型必须与src集合元素类型相同,或是其父类
public static <T> T copy(Collection<? super T> dest
, Collection<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);
}
}
Java 8 改进的类型推断
-------------------------------------------------------
1. 可以通过调用方法的上下文来推断类型参数的目标类型
2. 可在方法调用链中,将推断得到的类型参数传递到最后一个方法。
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();
}
}
5 擦除和转换
-----------------------------------------------------------------------------------------------------------------
在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但为了与老的 java 代码保持一致,也允许在使用带泛型声明的类时不指定实际的类型参数。
如果没有为这个泛型类指定实际的类型参数,则该类型参数被称为 raw type (原始类型),默认是声明该类型参数时指定的第一个上限类型。
当把一个具有泛型信息的对象赋值给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都将被扔掉。比如 List<String> 类型转换为 List, 则该 List 对集合元素的类型检查变成了类型参数的上限(即 Object)。
class Apple<T extends Number>
{
T size;
public Apple()
{
}
public Apple(T size)
{
this.size = size;
}
public void setSize(T size)
{
this.size = size;
}
public T getSize()
{
return this.size;
}
}
public class ErasureTest
{
public static void main(String[] args)
{
Apple<Integer> a = new Apple<>(6); // ①
// a的getSize方法返回Integer对象
Integer as = a.getSize();
// 把a对象赋给Apple变量,丢失尖括号里的类型信息
Apple b = a; // ②
// b只知道size的类型是Number
Number size1 = b.getSize();
// 下面代码引起编译错误
Integer size2 = b.getSize(); // ③
}
}
Java 允许直接把一个 List 对象赋值给一个 List<Type> (Type 可以是任何类型)类型的变量,只是发出 “未经检查的转换” 警告。
但对 list 变量实际上引用的是 List<Integer>集合,所以当试图把集合里的元素当成 String 类型的对象取出时,将引发运行时异常。
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里的元素,如下面代码将引起运行时异常。
System.out.println(ls.get(0));
}
}