天天看点

第9章:泛型

9.1 泛型入门

9.1.1 编译时不检查类型所导致异常

将Integer装入集合,集合会丢失对象状态信息,集合只知道自己装的是Object,读取集合元素时,如果将Integer强转成String引发ClassCastException,所以引入泛型机制,保证编译只要不报错,运行就不应报错

9.1.2 使用泛型

java5以后允许创建集合时指定集合元素的类型,即参数化类型,也被称作泛型

//类型参数为String
List<String> list = new ArrayList<String>();
//除了String外其他类型无法插入
//list.add(123);
list.add("4");
//从list中取出元素默认为String类型,不需要再强转
String a = list.get(0);
//实际上左边有<String>就已经可以不进行强制转换了,为什么还要写右边的<String>?其实对于ArrayList这个类来讲,这两种写法,真的是一点区别没有,但编译器会发现你使用不带泛型时候就进行报错,原因是可以尝试如下代码
import java.util.ArrayList;
import java.util.List;

public class TestWu {
	public static void main(String[] args) {
		List<String> a = new ArrayList() {
			{
				add(3);
			}
		};
		//运行期间该语句报错,Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
		//且如果new ArrayList<String>();那么就可以在编译期,就发现这个转换错误,而不是运行期间
		System.out.println(a.get(0));
	}
}

           
9.1.3 java7泛型语法:菱形语法
//1. java7以后,构造器后不用再带泛型,用<>代替即可
//2. 这是因为左方"String"为编译时类型,可以保证list.add时必须add一个String对象,又由于ArrayList<其他类型>都不是List<String>的子类型,编译会报错,所以可以省略,产生菱形语法这种形式
//3. 其实可以理解为如下写法都不正确,即其实左边固定,右边也固定了,因此也没必要写右边了
//List<String> list = new ArrayList<Object>();
//List<Object> list = new ArrayList<String>();
List<String> list = new ArrayList<>();
           

9.2 深入泛型

9.2.1 定义泛型接口,类
  1. 定义泛型类(接口)实际上就是指泛型在整个类(接口)中生效
  2. 泛型的实质:将数据的类型(String,BigDecimal,Object,Integer…)参数化,从而允许同一形参列表中传入不同种的数据类型,形参列表中的参数类型叫做类型形参,类型形参在接口、类内被当成类型使用,这个类型形参将在声明变量,调用方法,调用构造器时动态指定(即传入实际的类型参数,也称为类型实参)
//T:类型形参,<T>:声明一个类型形参
class Apple<T>{
    public void print(List<T> a){
        
    }
    public void printNew(List<? extends Object> a){
        
    }
    public <F>void printOther(List<F extends Object> a){
        
    }
}
//String为类型实参
Apple<String> a = new Apple<String>();
           
  1. 代码分析
public interface List<E>{
    void add(E x);
    Iterator<E> iterator();
    ...
}
...
//相当于将E变为String类型,所以add方法只能放入String类型,从而控制了集合中只能放入String元素
List<String> list;
...
//List<String>功能上等同于如下接口,但注意List<String>并不是List子类,他不是一个新的类,系统不会为List<String>单独生成class文件
public interface ListString extends List{
    void add(String x);
    Iterator<String> iterator();
    ...
}
           
  1. 自定义泛型类或接口
package mytest.javaBase;

public class Apple<T> {
	//注意定义构造器时不要加泛型
	public Apple(){
		
	}
	public T getINFO();
	public static void main(String[] args) {
		//使用泛型类时(声明变量,调用构造器,调用方法),需要传入类型实参
		Apple<String> a = new Apple<String>();
	}
}
 
           
9.2.2 从泛型类派生子类
//错误的定义
public class A extends Apple<T>{
    
}
//以下两个定义方式都正确
public class A extends Apple<String>{
    //此时相当于Apple中的T全由String代替,因此A重写Apple的方法时应该注意
    //public T getInfo();此时变为public String getInfo();重写时子类的getInfo()返回值需小于等于String类型
}
//此时类型实参被当做Object处理
public class A extends Apple{
    
}
           
9.2.3 并不存在泛型类
List<String> l1 = new ArrayList();
List<Integer> l2 = new ArrayList();
//1.返回true
l1.getClass() == l2.getClass();
//2.List<String>与List<Integer>被当做同一个类进行处理,在内存中占用同一块内存空间,所以静态方法与静态初始化块中不允许泛型
//static T info;编译报错
/*public static void bar(T msg){
    
}编译报错*/
//3.由于系统不会真正生成泛型类,所以instanceof运算符后不能使用泛型类
Collection<String> cs = new ArrayList<>();
//cs instanceof ArrayList<Stirng>;编译报错
cs instanceof ArrayList;
cs instanceof ArrayList<?>;
           

