泛型程序设计

泛型,即“参数化类型”,将原来的具体类型参数化。在不创建新类型的情况下,通过泛型指定不同的类型形参,来控制实际传入实参的具体类型。换句话说,就是在使用和调用时传入具体的类型。

为什么使用泛型?

  • 能够对类型进行限定(比如集合)
  • 将运行期错误提前到编译期错误
  • 获取明确的限定类型时无需进行强制类型转化
  • 具有良好的可读性和安全性

泛型类

泛型类的定义

一个简单的泛型类,和普通类的区别是,类名后添加了<T>一个泛型标识,“T"类型参数(类型形参),传入的是类型实参,当然也可以用其他字母标识,但是"<>"左右尖括号必须存在。

public class Generic<T> {
    private T data;

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
    public static void main(String[] args) {
        /* 1.没有传入具体的类型参数,可以存放任何类型的数据【下图】
         * 本质:虚拟机会对泛型代码进行类型擦除,类型擦除后Generic<T>会变为
         * Generic(原始类型),后面会讲到,无限定类型的域用Object代替,即擦除
         * 后T data->Object data,这也是为什么没有传入具体类型,却能存放多种
         * 类型的原因。
         */
        Generic generic = new Generic();
        generic.setData(1);
        generic.setData("String");
        generic.setData(new Object());
        Generic genericStr = new Generic<String>();//CORRECT [1]
        genericStr.setData("hello");
        genericStr.setData(1);
        /*
        * 2.参数化类型(初始化时传入具体的类型参数)【下图】
        * 本质:由编译器进行类型转化的处理,无需人为干预。
        * 当调用泛型方法时,编译器自动在调用前后插入相应的
        * 强转和调用语句。
         */
        Generic<String> genericString=new Generic<>();
        genericString.setData("hello");

        //Generic<int> genericInt=new Generic<>(); ERROR [2]
        Generic<Integer> genericInt=new Generic<>();
    }
}

在这里插入图片描述

【1】原始类型可以接受任何参数化类型,即Generic generic == new Generic<String>()如[1]处所示。

【2】泛型的类型参数只能是类类型,不能是基本类型。如[2]处,但可以使用期包装类型。

【3】泛型参数命名规范如下:

泛型命名规范:国际惯例,类型参数的命名采用单个大写字母。

常见的泛型命名有:

  • T - Type:第一类通用类型参数。
  • S - Type:第二类通用类型参数。
  • U - Type:第三类通用类型参数。
  • V - Type:第四类通用类型参数。
  • E - Element:主要用于Java集合(Collections)框架使用。
  • K - Key
  • V - Value
  • N - Number
  • R - Result

泛型接口

泛型接口的定义

和泛型类定义相似,如下:

public interface GenericInterface<T> {
    T getData();
    T setData(T data);
}

类接口的实现

类接口的实现存在三种形式,第一种无泛型,域类型用Object定义;第二种有泛型,域变量用泛型参数定义;第三种传递具体的类型参数,域变量的类型为具体的类型。

public class GenericInterfaceImpl implements GenericInterface {
    @Override
    public Object getData() {
        return null;
    }

    @Override
    public Object setData(Object data) {
        return null;
    }
}
/*
* 实现类的类型参数也需要声明,否则编译器会报错
*/
class GenericInterfaceImpT<T> implements GenericInterface<T> {

    @Override
    public T getData() {
        return null;
    }

    @Override
    public T setData(T data) {
        return null;
    }
}
/*
* 传入具体的类型实参
*/
class GenericInterfaceImplStr implements GenericInterface<String> {

    @Override
    public String getData() {
        return null;
    }

    @Override
    public String setData(String data) {
        return null;
    }
}

泛型方法

泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型

1、泛型方法的定义:类型变量放在修饰符前,返回类型的后面

