三、泛型
1、泛型原理与使用
简介
重点:Java泛型的设计原则是,只要代码在编译时没有出现警告,就不会遇到运行时ClassCastException异常。
产生原因:将对象放入到集合中,集合并不能记住对象的类型,直接转换为Object类型。取对象时需要进行强制转换,造成代码臃肿冗余,还可能出现ClassCastException异常
改进:java 5 增加支持泛型的集合,在编译期能够记住集合中的类型,并进行检查,增强了软件的健壮性
使用泛型的接口,构造器后面必须带泛型,在Java 7以前这是必需的:List<String> list = new ArrayList<String>();
java7之后可以不用带:List<String> list = new ArrayList<>();
泛型:允许在定义类、接口、方法时使用类型形参,这个类型形参(或叫泛型)将在声明变量、创建对象、调用方法时动态的指定(即传入实际的类型参数,或类型实参)。
优点:包含泛型声明的类型可以在定义变量、创建对象时传入一个类型实参,从而可以动态地生成无数多个逻辑上的子类,但这种子类在物理上并不存在。
案例
Apple.java
package crazy.java.chapter9;
public class Apple<T> {
private T info;
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) {
Apple<String> a1 = new Apple<>("苹果");
System.out.println(a1.getInfo());
Apple<Double> a2 = new Apple<>(5.62);
System.out.println(a2.getInfo());
}
}
ApplePie.java
package crazy.java.chapter9;
public class ApplePie<T> extends Apple<T> {
public static void main(String[] args) {
ApplePie a = new ApplePie("苹果");
ApplePie b = new ApplePie(5.56);
System.out.println(a.getInfo());
System.out.println(b.getInfo());
}
public ApplePie(T info) {
super(info);
}
}
下面这段代码的值,总是true,并没有生成新的class文件。不管为泛型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此在静态方法、静态初始化块或者静态变量(它们都是类相关的)的声明和初始化中不允许使用泛型形参。
List<String> l1 = new ArrayList<>();
List<Integer> l2 = new ArrayList<>();
System.out.println(l1.getClass() == l2.getClass());
2、类型通配符
2.1、类型通配符 (?)
List<?>,这种写法可以适应于任何支持泛型声明的接口和类,比如写成Set<?>、Collection<?>、Map<?, ?>等。
问号(?)被称为通配符,它的元素类型可以匹配任何类型。
没有指定通配符上限的泛型类,相当于通配符上限是Object
案例
public static void test2(List<?> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
? 可以表示任何类型,此处List仅表示它是各种泛型List的父类,并不能把元素加入到其中。但是可以获取其中的值,其返回的是一个未知类型,但是可以肯定的是它总是一个Object。
2.2、类型通配符的上限
为了限制List集合的所有元素是Shape的子类,Java泛型提供了被限制的泛型通配符。
可以将A赋值给A<? extends Bar>类型的变量,这种型变方式被称为协变。协变只出不进! (此处的只出不进意思是只能获取其中的元素,不能往里面新增元素)
public void drawAll(List<? extends Shape> shapes){
for(Shape s : shapes){
s.draw(this);
}
}
白话文解释:就是List集合里面的元素类型必须是Shape接口的子类或者是Shape,可以取出里面的值,因为所有的元素类型都是Object的子类,可以向上转型获取。但是不能新增,因为不清楚具体的集合元素类型,很容易出问题。
2.3、类型通配符的下限
Java也允许指定通配符的下限,通配符的下限用<? super 类型> 的方式来指定。
程序可以将A、A赋值给A<? super Foo>类型的变量,这种型变方式被称为逆变 。
package crazy.java.chapter9;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class MyUtils {
public static void main(String[] args) {
List<Number> ln = new ArrayList<Number>();
List<Integer> li = new ArrayList<Integer>();
li.add(5);
li.add(10);
Integer last = copy(ln, li);
System.out.println(last);
}
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;
}
}
白话文解释:不管src集合元素的类型是什么,只要dest集合元素的类型与前者相同或者是前者的父类即可 。
JDK案例:TreeSet
package crazy.java.chapter9;
import java.util.Comparator;
import java.util.TreeSet;
public class TreeSetTest {
public static void main(String[] args) {
TreeSet<String> ts1 = new TreeSet<>(
new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
return hashCode() > o2.hashCode() ? 1 :
hashCode() < o2.hashCode() ? -1 : 0;
}
}
);
ts1.add("hello");
ts1.add("world");
System.out.println(ts1);
TreeSet<String> ts2 = new TreeSet<>(
new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length() > o2.length() ? -1 :
o1.length() < o2.length() ? 1 : 0;
}
}
);
ts2.add("hello");
ts2.add("world");
System.out.println(ts2);
}
}
经验总结:查看源码中的英文注释,能够帮助理解API的作用。
2.4、泛型形参的上限
上面三种是设置在使用上面(更像方法层面)的参数限制,这种是设定在定义上面。
Java泛型不仅允许在使用通配符形参时设定上限,而且可以在定义泛型形参时设定上限,用于表示传给该泛型形参的实际类型要么是该上限类型,要么是该上限类型的子类
public class Orange<T extends Number> {
public static void main(String[] args) {
Orange<Integer> orange = new Orange();
Orange<Double> orange1 = new Orange<>();
// Orange<String> orange2 = new Orange<String>();
// String不是Number的子类,编译时会引起编译错误
}
}
在一种更极端的情况下,程序需要为泛型形参设定多个上限(至多有一个父类上限,可以有多个接口上限),表明该泛型形参必须是其父类的子类(是父类本身也行),并且实现多个上限接口。
与类同时继承父类、实现接口类似的是,为泛型形参指定多个上限时,所有的接口上限必须位于类上限之后。也就是说,如果需要为泛型形参指定类上限,类上限必须位于第一位。
// 表明T类型必须是Number类或其子类,并必须实现java.io.Serializable接口
public class Orange<T extends Number & java.io.Serializable> {
....
}
3、泛型方法
3.1、定义泛型方法
在定义类、接口时可以使用泛型形参,在该类的方法定义和成员变量定义、接口的方法定义中,这些泛型形参可被当成普通类型来用
在定义类、接口时没有使用泛型形参,但定义方法时想自己定义泛型形参,这也是可以的,Java 5还提供了对泛型方法的支持。
泛型方法语法格式:
修饰符 <T, S> 返回值类型 方法名(形参列表){
// 方法体 .....
}
泛型形参声明以尖括号括起来,多个泛型形参之间以逗号(,)隔开,所有的泛型形参声明放在方法修饰符和方法返回值类型之间。
案例
import java.util.ArrayList;
import java.util.Collection;
public class GenericMethodTest {
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<>();
fromArrayToCollection(oa, co);
String [] sa = new String[100];
Collection<String> cs = new ArrayList<>();
fromArrayToCollection(sa, cs);
Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number> cn = new ArrayList<>();
fromArrayToCollection(ia, cn);
fromArrayToCollection(fa, cn);
fromArrayToCollection(na,cn);
// fromArrayToCollection(na, cs);
// 不是它的子类,类型转换有问题,所以编译错误
}
}
总结:实际不难看出,这个是一个父类和子类转型的问题。子类可以向上转型为父类,所以在往集合中添加数据的时候不会有问题能通过泛型的编译检查,但是如果不是同一个类型进行转换就会有问题,自我感觉这里是把在运行期才能确定的问题提前在编译期检查出来。
3.2、泛型方法和类型通配符的区别
问题:到底何时使用泛型方法?何时使用类型通配符呢?
使用通配符:通配符就是被设计用来支持灵活的子类化的。
泛型方法允许泛型形参被用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样的类型依赖关系,就不应该使用泛型方法。
3.3、“菱形”语法与泛型构造器
Java 7新增的“菱形”语法,它允许调用构造器时在构造器后使用一对尖括号来代表泛型信息。
public class GenericDiamondTest {
public static void main(String[] args) {
MyClass<String> myClass = new MyClass<>(5);
MyClass<String> myClass1 = new <Integer>MyClass<String>(4);
MyClass<String> myClass2 = new MyClass<String>(3);
// MyClass类声明中的E形参是String类型
// 如果显示指定泛型构造器中声明的T形参是Integer类型
// 此时就不能使用“菱形”语法,下面代码是错误的
// MyClass<String> myClass3 = new <Integer>MyClass<>(7);
}
}
class MyClass<E> {
public <T> MyClass(T t) {
System.out.println("t参数的值为:" + t);
}
}
3.4、搽除与转换
当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都将被扔掉。比如一个List类型被转换为List,则该List对集合元素的类型检查变成了泛型参数的上限(即Object)
泛型被搽除之后,又赋值给List :“未经检查的转换”警告,可能产生ClassCastException异常。
import lombok.Data;
public class ErasureTest {
public static void main(String[] args) {
Fruit<Integer> a = new Fruit<>(6);
Integer as = a.getSize();
Fruit b = a;
Number size = b.getSize();
// 下面代码会编译错误
// Integer size2 = b.getSize();
}
}
@Data
class Fruit<T extends Number>{
T size;
public Fruit(){}
public Fruit(T size){
this.size = size;
}
}