9.3 类型通配符

java泛型设计原则为编译时没有警告,就不会遇到运行时的classCastException

List<Integer> iList = new ArrayList<>();
//编译报错,因为List<Integer>不是List<Number>的子类型
//List<Number> nList = iList;
Integer[] ia = new Integer[5];
//不报错,因为Integer[]是Number[]的子类型,Integer[]也为Object[]子类型
Number[] na = ia;
//运行时候报错,ArrayStoreException
na[0] = 0.5;
           
9.3.1 产生原因
test1(List t){
	Shape a = (Shape)t.get(0);
	...
}
test2(List<Shape> t){
	Shape a = t.get(0);
	....;
}
//对于test1方法,无论集合中元素是如何,都可以放入,其方法内想执行转换时会报错
//对于test2方法,虽然转换不再存在报错,但由于即使Circle为Shape子类型,但List<Circle>并不是List<Shape>的子类型,于是如下代码会报错
//List<Circle> l = new ArrayList<>();
//test2(l);
//想解决以上两个问题,需要引入类型通配符"?",从而可以表示所有包含Shape子类元素的集合的父类型,即List<? extends Shape>为List<Circle>的父类型,修改后的方案,既不用强制转换,又允许传入
test3(List<? extends Shape> t){
	Shape a = t.get(0);
	....;
}
           
9.3.2 使用类型通配符
  1. List<?>为所有泛型List的父类(list instanceof List<?>正确)
  2. 使用
List<?> c = new ArrayList<String>();
//编译错误,由于类型实参为?,此时add(?)无法实现
c.add(new Object());
//null为所有引用类型实例,所以可以
c.add(null);
//类型不确定但一定是Object型,所以可以get
c.get();
           
  1. list<?>与List区别:List会导致编译报错,且什么类型都可以add进该list,容易导致运行时异常
9.3.3 设定类型通配符的上限与下线
//只有List<Shape的子类>,例:List<Circle>,List<Rectangle>的集合可以传入,相当于设置了类型上限为Shape
public void drawAll(List<? extends Shape> shapes){
	
}
//只有List<Shape的父类>类型的集合可以传入,相当于设置了类型下限为Shape
public void drawAll(List<? super Shape> shapes){
	
}
List<? extends Object> list = new ArrayList();
//插入什么都会报错,可以理解为,该list中需放入一个Object子类,但不一定是哪个子类,那么你放入的元素必须一定可以转为该子类,这样的元素类型是不存在的,除非放入null,因为null是所有类的引用。
//list.add(只能null);
List<? super String> list = new ArrayList();
//只能插入String的子类型对象,可以理解为,该list中必须插入一个String的父类对象,那么插入的东西,必须可以转换成任意String父类,而子类转父类为自动转型,因此只要传入String的子类型即可
//list.add(必须为String子类型);
           
9.3.4 设定类型形参的上限与下线
  1. 这和通配符上下限的设定目的不同
  2. 只能有一个类上限,但可以有多个接口上限
//与类同时继承父类、实现接口类似,为类型形参指定多个上限时,所有接口上限必须位于类上限之后
public class Apple<T extends Number & java.io.Serializable>{
    
}
//无法设定类型形参的下线
/*public class Apple<T super Number>{
    
}*/
...
//这样设定好后,如下代码不允许执行,因为String不是Number子类,也没实现Serializable接口,可以看出与通配符作用不同
//new Apple<String>();
           

9.4 泛型方法

9.4.1 定义泛型方法:写在返回值类型之前
  1. 定义泛型方法实际上就是指泛型在整个方法中生效,由于静态方法由于属于类,因此其中不能存在泛型类中定义的类型形参,但可以针对该方法增加类型形参