# 修饰符 <T> 返回值 方法名(...);
* 示例
class ArrayAlg {
	/*
	*修饰符与返回值(T)中间的<T>标识此方法为泛型方法
	*<T>表明该方法可以使用泛型类型T,可以在形参或者方法体中声明变量
	/
	public static <T> T getMiddle(T...a) {
		return a[a.length/2];
	}
}

2、调用泛型方法,在方法名前的尖括号放入具体的类型:

String middle = ArrayAlg.<String>getMiddle("John","Q","Public");
// 类型推断:类型参数可以省略 等同于
String middle = ArrayAlg.getMiddle("John","Q","Public");

使用泛型方法时,通常不需要指定参数类型,因为编译器会找出这些类型。 这称为 类型参数推断

3、泛型方法辨别真假

/*
 * 泛型类
 * 注意:下面为了介绍,不把泛型方法归类到成员方法里,泛型方法是特指!
 */
public class GenericMethod<T> {
    private T data;

    /*
    * [成员方法:非泛型方法]
    * T getData()和setData(T data) 都不是泛型方法
    * 他们只是类中的成员方法,只不过是方法的返回值类型
    * 和方法的形参类型是用的泛型类上的T所声明的。
     */
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }

    /*
    * [泛型方法]
    * 泛型参数可以有多个,这里的T和泛型类上的T无任何关联,但是但是
    * 它和泛型类上的参数类型变量相同,这时候idea会给予一个rename提示
     */
    public <T,S> T genericMethod(S...a) {
        return null;
    }

     /*
    * [泛型方法]
    * 使用泛型类上的泛型变量
    * 这时候的T就和泛型类的类型相关了
     */
    public <V> T genericMethod$1(T a, V b) {
        return null;
    }
    
    /*
     * [静态方法]
     * 静态方法不能使用泛型类上的类型参数
     * 如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法
     */
    //public static T getDataStatic(T e) { } //ERROR [1]
    
    /*
    * [泛型方法]
    * 静态泛型方法
     */
    public static <E> E genericMethodS(E e) {
        return e;
    }
}
/*
*普通类中的泛型方法
*/
class OrdinaryClass {
    public <T> void sayHello() {
        T a;
        //...
    }
}

下面以苹果为例:

public class GenericClass<T> {

    //成员方法,形参类型与泛型类的类型参数相关
    public void print$1(T a) {
        System.out.println(a.toString());
    }

    //下面三个都为泛型方法
    //--begin
    public <T> void print$2(T a) {
        System.out.println(a.toString());
    }

    public <S> void print$3(S a) {
        System.out.println(a.toString());
    }

    public static <T> void print$4(T a) {
        System.out.println(a.toString());
    }
    //--end
    public static void main(String[] args) {
        Apple apple = new Apple();
        MacBook macBook = new MacBook();
        HongFuShi hongFuShi = new HongFuShi();
        // 泛型类在初始化时限定了参数类型,成员方法中若使用泛型参数将会受限
        GenericClass<Apple> genericCls = new GenericClass<>();
        
        genericCls.print$1(apple);
        //MacBook是apple的一个子类
        genericCls.print$1(macBook);// OK 
        
        //由于初始化指定了泛型类型,print$1形参中的参数类型和泛型类的类型参数相关联
        //所以,只能打印Apple及其子类
        //genericCls.print$1(hongFuShi); ERROR

        //泛型方法中的泛型变量类型与泛型类中的泛型参数没有任何关联
        //所以说下面都能正常执行
        genericCls.print$2(apple);
        genericCls.<MacBook>print$2(macBook);//类型参数可以省略		[2]
        genericCls.print$2(hongFuShi);

        GenericClass.print$4(hongFuShi);

    }
}
class Apple {
    @Override
    public String toString() {
        return "Apple,Steve Jobs";
    }
}
class MacBook extends Apple {
    @Override
    public String toString() {
        return "MacBook";
    }
}
class HongFuShi{
    @Override
    public String toString() {
        return "HongFushi";
    }
}

小结:

【1】泛型方法的标识:方法修饰符后返回值之前有"<...>"的声明。(判断是否为泛型方法)

【2】泛型方法可以定义在普通类中,也可以定义在泛型类中。

【3】静态方法不能使用泛型类上的类型参数,如[1]处

【4】成员方法中使用的参数类型和泛型类中的声明的类型参数有关联。

