八、泛型

目录

8.1 为什么要使用泛型程序设计

8.1.1 类型参数的好处

8.2 泛型类

8.3 泛型方法

8.4 类型变量的限定

8.5 泛型代码和虚拟机

8.5.1 类型擦除

8.5.2 翻译泛型表达式

8.5.3 翻译泛型方法

8.6 约束与局限性

8.6.1 不能用基本类型实例化类型参数

8.6.2 运行时类型查询只适用于原始类型

8.6.3 不能创建确切类型的泛型数组(通配符可以)

8.6.4 不能实例化类型变量

8.6.5 不能构造泛型数组

8.6.6 不能重载参数类型为相同原始类型的方法

8.6.7 不能抛出或捕获泛型类的实例

8.6.8 不允许静态属性是参数化类型,并且类中的静态方法,如果要使用泛型能力,必须将该方法声明为泛型方法

8.7 泛型类型的继承规则

8.8 通配符类型

8.8.1 泛型的上下边界

8.8.2 PECS原则


8.1 为什么要使用泛型程序设计

泛型是JavaSE1.5的新特效,泛型的本质是参数化类型,就是说所操作的数据类型被指定为一个参数,这种参数可以用在类、接口和方法中创建,分别称为泛型类、泛型接口、泛型方法。引用泛型的好处是安全简单。

泛型机制将类型转换时的类型检查从运行时提前到了编译时,使用泛型编写的代码比使用Object时强制类型转换的机制具有更好的可读性和安全性。

8.1.1 类型参数的好处

试想在没有泛型设计之前,要想使ArrayList对任意对象都通用,需要使用继承实现,即只维护一个Object引用的数组:

public class ArrayList { // before generic classes

    private Object[] elementData; // jdk8中依然是定义一个Object数组,强制类型转换交由编译器去完成

    public Object get(int i) { . . .} 

    public void add(Object o) { . . . }

}

这种写法会存在两个问题:

  • 取值时必须进行强制类型转换

  • 可以添加任意类型的对象

没有进行类型检查,在运行期间就可能报错。

泛型提供了解决方案:类型参数。

ArrayList<String> files = new ArrayList<String>();

这使得代码具有更好的可读性。人们一看就知道这个数组列表中包含的是 String 对象。

编译器会依据声明的类型参数,进行类型检查,使得程序具有更好的可读性和安全性

  • 使用泛型能写出更加灵活通用的代码

  • 泛型将代码安全性检查提前到编译期

  • 泛型能够省去类型强制转换(编译器自动插入强制类型转换)

 


 

8.2 泛型类

一个泛型类(generic class) 就是具有一个或多个类型变量的类。

/**
* 定义一个简单的泛型类
* @param <T>
*/
public class Pair<T> {

    private T first;

    private T second;

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;

    }

    @Override
    public String toString() {
        return "Pair{" +
                "first=" + first +
                ", second=" + second +
                '}';
    }
}

 


 

8.3 泛型方法

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

/**
* 泛型方法
* 获取中间元素
* @param a
* @param <T> 放在修饰符和返回值类型中间
* @return
*/
public static <T> T getMiddleElement(T... a) {
    if (a == null || a.length <= 0) {
        return null;
    }

    return a[a.length / 2];
}

类型变量在方法修饰符的后面,返回值的前面。

 


 

8.4 类型变量的限定

/**
* 获取数组最值
* @param a
* @return
*/

public static <T extends Comparable> Pair<T> minmax(T[] a) {

    if (a == null || a.length <= 0) {
        return new Pair(null, null);
    }

    // 不改变入参

    T[] temp = a.clone();

    List<T> aList = Arrays.asList(temp);

    aList = aList.stream().filter(obj -> obj != null).collect(Collectors.toList());

    if (org.springframework.util.CollectionUtils.isEmpty(aList)) {

        return new Pair<>(null, null);

    }

    T min = aList.get(0);

    T max = aList.get(0);

    for (T str : aList) {

        if (str.compareTo(min) < 0) {
            min = str;
        }

        if (str.compareTo(max) > 0) {
            max = str;
        }

    }
    return new Pair<>(min, max);

}

T extends Comparable & Serializable

Java 的继承中,可以根据需要拥有多个接口超类型但限定中至多有一个类 如果用一个类作为限定它必须是限定列表中的第一个

 


 

8.5 泛型代码和虚拟机

8.5.1 类型擦除

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

举个例子:

class Interval<T extends Serializable & Comparable>

