疯狂Java讲义-泛型

泛型

本章思惟导图

泛型

泛型入门

Java集合有个缺点——把一个对象“丢进”集合里以后,集合就会“忘记”这个对象的数据类型,当再次取出该对象时,该对象的编译类型就变成了Object类型(其运行时类型没变)。java

编译时不检查类型的异常

下面代码将会看到编译时不检查类型所致使的异常。web

import java.util.ArrayList;
import java.util.List;

public class ListErr {

	public static void main(String[] args) {
		// 建立一个只想保存字符串的List集合
		List strList = new ArrayList();
		strList.add("十年寒窗无人问");
		strList.add("纵使相逢应不识");
		// "不当心"把一个Integer对象"丢进"了集合
		strList.add(5);
		strList.forEach(str -> System.out.println(((String) str).length()));
	}

}

上面程序建立了一个List集合,并且只但愿该List集合保存字符串对象——但程序不能进行任何限制,上面程序将引起ClassCastException异常。数组

使用泛型

从Java5之后,Java引入了参数化类型(parameterized type)的概念,容许程序在建立集合时指定集合元素的类型。Java参数化类型被称为泛型(Generic)。安全

建立这种特殊集合的方法是:在集合接口 、类后增长尖括号,尖括号里放一个数据类型,即代表这个集合接口、集合类只能保存特定类型的对象。从而使集合自动记住全部集合元素的数据类型,从而无须对集合元素进行强制类型转换。框架

Java9加强的”菱形“语法

在Java7之前,若是使用带泛型的接口、类定义变量,那么调用构造器建立对象时构造器的后面也必须带泛型,这显得有些多余了。例如以下两条语句。svg

List<String> strList = new ArrayList<String>();
Map<String, Integer> scores = new HashMap<String, Integer>();

上面两条语句中的构造器后面的尖括号部分彻底是多余的,在Java7之前这是必需的,不能省略。从Java7开始,Java容许在构造器后不需带完整的泛型信息,只要给出一对尖括号(<>)便可,Java能够推断尖括号里应该是什么泛型信息。上面两条代码能够改写为以下形式。学习

List<String> strList = new ArrayList<>();
Map<String, Integer> scores = new HashMap<>();

Java9再次加强了”菱形“语法,它甚至容许在建立匿名内部类时使用菱形语法,Java可根据上下文来推断匿名内部类中泛型的类型。下面代码示范了在匿名内部类中使用菱形语法。this

interface Foo<T> {
	void test(T t);
}

public class AnnoymousDiamond {

	public static void main(String[] args) {
		// 指定Foo类中泛型为String
		Foo<String> f = new Foo<>() {
			// test()方法的参数类型为String
			public void test(String t) {
				System.out.println("test方法的t参数为: " + t);
			}
		};
		// 使用泛型通配符,此时至关于通配符的上限为Object
		Foo<?> fo = new Foo<>() {
			// tes()方法的参数类型为Object
			public void test(Object t) {
				System.out.println("test方法的Object参数为: " + t);
			}

		};
		// 使用泛型通配符,通配符的上限为Number
		Foo<? extends Number> fn = new Foo<>() {
			// 此时test()方法的参数类型为Number
			public void test(Number t) {
				System.out.println("test方法的Number参数为: " + t);
			}
		};
	}

}

上面的代码定义了带泛型声明的接口。spa

深刻泛型

所谓泛型,就是容许在定义类、接口、方法时使用类型的形参,这个类型形参(或叫泛型)将在声明变量、建立对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)。Java5改写了集合框架中的所有接口,为这些接口、类增长了泛型支持,从而能够在声明集合变量、建立集合对象时传入类型实参。设计

定义泛型接口、类

下面是Java5改写后List接口、Iterator接口、Map的代码片断。

// 定义接口时制定了一个泛型形参,该形参名为E
public interface List<E> {
    // 在该接口里,E可做为类型使用
    // 下面方法可使用E做为参数类型
    void add(E x);
    Iterator<E> iterator();
    ...
}
// 定义接口时指定了一个泛型形参,该形参名为E
public interface Iterator<E> {
    // 在该接口里E彻底可做为类型使用
    E next();
    boolean hasNext();
    ...
}
// 定义接口时指定了一个泛型形参,该形参名为E
public interface Map<K, V> {
    // 在该接口里K、V彻底可做为类型使用
    Set<K, V> keySet();
    V put(K key, V value);
    ...
}