【5】泛型类中的参数类型与泛型方法中的参数类型的关联:泛型方法可以使用泛型类上的参数类型,这时候就与泛型类上的参数相关联;如果泛型方法中声明了与泛型类上相同的参数类型,那么优先使用泛型方法上的参数类型,这时候idea会给予一个rename提示。

【6】泛型方法的使用过程中,无需对类型进行声明,它可以根据传入的参数,自动判断。[2]

【7】方法中泛型参数不是凭空而来的,要么来自于泛型类上所定义的参数类型,要么来自于泛型方法中定义的参数类型。

泛型方法能独立于类而发生变化,所以说在使用原则上,在能达到目的的情况下,尽量使用泛型方法。即,如果使用泛型方法可以取代将整个类泛化,那么应该有限采用泛型方法。

类型变量的限定

对于类型变量没有限定的泛型类或方法, 它是默认继承自Object,当没有传入具体类型时,它有的能力只有Object类中的几个默认方法实现,原因就是类型擦除

如果某个类实现Comparable接口中的compareTo方法,我们就可以通过compareTo比较两个值的大小。比如我们要计算数组中的最小元素:

public static void main(String[] args) {
        // 传入 4 , 2 , 自动装箱成Integer类
        int r = max(4, 2);
    }
static <T> T min(T[] a) {
    if (a == null || a.length == 0)
        return null;
    T smallest = a[0];
    for (int i = 1; i < a.length; i++) 
        if (smallest.compareTo(a[i]) > 0)// ERROR,因为编译器不知道T声明的变量是什么类型
            smallest = a[i];
    return smallest;
}

如果没有对类型进行限定,它默认只有Object能力,变量smallest类型为T,编译器不知道他是否是实现了Comparable接口(是否是Comparable类型),所以可以通过将T限定为实现了Comparable接口的类,就可以解决这一问题。

对 类型参数进行限定,让它能够默认拥有一些类的"能力"

static <T extends Comparable> T min(T[] a){...}

类型变量限定格式:<T extends BoundingType>

【1】T应该是绑定类型的子类型(subtype)。T和绑定类型可以是类,也可以是接口。

【2】一个类型变量或通配符可以有多个限定,限定类型用”&“分隔,类型变量用逗号分隔。例如:

<T extends Comparable & Serializable>;
<T,E extends Comparable & Serializable>;

【3】在 Java 的继承中, 可以拥有多个接口超类型, 但限定中至多有一个类。 如果用 一个类作为限定, 它必须是限定列表中的第一个。

<T extends ArrayList & LinkedList>;//ERROR,限定中至多有一个类
<T extends Comparable & LinkedList>;//ERROR,必须是限定列表中的第一个
<T extends ArrayList & Comparable>;//CORRECT

【4】类型限定不仅可以在泛型方法上,也可以在泛型类上,类型限定必须与泛型的声明在一起

public <T extends Number> T compare(Generic<T extends Comparable> a) {..}//ERROR
public <T extends Number> T compare(Generic<T> a) {..}//ERROR

类型擦除

虚拟机没有泛型类型对象—所有对象都属于普通类

类型擦除:无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型的名字就是删去类型参数后的泛型类型名。擦除(erased)类型变量,并替换为限定类型,如果没有给定限定就用Object替换

例如,Holder的原始类型如下:

public class Holder {
    private Object holder;

    public Holder(Object holder) {
        this.holder = holder;
    }

    public Object getHolder() {
        return holder;
    }
}
// 类型擦除也会出现在泛型方法中。
public static <T extends Comparable> T min(T[] a);
// 擦除类型之后,只剩下
public static Comparable min(Comparable[] a);

Java泛型 转换的事实

  • 虚拟机中没有泛型,只有普通的类和方法。
  • 所有的类型参数都用它们的限定类型替换。
  • 桥方法被合成来保持多态。
  • 为保持类型安全性,必要时插入强制类型转换。

泛型的约束与局限

(1)不能用基本类型实例化类型参数

其原因是当类型擦除后,Object类型的域不能存储基本类型的值。

(2)所有的类型查询只产生原始类型