如果这样做,原始类型用Serializable替换T, 而编译器在必要时要将 Comparable 插入强制类型转换。为了提高效率,应该将标签(tagging) 接口 (即没有方法的接口)放在边界列表的末尾。

  • 将泛型中所有参数化类型替换为泛型边界,如果参数化类型是无界的,则替换为 Object 类型。字节码中没有任何泛型的相关信息。

  • 为了类型安全,在必要时插入类型转换代码。

  • 生成桥接方法来保持泛型类型的多态性。 ——>  桥接方法什么鬼?

8.5.2 翻译泛型表达式

当程序调用泛型方法时,如果擦除返回类型,编译器自动插入强制类型转换。——>  类型被擦除了,在必要的时候当然要强制类型转换

Pair<Employee> buddies = . .

Employee buddy = buddies.getFirst(); // 类型擦除之后,返回值是Object,编译器在字节码中自动插人 Employee 的强制类型转换。

8.5.3 翻译泛型方法

一个日期区间是一对 LocalDate 对象,并且需要覆盖(Override)这个方法来确保第二个值永远不小于第一个值。

class DateInterval extends Pair<LocalDate> {
    public setSecond(LocalDate second) {
        if (second.compareTo(getFirst) >= 0) {
            super.setSecond(second);
        }
    }
    ...
}

这个类擦除后变成:

class DateInterval extends Pair { // after erasure
    public void setSecond(LocalDate second) { . . . }
    ...
}

注意:实际存在另一个从 Pair 继承的 setSecond 方法,不满足重写规则了,虚拟机该怎么办?

public void setSecond(Object second) {...}

再考虑下面这个语句序列:

DateInterval interval = new DateInterval(...);

Pair<LocalDate> pair = interval; // OK assignment to superclass 

pair.setSecond(aDate);

这里,希望对 setSecond 的调用具有多态性,并调用最合适的那个方法。由于 pair 引用 Datelnterval 对象,所以应该调用 Datelnterval.setSecond。问题在于类型擦除与多态发生了冲突。要解决这个问题,就需要编译器在 Datelnterval 类中生成一个桥方法(bridge method): ——>  编译器辛苦了。。生成字节码

public void setSecond(Object second) { 
    setSecond((Date) second); 
}

 

桥方法可能会变得十分奇怪。假设 Datelnterval 方法也覆盖了 getSecond 方法:

public LocalDate getSecond() { return (Date) super.getSecond().clone(); }

在 Datelnterval 类中,有两个 getSecond 方,能否满足重写规则?

LocalDate getSecond() // defined in Datelnterval

Object getSecond() // overrides the method defined in Pair to call the first method

在虚拟机中,用参数类型和返回类型确定一个方法。因此,编译器可能产生两个仅返回类型不同的方法字节码,虚拟机能够正确地处理这一情况。—> 虚拟机也辛苦了。。满足重写的规则:返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类

 

总之需要记住有关 Java 泛型转换的事实

  • 虚拟机中没有泛型,只有普通的类和方法。

  • 所有的类型参数都用它们的限定类型替换。

  • 桥方法被合成来保持多态。

  • 为保持类型安全性,必要时插人强制类型转换。

 


 

8.6 约束与局限性

8.6.1 不能用基本类型实例化类型参数

 

8.6.2 运行时类型查询只适用于原始类型

虚拟机中的对象总有一个特定的非泛型类型

if (a instanceof Pair<String>) // Error

if (a instanceof Pair<T>) // Error

if (a instanceof Pair) // 实际上仅仅测试 a 是否是任意类型的一个 Pair

Pair<String> p = (Pair<String>) a; // Warning-can only test that a is a Pair

为提醒这一风险,试图查询一个对象是否属于某个泛型类型时,倘若使用 instanceof 会得到一个编译器错误,如果使用强制类型转换会得到一个警告。

同样的道理,getClass 方法总是返回原始类型。例如:

Pair<String> stringPair = . .

Pair<Employee> employeePair = . .

if (stringPair.getClass() == employeePair.getClass()) // they are equal

 

8.6.3 不能创建确切类型的泛型数组(通配符可以)

Pair<String>[] table = new Pair<String>[10]; // Error

Generic<?>[] generics = new Generic<?>[2]; // OK

 

8.6.4 不能实例化类型变量

不能使用像 new T(...) ,newT[...] 或 T.class 这样的表达式中的类型变量。

public Pair() { first = new T(); second = new T(); } // Error

 

8.6.5 不能构造泛型数组

public static <T extends Comparable> T[] minmax(T[] a) { T[] a = new T[2]; …} //Error

就像不能实例化一个泛型实例一样, 也不能实例化数组。 不过原因有所不同, 毕竟数组会填充 null 值,构造时看上去是安全的。不过, 数组本身也有类型, 用来监控存储在虚拟机中的数组。这个类型会被擦除。

 

8.6.6 不能重载参数类型为相同原始类型的方法