容许在定义接口、类时声明泛型形参,泛型形参在整个接口、类体内可当成类型使用,几乎全部可以使用普通类型的地方均可以使用这种泛型形参。

能够为任何类、接口增长泛型声明,并非只有集合类才可使用泛型声明。

从泛型类派生子类

当建立了带泛型声明的接口、父类以后,能够为该接口建立实现类,或从该父类派生子类,当使用这些接口、父类时不能再包含泛型形参。

方法中的形参表明变量、常量、表达式等数据。定义方法时能够声明数据形参,调用方法时必须为这些数据形参传入实际的数据;与此相似的是,定义类、接口、方法时能够声明泛型形参,使用类、接口、方法时应该为泛型形参传入实际的类型。

若是想从Apple类派生一个子类,以下代码。

// 使用Apple类时,为T形参传入String类型
public class A extends Apple<String>

调用方法时必须为全部的数据形参传入参数值,与调用方法不一样的是,使用类、接口时也能够不为泛型形参传入实际的类型参数,下面代码也是正确的。

// 使用Apple类时,没有为T形参传入实际的类型参数
public class A extends Apple

像这种使用Apple类时省略泛型的形式被称为原始类型(raw type)。

若是使用原始类型的形式继承父类,Java编译器可能发出警告:使用了未经检查或不安全的操做——这就是泛型检查的警告,若是但愿看到该警告提示的更详细信息,则能够经过为javac命令增长-Xlint:unchecked选项来实现。

并不存在泛型类

当一个类使用了泛型,系统并无为该类生成新的class文件,并且也不会将该类当成新类来处理。无论泛型的实际类型参数是什么,它们在运行时总有一样的类。

无论泛型形参传入哪种类型实参,对于Java来讲,它们依然被当成同一个类处理,在内存中也只占用一块,所以在静态方法、静态初始化块或者静态变量的声明和初始化中不容许使用泛型形参。下面程序演示了这种错误。