List<Number> numbers = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(numbers.getClass() == integers.getClass());//true
if (integers instanceof List){//true
    System.out.println(true);
}
/*if (integers instanceof List<Integer>){//compile error
            System.out.println(true);
        }*/

(3)不能创建一个确切的泛型类型的数组

//List<Integer>[] lists = new ArrayList<Integer>[10];//ERROR
//可以声明原始类型创建数组,但是会得到一个警告
//可以通过@SuppressWarnings("unchecked")去除
List<Integer>[] list = new ArrayList[10];
//使用通配符创建泛型数组也是可以的,但是需要强制转换
List<Integer>[] listWildcard = (List<Integer>[])new ArrayList<?>[1];

下面采用通配符的方式是被允许的:数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。

List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.    
Object o = lsa;    
Object[] oa = (Object[]) o;    
List<Integer> li = new ArrayList<Integer>();    
li.add(new Integer(3));    
oa[1] = li; // Correct.    
Integer i = (Integer) lsa[1].get(0); // OK 

(4)不能实例化类型变量

static <T> Object init(Class<T> cls) throws Exception {
    //T a = new T(); // ERROR
    // 注意不存在T.class.newInstance();
    T t = cls.newInstance();//Class本身也是一个泛型类
    return t;
}

(5)不能构造泛型数组

(6)泛型类的静态上下文中类型变量无效

(7)不能抛出或捕获泛型类的实例

不变 协变 逆变

首先看一段代码

Number[] n = new Integer[10];
ArrayList<Number> list = new ArrayList<Integer>(); // ERROR type mismatch

为什么Number类型的数组可以由Integer实例化,而ArrayList<Number>却不能被ArrayList<Integer>实例化呢?这就涉及到将要介绍的主题。

不变协变逆变的定义:

逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果AA、BB表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类);

  • f(⋅)是逆变(contravariant)的,当A≤B时有f(B)≤f(A)成立;
  • f(⋅)是协变(covariant)的,当A≤B时有f(A)≤f(B)成立;
  • f(⋅)是不变(invariant)的,当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。

容易证明数组是协变的即Number[] n = new Integer[10];泛型是不变的,即使它的类型参数存在继承关系,但是整个泛型之间没有继承关系 : ArrayList<Number> list = new ArrayList<Integer>();

泛型类型的继承规则

1. 泛型参数是继承关系的泛型类之间是没有任何继承关系的

在java中,Number是所有数值类型的父类,任何基本类型的包装类型都继承于它。

ArrayList<Integer> integers = new ArrayList<>();
ArrayList<Number> numbers = new ArrayList<>();
numbers = integers;     // ERROR,就上上面刚刚提到的泛型是不变的

2. 泛型类可以扩展或实现其他的泛型类。这一点和普通类没有声明区别。比如ArrayList<T>类实现List<T>接口。这意味着,一个ArrayList<Integer>可以转换为一个List<Integer>(父类指向了子类的引用),但是,一个ArrayList<Integer>不是一个ArrayList<Number>List<Number>(泛型参数继承与类无关)。

通配符

Java中引入通配符?来实现逆变和协变,通过通配符之前的操作也能赋值成功,如下所示:

List<? extends Number> number = new ArrayList<Integer>();// CORRECT
List<? super Number> list = new ArrayList<Object>();// CORRECT 逆变的代表

使用通配符的子类型关系

ArrayList<Integer>ArrayList<? extends Number>的一个子类型。

通配符的分类

  • ? extends T(上边界通配符 upper bounded wildcard)

    "?"是继承自T的任意子类型,表示一种约束关系。即泛型类型的范围不能超过T。

    可以取元素,不能添加元素

  • ? (无限定通配符)

  • ? super T(下边界通配符 lower bounded wildcard)

    可以取元素,但是取出的元素是Object,可以添加元素,添加的元素,必须是T类或者其子类

记忆:上不存,下不取

示例:类型上边界通配符为什么只能添加?

ArrayList<Integer> integers = new ArrayList<>();
ArrayList<Object> objects = new ArrayList<>();
ArrayList<Number> nums = new ArrayList<>();
/*
 * 1. 类型上边界通配符
 * 对变量numbers赋值,引用的集合类型参数只能是Number或者其子类。
 */