//1.现需定义一个方法,将Object数组中所有元素放入Collection集合,但此方法功能有限,如果想传入Collection<String>编译无法通过,如果形参列表改成Collection<? extends Object> c也不行,因为这样无法用add插入集合
static void from ArrayToCollection(Object[] a,Collection<Object> c){
    for(Object o:a){
        c.add(o);
    }
}
//2.于是引入泛型方法,由于类中没有声明T,所以在方法返回值前,方法修饰符后进行声明"<T>"
//例1
static <T> void fromArrayToCollection(T[] a,Collection<T> c){
    
}
//例2
static <T> void test(Collection<T> from,Collection<T> to){
    
}
//3.泛型方法的使用
//方法中的类型形参无需显示传入实际类型参数,编译器会根据方法的实参推断出类型形参的值
//例1:系统判断T为Object可以满足条件
fromArrayToCollection(new String[5],new ArrayList<Object>());
//例2:系统发现无论T为什么,方法都不正确,所以会编译报错,需要改造该方法
test(new ArrayList<String>(),new ArrayList<Object>());
//改造后方法
static <T> void test(Collection<? extends T> from,Collection<T> to){
    
}
           
9.4.2 泛型方法与类型通配符的区别
  1. 大多数情况可以通过泛型方法来替代类型通配符
//但对于如下两条,第一个方法中无法再调用c的add方法
boolean containsAll(Collection<?> c);
<T> boolean containsAll(Collection<T> c);
           
  1. 如果类型参数被用来表示方法的一个或多个参数之间类型依赖关系,或方法返回值与参数间类型依赖关系,应使用泛型方法
public static <T> void copy(List<T> dest,List<? extends T> src){
    
}
           
  1. 写泛型方法时,类型形参必须显示写出,而使用类型通配符时不用如此麻烦
9.4.3 定义泛型构造器
  1. 定义构造器实际上就是指泛型在整个构造器中生效
//1.没有返回值,所以泛型声明直接在public/protected/private之后
public <T> Foo(T){
    
}
//2.T参数为String
new Foo("123");
//人为限制T为String,则构造器中参数必须为String型
new <String> Foo("123");
//new <String> Foo(123);编译错误
//3.如果调用构造器时显式指定了构造器的类型实参,则不可以使用菱形语法
package mytest.javaBase;

public class MyClass <E>{
	public <T> MyClass(T t){
		
	}
	public static void main(String[] args) {
		MyClass<String> mc1 = new MyClass<>(5);
		MyClass<String> mc2 = new <Integer>MyClass<String>(5);
		//编译报错
		//MyClass<String> mc3 = new <Integer>MyClass<>(5);
	}
}

           
9.4.4 泛型方法与方法重载
//java8以前这两个方法可以编译过去,但调用时会报错,系统无法正确选取方法
public static<T> void copy(Collection<T> dest,Collection <? extends T> src){
		System.out.println("方法1");
	}
	public static<T> T copy(Collection<? super T> dest,Collection<T> src){
		System.out.println("方法1");
		return null;
	}
           

9.5 擦除与转换

class Apple<T extends Number>{
    public static void main(String[] args) {
        //如果没有为泛型类指定一个类型实参,则该类型实参被称为原始类型,默认为声明该类型形参时指定的第一个上线此时T默认为Number
		Apple a = new Apple();
	}
}
           
List<Integer> liInt = new ArrayList<Integer>();
liInt.add(1);
List list = liInt;
//1.当把一个具体泛型信息的对象(liInt指向的对象)赋值给另外一个没有泛型信息的变量(list)时,会发生擦除,list.get(0)返回Object型,相当于将其原泛型Integer遗忘,此处Object无法自动转为Integer,编译报错,需要强制转换
//Integer i = list.get(0);
//打印class java.lang.Integer,运行时候对象的类型没变,擦除只是发生在编译阶段  
System.out.println(list.get(0).getClass());
//2.将没有泛型信息的对象(list所指向的对象)赋给有泛型信息的引用(liStr),实际运行时会发生转换,从对象的实际类型(list中元素实际为Integer)转换成泛型对应的类型(liStr定义的为String),编译正确
List<String> liStr = list;
//不会报错
liStr.get(0);
//执行时报错,因为类型转换不是在get()时发生的,而是在使用liStr.get(0)这个结果处进行的
System.out.println(liStr.get(0));
//泛型只作用于代码编译阶段,在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦除,即成功编译后的class文件是不包含任何泛型信息的,泛型信息不会进入到运行时阶段,但实际上编译时,会查看liStr中元素类型,并在其get方法的返回值前加转换,即liStr.get(0)被改为(String)liStr.get(0)
           

9.6 泛型与数组

可以声明元素类型包含类型变量或类型实参的数组,但不可以创建这种数组对象

//正确
List<String>[] array;
//编译报错
array = new ArrayList<String>[10];