public class R<T> {
    // 下面代码错误,不能在静态变量声明中使用泛型形参
    static T info;
    T age;
    public void foo(T msg) {}
    // 下面代码错误,不能在静态方法声明中使用泛型形参
    public static void bar(T msg) {}

因为系统中并不会真正生成泛型类,因此instanceof运算符后面不能使用泛型类。下面代码是错误的。

Collection<String> cs = new ArrayList<>();
// 下面代码编译时引起错误:instanceof运算符后不能使用泛型
if (cs instanceof ArrayList<String>) {
    ...
}

类型通配符

当使用一个泛型类时(包括声明变量和建立对象两种状况),都应该为这个泛型类传入一个类型实参。若是没有传入类型实际参数,编译器就会提出泛型警告。

若是Foo是Bar的一个子类型(子类或者子接口),而G是具备泛型声明的类或接口,G并非G的子类型。

在数组中,程序能够直接把一个Integer[]数组赋给一个Number[]变量。若是试图把一个Double对象保存到该Number[]数组中,编译能够经过,但在运行时抛出ArrayStoreException异常。

Java在泛型设计时进行了改进,再也不容许把List对象赋值给List变量。

Java泛型设计原则是,只要代码在编译时没有出现警告,就不会遇到运行时ClassCastException异常。

数组和泛型有所不一样,假设Foo是Bar的一个子类型(子类或者子接口),那么Foo[]依然是Bar[]的子类型;但G不是G的子类型。Foo[]自动向上转型为Bar[]的方式被称为型变。Java数组支持型变,但Java集合并不支持型变。

使用类型通配符

为了表示各类泛型List的父类,可使用类型通配符,类型通配符是一个问号(?),将一个问号做为类型实参传给List集合,写做:List<?>(意思是元素类型未知的List)。这个问号(?)被称为通配符,它的元素类型能够匹配任何类型。例如代码。

public void test(List<?> c) {
    for (int i = 0; i < c.size(); i++) {
        System.out.println(c.get(i));
    }
}

如今使用任何类型的List来调用它,程序依然能够访问集合c中的元素,其类型是Object,这永远是安全的,由于无论List的真实类型是什么,它包含的都是Object。

但这种带通配符的List仅表示它是各类泛型List的父类,并不能把元素加入到其中。例以下代码,将会引发编译错误。

List<?> c = new ArrayList<String>();
// 下面代码引发编译错误
c.add(new Object());

由于程序没法肯定c集合中的元素类型,因此不能向其中添加对象。

程序能够调用get()方法来返回List<?>集合指定索引处的元素,其返回值是一个未知类型,但能够确定,它老是一个Object。

设定类型通配符的上限

为了表示List集合的全部元素是一个类F的子类,Java泛型提供了被限制的泛型通配符。被限制的泛型通配符表示以下:

// 它表示泛型形参必须是F子类的List
List<? extends F>

List<? extends F>是受限制通配符的例子,此处的问号(?)表明一个未知类型,可是必定是F类的子类型(也能够是F自己),所以能够把F称为这个通配符的上限(upper bound)。

相似地,因为程序没法肯定这个受限制的通配符的具体类型,因此不能把F对象或其子类的对象加入这个泛型集合中。例以下代码是错误的。

public void addFs(List<? extends F> fs) {
    // 下面代码引发编译错误
    fs.add(0, new S());
}

这种指定通配符上限的集合,只能从集合中取元素(取出的元素老是上限的类型),不能向集合中添加元素(由于编译器无法肯定集合元素实际是哪一种子类型)。

对于更普遍的泛型来讲,指定通配符上限就是为了支持类型型变。好比Foo是Bar的子类,这样A就至关于A<? extends Foo>的子类,能够将A赋值给A<? extends Foo>类型的变量,这种型变方法被称为协变

对于协变的泛型类来讲,它只能调用泛型类型做为返回值类型的方法(编译器会将该方法返回值当成通配符上限的类型);而不能调用泛型类型做为参数的方法。口诀是:协变只出不进。

对于指定通配符上限的类型类,至关于通配符上限是Object。

设定类型通配符的下限

通配符下限用<? super 类型>的方式来指定,通配符下限的做用与通配符上限的做用刚好相反。

指定通配符的下限就是为了支持类型型变。好比Foo是Bar的子类,当程序须要一个A<? super Bar>变量时,程序可将A、A赋值给A<? super Bar>类型的变量,这种型变方法被称为逆变

对于逆变的泛型集合来讲,编译器只知道集合元素是下限的父类型,但具体是哪一种父类型则不肯定。所以,这种逆变的泛型集合能向其中添加元素(由于实际赋值的集合元素老是逆变声明的父类),从集合中取元素时只能被当成Object类型处理(编译器没法肯定取出的究竟是哪一个父类的对象)。

设定泛型形参的上限

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<>();
	}
}

上面代码定义了一个Apple泛型类,该Apple类的泛型形参的上限是Number类,这代表Apple类是为T形参传入的实际类型只能是Number类或Number类的子类。

在一种更极端的状况下,程序须要为泛型形参设定多个上限(至多一个父类上限,能够有多个接口上限),代表该泛型形参必须是其父类的子类(父类自己也行),而且实现多个上限接口。

// 代表T类型必须是Number类或其子类,并必须实现java.io.Serializable接口
public class Apple<T extends Number & java.io.Serializable> {
    ...
}

与类同时继承父类、实现接口相似的是,为泛型形参指定多个上限时,因此的接口上限必须位于类上限以后(类上限位于第一位)。

泛型方法

定义泛型方法

Java5提供的泛型方法(Generic Method),在声明方法时定义一个或多个泛型形参。泛型方法的语法格式以下。

修饰符 <T, S> 返回值类型 方法名(形参列表) {
    // 方法体...
}

泛型形参声明以尖括号括起来,多个泛型形参之间以逗号(,)隔开,全部的泛型形参声明放在方法修饰符和方法返回值类型之间。