ArrayList<? extends Number> numbers;
numbers = nums;
numbers = integers;
//引用的类对象类型超过了泛型类型的上边界
//numbers = objects;    ERROR
integers.add(1);// 正常添加元素
//numbers.add(1); ERROR numbers只能读取,不能添加[1]
//但是可以添加null
numbers.add(null);
Number number = numbers.get(0);//可以读取元素

为什么只能读取,不能添加?[1]
? extends T 表示类型的上界,类型参数是T的子类,那么可以肯定的说,get方法返回的一定是个T(不管是T或者T的子类)编译器是可以确定知道的。但是add方法只知道传入的是个T,至于具体是T的那个子类,不知道。
转化到本例来说就是:
理解方式一
? extends Number指定类型参数必须是Number的子类,get方法返回的一定是Number
编译器确定,但是对于ArrayList的add方法为来说add(E e)->add(? extends Number e);
调用add函数不能够确定传入add的是Number的哪个子类型。编译器不确定。
理解方式二
List是线性表吧【线性结构的存储】,线性表是n个具有相同类型的数据元素的有限序列。假若number能够add,因为? extends Number泛型通配符,可以添加Number的任何子类型,那么numbers在get时,极有可能引发ClassCastException,比如numbers引用了<Integer>,但是在索引0处却add了float类型的数据,取出的时候如果numbers.get(0).intValue();就会抛出异常。并且这也违背了线性表中特性,只能存放单一类型的元素。

/*
* 2. 类型下边界通配符
* numbersSuper所能引用的变量必须是Number或者其父类
 */

ArrayList<? super Number> numbersSuper;
numbersSuper = objects;// 逆变
numbersSuper = nums;
//限定了通配符的下界,类型最低是Number,Integer达不到
//下界,类型不匹配
//numbersSuper = integers;  ERROR
numbersSuper.add(1);	[2]
numbersSuper.add(2.0f); [3]
//numbersSuper.add(new Object()) ERROR
Object object = numbersSuper.get(0);
System.out.println(object);

? super T 表示类型的下界,类型参数是T的超类(包括T本身), 那么可以肯定的说,get方法返回的一定是个T的超类,那么到底是哪个超类?不知道,但是可以肯定的说,Object一定是它的超类,所以get方法返回Object。 编译器是可以确定知道的。对于add方法来说,编译器不知道它需要的确切类型,但是T和T的子类可以安全的转型为T。

为什么? super Number就可以add了呢?[2、3]

首先要明确的一点是,add的时候只能是Number[T]及其它的子类,不要和numberSuper只能引用Number的父类所混淆了。正因为numberSuper引用了<Object>,那么numberSuper在add的时候类型确定,都可以看作是Object类型,即Number的子类Integer和Float也是其Object的子类。但是相对于? extends T就add就不能调用,numbers如果限定了<Integer>,还是那句话,假若能放的话,number存放float类型的数据,取值时极易引发类型转化异常。

泛型方法和类型通配符

类型通配符所能解决的泛型方法一定也能解决

# 类型通配符
`public void func(List<? extends E> list);`
# 泛型方法
`public <T extends E> void func(List<T> list);`
* 上面两种方法可以达到同样的效果,两者的主要区别还是
i. 泛型对象是只读的,不可修改,因为?类型是不确定的,可以代表范围内任意类型;
ii. 而泛型方法中的泛型参数对象是可修改的,因为类型参数T是确定的(在调用方法时确定),因为T可以用范围内任意类型指定;

泛型方法和类型通配符(上界和下界)我们应该如何选择

(1)泛型方法和通配符之间

修改最好使用泛型方法,在多个参数、返回值之间存在类型依赖关系就应该使用泛型方法,否则就应该使用通配符。

(2)什么时候用extends什么时候用super

PECS: producer-extends, consumer-super.

—《Effective Java》

  • 要从泛型类取数据时,用extends
  • 要往泛型类写数据时,用super
posted @ 2020-09-21 18:31  Wonkey  阅读(138)  评论(0编辑  收藏  举报