public class Example {
    // 不能有两个重载方法,当他们的方法签名在类型擦除后是一样的。
    public void print(List<String> list) {}
    public void print(List<Integer> list) {}
}

 

8.6.7 不能抛出或捕获泛型类的实例

既不能抛出也不能捕获泛型类对象。 实际上,甚至泛型类扩展 Throwable 都是不合法的。

public class Problem<T> extends Exception { /* . . . */ } // Error can't extend Throwable

catch 子句中不能使用类型变量。 例如, 以下方法将不能编译:

public static <T extends Throwable〉 void doWork(Class<T> t) {
    try {
        do work
    } catch (T e) {...} // Error can't catch type variable 

}

 

8.6.8 不允许静态属性是参数化类型,并且类中的静态方法,如果要使用泛型能力,必须将该方法声明为泛型方法

 


 

8.7 泛型类型的继承规则

// Pair<Manager> 是 Pair<Employee> 的一个子类吗?  不是!

Pair<Manager> a = ...;

Pair<Employee> result = a; // Error

必须注意泛型与 Java 数组之间的重要区别。 可以将一个 Manager[]数组賦给一个 类型为 Employee[] 的变量:

Manager[] managerBuddies = { ceo, cfo };

Employee[] employeeBuddies = managerBuddies; // OK

然而数组带有特别的保护。如果试图将一个低级别的雇员存储到 employeeBuddies[0] ,虚拟机将会抛出 ArrayStoreException 异常。

最后,泛型类可以扩展或实现其他的泛型类。就这一点而言,与普通的类没有什么区别。例如,ArrayList<T> 类实现 List<T> 接口。这意味着,一个 ArrayList<Manager> 可以被转换为一个 List< Manager>。但是,如前面所见,一个 ArrayList< Manager> 不是一个 ArrayList <Employee> 或 List<Employee>。

 


 

8.8 通配符类型

从需求看通配符的使用:

问题1:前面我们说,Pair<Manager> 和 Pair<Employee>没有什么关系,所以,在使用 Pair<Employee>作为形参的方法中,不能使用Pair<Manager>的实例传入。如果现在有需求要将参数类型是Employee及其子类的Pair对象作为方法参数,该如何实现呢?

// 参数类型

Pair<?> s 或者 Pair<? extends Object> s 或者 Pair<? extends Employee> s

问题2:定义一个方法,接收一个任意集合,并打印出集合中的所有元素,如下所示:

public static void printCollection(Collection<?> c) {

    c.size();

    // c.add(1); // 不能调用与类型相关的方法

    for (Object o : c) {
        System.out.println(o);
    }
}

8.8.1 泛型的上下边界

  • 在Java泛型定义时:

用<T>等大写字母标识泛型类型,用于表示未知类型。

用<T extends ClassA & InterfaceB …>等标识有界泛型,用于表示有边界的类型。

如果我们把泛型类的定义改一下:

public class Generic<T extends Number>{

    private T key;

    public Generic(T key) {
        this.key = key;
    }

    public T getKey(){
        return key;
    }
}
//这一行代码也会报错,因为String不是Number的子类

Generic<String> generic1 = new Generic<String>("11111");

再来一个泛型方法的例子:

//在泛型方法中添加上下边界限制的时候,必须在权限声明与返回值之间的<T>上添加上下边界,即在泛型声明的时候添加

//public <T> T showKeyName(Generic<T extends Number> container),编译器会报错:"Unexpected bound"

public <T extends Number> T showKeyName(Generic<T> container){

    System.out.println("container key :" + container.getKey());

    T test = container.getKey();

    return test;

}

 

  • 在Java泛型实例化时:

用<?>标识通配符,用于表示实例化时的类型。

用<? extends 父类型>标识上边界通配符,用于表示实例化时可以确定父类型的类型。

用<? super 子类型>标识下边界通配符,用于表示实例化时可以确定子类型的类型。

 

在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。

为泛型添加上边界,即传入的类型实参必须是指定类型或者该类型的子类型:

// 普通方法
public void showKeyValue1(Generic<? extends Number> obj){
    Log.d("泛型测试","key value is " + obj.getKey());

}


Generic<String> generic1 = new Generic<String>("11111");

Generic<Integer> generic2 = new Generic<Integer>(2222);

Generic<Float> generic3 = new Generic<Float>(2.4f);

Generic<Double> generic4 = new Generic<Double>(2.56);



//这一行代码编译器会提示错误,因为String类型并不是Number类型的子类

//showKeyValue1(generic1);

showKeyValue1(generic2);

showKeyValue1(generic3);

showKeyValue1(generic4);

8.8.2 PECS原则

“producer extends - consumer super” 这啥意思?  (多态)

先举一个应用泛型上下边界的例子:

public static void getOutFruits(List<? extends Fruit> basket){
    for (Fruit fruit : basket) {
        System.out.println(fruit);
        //...do something other
    }
}

public static void main(String[] args) {

    List<Fruit> fruitBasket = new ArrayList<>();
    fruitBasket.add(new Fruit());
    getOutFruits(fruitBasket);


    List<Apple> appleBasket = new ArrayList<>();
    appleBasket.add(new Apple());
    getOutFruits(appleBasket); //编译正确
}

问题出现在下面的例子中:

List<Apple> apples = new ArrayList<>();
apples.add(new Apple());
List<? extends Fruit> basket = apples; //按上一个例子,这个是可行的

// 一定是水果:多态
for (Fruit fruit : basket)
{
    System.out.println(fruit);
}


//basket.add(new Apple()); //编译错误,苹果不是香蕉

//basket.add(new Fruit()); //编译错误,水果不是苹果

篮子里面放的是水果或者是具体的水果,因为这个不确定性,编译器不允许往这个篮子里添加任何东西。编译器可以确定的是,篮子里面是水果(多态),所以允许读操作。

无法添加新元素的原因,是对于List<? extends Number>类型来说,可能是 List<Number>、 List<Integer>或List<Double>等类型,无法确定新元素的类型与集合里的类型一致,所以编译器会提示报错。所以可以将List<? extends Number>类型的列表看作非严格意义上的只读列表。

所以,简单的说,当只想从集合中获取元素,请把这个集合看成生产者,请使用<? extends T>,这就是Producer extends原则,PECS原则中的PE部分。

 

上一个例子里,我们不能往篮子里加水果。现在换一个角度,我们要实现如何往篮子里加水果,而且是不同的水果。这将用到<? super T>通配符泛型。

首先我们扩展一下水果的继承关系,增加苹果的子类型RedApple:

public class Fruit {...}
public class Apple extends Fruit {...}
public class RedApple extends Apple {...}



List<Apple> apples = new ArrayList<>();
apples.add(new Apple());
List<? super Apple> basket = apples; // 这里使用了super



basket.add(new Apple());
basket.add(new RedApple()); // 可以添加Apple及其子类,要添加只能添加子类,实现多态
//basket.add(new Fruit()); //编译错误



Object object = basket.get(0);//正确
//Fruit fruit = basket.get(0);//编译错误,需要强制类型转换
//Apple apple = basket.get(0);//编译错误,列表中存的可能是超类对象,向下转型可能会报错
//RedApple redApple = basket.get(0);//编译错误

篮子里面放的是苹果或者是其超类,因为编译不确定性里面放的是什么,所以不允许读操作。编译器可以确定的是,篮子里面是苹果(多态),所以允许把苹果或者红苹果放进篮子里。

无法读取列表的原因,是对于List<? super Number>类型来说,可能是List<Number>也可能是List<Object>类型,读取列表元素时不能确定元素类型。所以可以将List<? super Number>类型的列表看作只写列表。

因此,在上面的例子中的,我们将数据放进集合List<? super Apple> basket,所以这个篮子是实际上消费元素,例如Apple。简单的说,当你仅仅想增加元素到集合,把这个集合看成消费者,请使用<? super T>。这就是Consumer super原则,PECS原则中的CS部分。

参考:https://blog.51cto.com/flyingcat2013/1616068

总结PECS原则:

  • 如果你只需要从集合中获得类型T(当然包括Object),使用<? extends T>通配符

  • 如果你只需要将类型T(当然包括T的子类)放到集合中, 使用<? super T>通配符

  • 如果你既要获取又要放置元素,则不使用任何通配符。例如List<Apple>

  • 如果只读类型只用到 Object 的方法,即List<? extends Object>,可以用List<?>无界通配符

为什么要PECS原则:

List<Fruit>和List<Apple>之间没有任何继承关系。API的参数想要同时兼容两者,则只能使用PECS原则。这样做提升了API的灵活性。在java集合API中,大量使用了PECS原则,例如java.util.Collections中的集合复制的方法:

public static <T> void copy(List<? super T> dest, List<? extends T> src) {

    int srcSize = src.size();
    if (srcSize > dest.size())
        throw new IndexOutOfBoundsException("Source does not fit in dest");

    if (srcSize < COPY_THRESHOLD ||
        (src instanceof RandomAccess && dest instanceof RandomAccess)) {
        for (int i=0; i<srcSize; i++)
            dest.set(i, src.get(i));
    } else {
        ListIterator<? super T> di=dest.listIterator();
        ListIterator<? extends T> si=src.listIterator();
        for (int i=0; i<srcSize; i++) {
            di.next();
            di.set(si.next());
        }
    }
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值