与接口、类声明中定义的泛型不一样的是,方法声明中定义的泛型只能在该方法里使用,而接口、类声明中定义的泛型则能够在整个接口、类中使用。

与类、接口中使用泛型参数不一样的是,方法的泛型参数无须显示传入实际类型参数,系统能够知道为泛型实际传入的类型,由于编译器根据实参推断出泛型所表明的类型,它一般推断出最直接的类型。

为了让编译器能准确地推断出泛型方法中泛型的类型,不要制做迷惑。以下程序。

public class ErrorTest {
	// 声明一个泛型方法,该泛型方法中带一个T泛型形参
	static <T> void test(Collection<T> from, Collection<T> to) {
		for (T ele : from) {
			to.add(ele);
		}
	}
	public static void main(String[] args) {
		List<Object> ao = new ArrayList<>();
		List<String> as = new ArrayList<>();
		// 下面代码将产生编译错误
		test(as, ao);
	}
}

该方法中的两个形参from、to的类型都是Collection<T>,这要求调用该方法时的两个集合实参中的泛型类型相同,不然编译器没法准确地推断出泛型方法中泛型形参的类型。。

可将代码改为以下。

public class RightTest {
	// 声明一个泛型方法,该泛型方法中带一个T泛型形参
	static <T> void test(Collection<? extends T> from, Collection<T> to) {
		for (T ele : from) {
			to.add(ele);
		}
	}
	public static void main(String[] args) {
		List<Object> ao = new ArrayList<>();
		List<String> as = new ArrayList<>();
		// 下面代码彻底正常
		test(as, ao);
	}
}

上面代码改变了test()方法签名,将该方法的前一个形参类型改成Collection<? extends T>,这种采用类型通配符的表示方式,只要test()方法的前一个Collection集合里的元素类型是后一个Collection集合里元素类型的子类便可。

泛型方法和类型通配符的区别

大多数的时候均可以使用泛型方法来代替类型通配符。例如,对于Java的Colletion接口中两个方法定义:

public interface Collection<E> {
    boolean containsAll(Collection<?> c);
    boolean addAll(Colletion<? extends E> c);
    ...
}

上面集合中两个方法的形参都采用了类型通配符的形式,也能够采用泛型方法的形式,以下所示。

public interface Collection<E> {
    <T> boolean containsAll(Collection<T> c);
    <T extends E> boolean addAll(Collection<T> c);
    ...
}

上面方法使用了<T extends E>泛型形式,这时定义泛型形参时设定上限。

上面两个方法中泛型形参T只使用了一次,泛型形参T产生的惟一效果是能够在不一样的调用点传入不一样的实际类型。对于这种状况,应该使用通配符:通配符就是被设计用来支持灵活的子类化的。

泛型方法容许泛型形参被用来表示方法的一个或多个参数之间的类型依赖,或者方法返回值与参数之间的类型依赖关系。若是没有这样的类型依赖关系,就不该该使用泛型方法。

若是某个方法中一个形参(a)的类型或返回值的类型依赖于另外一个形参(b)的类型,则形参(b)的类型声明不该该使用通配符——由于形参(a)或返回值的类型依赖于该形参(b)的类型,若是形参(b)的类型没法肯定,程序就没法定义形参(a)的类型。在这种状况下,只能考虑使用在方法签名中声明泛型——也就是泛型方法。

若是有须要,也能够同时使用泛型方法和通配符,如Java的Colletions.copy()方法。

public class Colletions {
    public static <T> void copy(List<T> dest, List<? extends T> src) {
        ...
    }
}

上面的copy方法中的dest和src存在明显的依赖关系,从源List中复制出来的元素,必须能够存放在目标List中,因此源List集合元素的类型只能是目标集合元素的类型的子类型或者它自己。但JDK定义src形参类型时使用的是类型匹配符,而不是泛型方法。这是由于:该方法无须向src集合中添加元素,也无须修改src集合里的元素,因此可使用类型通配符,无须使用泛型方法。

简而言之,指定上限的类型通配符支持协变,所以这种协变的集合能够安全地取出元素(协变只出不进),所以无须使用泛型方法。

