泛型
一、入门
增加了泛型支持后的集合,完全可以记住集合中元素的类型,并可以在编译时检查集合中元素的类型,如果试图向集合中添加不满足类型要求的对象,编译器就会提示错误。
1.1 编译时不检查类型异常
public class ListErr {
public static void main(String[] args) {
List l = new ArrayList();
l.add("java");
l.add("c");
l.add(1);
l.forEach(str -> System.out.println(((String) str).length()));
//报错java.lang.ClassCastException: 因为程序试图将integer转换为string
}
}
1.2 使用泛型在编译时提示报错
二、使用泛型
泛型的实质:允许在定义接口、类时声明泛型形参,泛型形参在整个接口、类体内可当成类型使用,几乎所有可使用普通类型的地方都可以使用这种泛型形参。
public class Apple <T>{ //定义Apple类用泛型声明
private T info; //用T定义实例变量
public Apple(){
}
public T getInfo() {
return info;
}
public void setInfo(T info) {
this.info = info;
}
public Apple(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<>(23.3);
System.out.println(a2.getInfo()); //23.3
}
}
2.1 派生子类
当使用这些接口、父类时不能再包含泛型形参。
解决:也可以不带
重写父类
public class A extends Apple{
@Override
public String getInfo(){
return super.getInfo().toString(); //super.getInfo()返回的是object类型加toString()才是String类型
}
}
2.2 不存在泛型类
不管为泛型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用泛型形参。
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass()); //true
三、 泛型方法
3.1 定义泛型
与接口、类声明中定义的泛型不同的是,方法声明中定义的泛型只能在该方法里使用,而接口、类声明中定义的泛型则可以在整个接口、类中使用。
public class fromArrayToCollection {
static<T> void fromArrayToCollection(T[] a, Collection<T> c){
for (T o:a){
c.add(o);
}
}
public static void main(String[] args) {
Object[] o1 = {"a","b"}; //T代表object类型
List<Object> o2 = new ArrayList<>();
fromArrayToCollection(o1,o2);
String[] s1 = {"a","b"}; //T代表String类型
List<String> s2 = new ArrayList<>();
fromArrayToCollection(s1,s2);
Integer[] i1 = {1,2}; //T代表Integer类型
List<Integer> i2 = new ArrayList<>();
fromArrayToCollection(i1,i2);
Number[] n1 = {1,2}; //T代表Number类型
List<Number> n2 = new ArrayList<>();
fromArrayToCollection(n1,n2);
}
}
3.2 泛型方法与类型通配符区别
public interface Collection<E>
<T>boolean containsAll(Collection<T>c);
<T extends E>boolean addAll(Collection<T>c);
//上面方法使用了<T extends E>泛型形式,这时定义泛型形参时设定上限(其中E是Collection接口)里定义的泛型,在该接口里E可当成普通类型使用)。
泛型方法允许泛型形参被用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样的类型依赖关系,就不应该使用泛型方法。
类型通配符与泛型方法(在方法签名中显式声明泛型形参)还有一个显著的区别:类型通配符既可以在方法签名中定义形参的类型,也可以用于定义变量的类型:但泛型方法中的泛型形参必须在对应方法中显式声明。
3.3 泛型构造器
Java也允许在构造器签名中声明泛型形参,产生了所谓的泛型构造器。
class Foo{
public <T> Foo(T t){
System.out.println(t);
}
}
public class GenericConstructor {
public static void main(String[] args) {
new Foo("java");
new Foo(200);
new <String> Foo("java"); //已经显示指定是string类型
new <String> Foo(200); //error: java: 不兼容的类型: int无法转换为java.lang.String
}
}
“菱形”语法,它允许调用构造器时在构造器后使用一对尖括号来代表泛型信息。但如果程序显式指定了泛型构造器中声明的泛型形参的实际类型,则不可以使用“菱形”语法。
如下程序所示。
class Foo<E>{
public <T> Foo(T t){
System.out.println(t);
}
}
public class GenericConstructor {
public static void main(String[] args) {
Foo<String> f1 = new Foo<>(5); //显示声明E形参是string,T形参是Integer
Foo<String> f2 = new <Integer> Foo<String>(5); //显示声明T形参是Integer
Foo<String> fe = new <Integer> Foo<>(5); //显示声明E形参是string,若显示声明T形参是Integer,则是错误
}
}
3.4 设定通配符下限
它表示必须是Type本身,或是Type父类<? super T>
public class MyUtils {
public static <T>T copy(Collection<? super T> dest ,Collection<T> src)
{
T last = null;
for (T o:src){
last = o;
src.add(o);
}
return last;
}
public static void main(String[] args) {
List<Number> ln = new ArrayList() ;
List<Integer> lt = new ArrayList() ;
lt.add(5);
Integer last = copy(ln,lt); //已经知道是Integer
System.out.println(ln);
}
}
3.5 泛型方法与方法重载
因为泛型既允许设定通配符的上限,也允许设定通配符的下限,从而允许在一个类里包含如下两个方法定义。
MyUtils类中包含两个copy()方法,这两个方法的参数列表存在一定的区别,但这种区别不是很明确:这两个方法的两个参数都是Collection对象,前一个集合里的集合元素类型是后一个集合里集合元素类型的父类。如果只是在该类中定义这两个方法不会有任何错误,但只要调用这个方法就会引起编译错误。
public class MyUtils
public static <T>void copy(Collection<T>dest Collection<?extends T> src)
{..} //1①
public static <T>T copy(Collection<?super T>dest Collection<T> src)
{..} //1②
3.6 Java8改进的类型推断
Java8改进了泛型方法的类型推断能力,类型推断主要有如下两方面。
可通过调用方法的上下文来推断泛型的目标类型。
可在方法调用链中,将推断得到的泛型传递到最后一个方法。
class inference<E>{
public static <Z> inference<Z> nil() {
return null;
}
public static <Z> inference<Z> cons(Z head,inference<Z> tail) {
return null;
}
E head() {
return null;
}
}
public class MyUtils {
public static void main(String[] args) {
inference<String> s1 = inference.nil() ; //可以通过方法赋值的目标参数来推断泛型为String
inference<String> s2 = inference.<String>nil() ; //无须使用下面语句在调用ni1()方法时指定泛型的类型
inference.cons(42,inference.nil()); //可调用cons()方法所需的参数类型来推断泛型为Integer
inference.cons(42,inference.<Integer>nil()); //无须使用下面语句在调用ni1()方法时指定泛型的类型
}
}
四、擦除与转换
当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都将被扔掉。比如一个List类型被转换为List,则该List对集合元素的类型检查变成了泛型参数的上限(即Object)。
擦除
class Applea<T extends Number>{ //Number定义size
T size;
public Applea(){ }
public Applea(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) {
Applea<Integer> a = new Applea<>(6); //传入形参Integer
Integer as = a.getSize(); //返回Integer
Applea b = a; //a赋值给不带泛型信息的b时,编译器丢失a对象的泛型信息
Number c = b.getSize(); //泛型形参是number,所以知道b返回的是number
// Integer d = b.getSize(); //识别不到Integer
}
}
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;
System.out.println(ls.get(0));
}
}
五、泛型与数组
Java泛型有一个很重要的设计原则,如果一段代码在编译时没有提出“[unchecked未经检查的转换”警告,则程序在运行时不会引发ClassCastException异常。正是基于这个原因,所以数组元素的类型不能包含泛型变量或泛型参,除非是无上限的类型通配符。但可以声明元素类型包含泛型变量或泛型形参的数组。也就是说,只能声明List【】形式的数组,但不能创建ArrayList[10]这样的数组对象。