固然,也能够将上面的方法签名改成使用泛型方法,不使用类型通配符,以下所示。

class Collections {
    public static <T, S extends T> void copy(List<T> dest, List<S> src) {
        ...
    }
}

这个方法签名能够代替前面的方法签名。但注意上面的泛型形参S,它仅使用了一次,其余参数的类型、方法返回值类型都不依赖与它,那泛型形参S就没有存在的必要,便可以用通配符来代替S。使用通配符比使用泛型方法(在方法签名中显式声明泛型形参)更加清晰和准确,所以Java设计该方法时采用了通配符,而不是泛型方法。

类型通配符和泛型方法还有一个显著的区别:类型通配符既能够在方法签名中定义形参的类型,也能够用于定义变量的类型;但泛型方法中的泛型形参必须在对应方法显式声明。

Java7的菱形语法与泛型构造器

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("好好学习");
		// 泛型构造器中的T类型为Integer
		new Foo(1024);
		// 显式指定泛型构造器中T类型为String
		// 传给Foo构造器的实参也是String对象 彻底正确
		new <String>Foo("每天向上");
		// 显式指定泛型构造器中T类型为String
		// 传给Foo构造器的实参是Double对象 下面代码出错
		new <String>Foo(3.14);
    }
}

Java7新增的菱形语法,容许调用构造器时在构造器后使用一对尖括号来表明泛型信息。但若是程序显式指定了泛型构造器中声明的泛型形参的实际类型,则不可使用菱形语法。以下程序所示。

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<>(9);
        
		// 显式指定泛型构造器中声明的T形参是Integer类型
		MyClass<String> mc2 = new <Integer>MyClass<String>(9);
        
		// MyClass类声明中E形参是String类型
		// 若是显式指定泛型构造器中声明的T形参是Integer类型
		// 此时就不能使用菱形语法 下面代码是错的
		MyClass<String> mc3 = new <Integer>MyClass<>(9);
	}
}

泛型方法与方法重载

由于泛型即容许设定通配符上限,也容许设定通配符的下限,从而容许在一个类里包含以下两个方法定义。

public class MyUtils {
    public static <T> void copy(Collection<T> dest, Collection<? extends T> src) {
        ...
    }
    public static <T> T copy(Collection<? super T> dest, Collection<T> src) {
        ...
    }
}

这两个方法参数都是Collection对象,前一个集合里的集合元素类型是后一个集合里集合元素类型的父类。若是只是在该类中定义这两个方法不会有任何错误,但只要调用这个方法就会引发编译错误。例以下代码。

List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
copy(ln, li);

调用copy()方法既能够匹配第一个copy方法也能够匹配第二个copy方法,编译器没法肯定想调用哪一个copy方法,因此引发编译错误。

Java8改进的类型推断

Java8改进了泛型方法的类型推断能力,类型推断主要有以下两方面。

  • 可经过调用方法的上下文来推断泛型的目标类型。
  • 可在方法调用链中,将推断获得的泛型传递到最后一个方法。
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()类方法做用彻底相同,第一个无须在调用nil()方法时显式指定泛型参数为String,这是由于程序须要将该方法返回值赋值给MyUtil<String>类型,所以系统能够自动推断出此处的泛型参数为String类型。

后两个调用cons()方法做用也彻底相同,第一个无须再调用cons()方法时显式指定泛型参数为Integer,这是由于程序将nil()方法返回值做为了cons()方法的第二个参数,而程序可根据cons()方法的第一个参数(42)推断出此处的泛型参数为Integer类型。

虽然Java8加强了泛型推断的能力,但泛型推断不是万能的,以下代码就是错误的。

// 但愿系统能推断出调用nil()方法时泛型为String类型
// 但实际上Java8依然推断不出来,因此下面代码报错
String s = MyUtil.nil().head();

所以,上面这行代码必须显式指定泛型的实际类型,即将代码改成以下形式:

String s = MyUtil.<String>nil().head();

擦除和转换

容许再使用带泛型声明的类时不指定实际的类型。若是没有为这个泛型类指定实际的类型,此时被称做raw type(原始类型),默认是声明该泛型形参时指定的第一个上限类型。

当把一个具备泛型信息的对象赋给另外一个没有泛型信息的变量时,全部在尖括号之间的类型信息都将被扔掉。好比一个List<String>类型被转换为List,则该List对集合元素的类型检查变成了泛型参数的上限(即Object)。下面程序示范了这种擦除。

class Milk<T extends Number> {
	T size;
	public Milk() {}
	public Milk(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) {
		Milk<Integer> a = new Milk<>(6);
		// a的getSize()方法返回Integer
		Integer as = a.getSize();
		// 把a对象赋给Milk变量,丢失尖括号里的类型信息
		Milk b = a;
		// b只知道size的类型是Number类
		Number size1 = b.getSize();
		// 下面代码引发编译错误
		Integer size2 = b.getSize();		
	}
}

当把a赋给一个不带泛型信息的b变量时,编译器就会丢失a对象的泛型信息,即全部尖括号里的信息都会丢失——由于Milk的泛型形参上限是Number类,因此编译器依然知道b的getSize()方法返回Number类型,但具体是Number的哪一个子类就不清楚了。

从逻辑上看,List<String>是List的子类,若是直接把一个List对象赋给一个List<String>对象应该引发编译错误,但实际上不会。对泛型而言,能够直接把一个List对象赋给一个List<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));
	}
}

当把这个List<Integer>对象赋给一个List类型后,编译器就会丢失前者的泛型信息,即丢失集合里元素的类型信息,这就是典型的擦除。当试图把该集合里的元素当成String类型对象取出时,将引起ClassCastException异常。

泛型与数组

Java泛型有一个很重要的设计原则——若是一段代码在编译时没有提出“[unchecked]未经检查的转换”警告,则程序在运行时不会引起ClassCastException异常。正是基于这个缘由,因此数组元素的类型不能包含泛型变量或泛型形参,除非是无上限的类型通配符。但能够声明元素类型包含泛型变量或泛型形参的数组。也就是说,只能声明List<String>[]形式的数组,但不能建立ArrayList<String>[10]这样的数组对象。

加入Java支持建立ArrayList[10]这样的数组对象,则有以下程序:

// 下面代码其实是不容许的
List<String>[] lsa = new ArrayList<String>[10];
// 将lsa向上转型位Object[]类型的变量
Object[] oa = lsa;
List<Integer> li = new ArrayList<>();
li.add(3);
// 将List<Integer>对象做为oa的第二个元素
// 下面代码没有任何警告
oa[1] = li;
// 下面代码也不会有任何警告,但将引起ClassCastException异常
String s = lsa[1].get(0);

若是第一行代码是合法的,势必在最后一行引起运行时异常,这就违背了Java泛型的设计原则。

若是将程序改为以下形式:

// 下面代码编译时有“[unchecked] 未经检查的转换”警告
List<String>[] lsa = new ArrayList[10];
// 将lsa向上转型位Object[]类型的变量
Object[] oa = lsa;
List<Integer> li = new ArrayList<>();
li.add(3);
oa[1] = li;
// 下面代码引发ClassCastException异常
String s = lsa[1].get(0);

不容许建立List<String>[]类型的对象,但能够建立一个类型为ArrayList[10]的数组对象。只是在第一行会有编译警告。

Java容许建立无上限的通配符泛型数组,例如new ArrayList<?>[10],在这种状况下,程序不得不进行强制类型转换。在进行强制类型转换以前应经过instanceof运算符来保证它的数据类型。以下代码。

List<?>[] lsa = new ArrayList<?>[10];
Object[] oa = lsa;
List<Integer> li = new ArrayList<>();
li.add(3);
oa[1] = li;
Object target = lsa[1].get(0);
if (target instanceof String) {
    // 下面代码安全
    String s = (String)target;
}

与此相似的是,建立元素类型是泛型类型的数组对象也将致使编译错误。以下所示。

<T> T[] makeArray(Collection<T> coll) {
    // 下面代码致使编译错误
    return new T[coll.size()];
}

因为类型变量在运行时并不存在,而编译器没法肯定实际类型是什么,所以编译器报错。

相关文章
相关标签/搜索