Java基础之泛型

泛型

看了《Java 核心技术 卷1》泛型以及多篇博客之后,感受颇深,所以写下了这篇心得体会。

1 泛型程序说明

泛型程序意味着编写的代码可以对多种不同类型的对象重用

ArrayList类可以收集任何类的对象,里面的方法可以对String对象生效也可以对File对象生效,这就是泛型程序的强大之处。

2 泛型类

泛型类:就是有一个或多个类型变量的类。

定义一个简单的泛型类:

public class Pair<T> {

    private T first;
    private T second;

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

    public T getFirst() {
        return first;
    }

    public void setFirst(T first) {
        this.first = first;
    }

    public T getSecond() {
        return second;
    }

    public void setSecond(T second) {
        this.second = second;
    }
}

Pair类引入了一个类型变量 T,用<>括起来,放在类名的后面。

一个泛型类可以拥有多个类型变量,例如:

public class Pair<T,U>{}
pulbic class Pair<T,U,I>{}

类型变量在整个类定义中用于指定方法的 返回类型 以及 字段 和 局部变量的类型

2.1 实例化一个泛型类

用具体的类型替换掉类型变量来实例化泛型类型,例如

Pair<String> pair = new Pair<String>("firstStr","secondStr")

3 泛型方法

类型变量T用<>括起来并放在方法修饰符之后(这里的修饰符是public static),方法返回类型之前。例如:

public class ArrayAlg{
    public static <T> T getMiddle(T[] a){
        return a[a.length / 2]
    }
}

泛型方法既可以定义在普通类中也可以定义在泛型类中

4 类型变量的限定

看下面的这个方法

public class ArrayAlg {
    
    public static <T> T getMin(T[] a){
        if (a == null || a.length == 0){
            return null;
        }
        T min = a[0];
        for (int i = 0; i < a.length; i++) {
            if (min.compareTo(a[i]) > 0){ //这行代码会报错
                min = a[i];
            }
        }
        return min;
    }
}

这里有一个问题,如何能保证T这个类型拥有compareTo方法?

可以给T增加一个限定,即:

 public static <T extends Comparable> T getMin(T[] a){
        if (a == null || a.length == 0){
            return null;
        }
        T min = a[0];
        for (int i = 0; i < a.length; i++) {
            if (min.compareTo(a[i]) > 0){ //这行代码不再报错
                min = a[i];
            }
        }
        return min;
    }

区别在于换成了这就意味着类型变量T不能再表示任意类型了,而是必须实现了Comparable接口的任意类型。

类型变量T可以有多个限定类型,例如:

<T extends Comparable & Serializable>

多个限定类型之间用“&”来分隔,但是只能拥有一个类类型限定且类必须在第一个,可以拥有多个接口类型限定。

5 类型擦除

虚拟机没有泛型类型对象—所有对象属于普通类。泛型程序的类型变量会被编译器“擦除”。

无论何时定义个泛型类型,都会自动提供一个相应的原始类型。这个原始类型的名字就是去掉类型参数后的泛型类型名。

类型变量会被擦除,并替换为其限定类型,如果没有限定类型用Object替代

例如上面所定义的泛型类Pair,类型擦除之后的原始类型为:

public class Pair {
    private Object first;
    private Object second;
    public Object getFirst(){return first};
    public void setFirst(Object first){this.first = first}
}

上面定义的泛型方法,类型擦除之后为:

public static Comparable getMin(Comparable[] a)

来看一下这个程序

Pair<String> pair = new Pair<String>("fir","sec");
String fir = pair.getFirst();

类型擦除之后调用Pair.getFirst()方法实际上分为两步:

  • 调用getFirst()方法
  • 将返回的Object强制替换为String类型

5.1 桥方法

假设上面的泛型类Pair有一个子类:

public class DateInterval extends Pair<LocalDate> {
    
    public void setSecond(LocalDate second){// 子类重写了setSecond()
    	...
    }
}

Pair类代码:


public class Pair<T> {

    private T first;
    private T second;
	...
    public void setSecond(T second) {
        ...
    }
    ...
}

根据刚才所说的类型擦除,那么Pair类中的setSecond()会变成:

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

那么在DateInterval类中就会有两个方法:

public void setSecond(Object second){...}//从父类继承的
public void setSecond(LocalDate second){...}// 重写的  

首先,我们在一个类中编写这样的代码是不合法的!但是在虚拟机中,确认一个方法是由参数类型和返回类型来指定的。因此在虚拟机中这是两个方法。

当我们书写下面的代码:

Pair<LocalDate> pair = new DateInterval();// 这是多态,父类引用指向子类实例

Pair中只有一个setSecond(Object second)的方法,虚拟机在pair引用上调用setSecond()方法时,由于这个对象是DateInterval类型,因此将会调用DateInterval.setSecond(Object second)方法,但是DateInterval的setSecond(Object second)方法是一个桥方法,它的方法体是:

public void setSecond(Object second){
    this.setSecond((LocalDate)second)// 会把Object类型的second转为LocalDate类型并调用另一个setSecond(LocalDate second)方法
}

总结

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

6 限制与局限性

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

即不能这样做:

Pair<double>,Pair<int>...

可以使用他们的包装类

Pair<Double>,Pair<Integer>...

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

虚拟机中的对象总有一个特定的非泛型类型。因此所有的类型查询只产生原始类型,例如:

if(a instanceof Pair<String>) //error
if(a instanceof pair<T>)// error
    
Pair<String> strPair = ...;
Pair<Double> douPair = ...;
if(strPair.getClass() == douPair.getClass())// true

调用getClass()都是返回Pair.class

6.3 不能创建参数化类型的数组

即不能new Pair[10]…

6.4 不能实例化类型变量

即不能T t = new T();

6.5 不能构造泛型数组

即不能T[] t = new T[2]

6.6 泛型类的静态上下文中类型变量无效

下面的做法是错误的

public class Singleton<T> {
    public static T singleInstance;// error
    public static T getSingleInstance(){return singleInstance;};//error
}

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

try{...}
catch(T t) {...} // error
throw t //error

6.8 注意擦除后的冲突

若两个接口类型是同一接口的不同参数化,一个类或类型变量就不能同时作为这两个接口类型的子类

下面的代码是非法的

public class Person implements Comparable<Person>{...}
public class Student extends Person implements Comparable<Student>{...}//error

Student会实现Comparable和Comparable,这是同一接口Comparable的不同参数化。

7 泛型类型的继承规则

Student和Person是继承关系,但是Pair和Pair没有任何关系!

泛型类可以继承其他泛型类或者实现其它泛型接口

ArrayList和List是实现关系,ArrayList和List是实现关系;因为ArrayList类 实现了泛型接口 List。ArrayList和List没有关系。

8 通配符概念

通配符"?"允许类型参数发生变化。例如,通配符类型

Pair<? extends Person> 表示类型参数只能是Person类或它的子类。

并且 Pair,Pair都是Pair<? extends Person>的子类。

8.1 通配符的子类型限定和父类型限定

子类型限定:Pair<? extends Person> 表示:类型参数只能是Person及其子类;子类型限定又称为 上界限定

父类型限定:Pair<? super Student> 表示:类型参数只能是Student和其父类;父类型限定又称为超类型限定或下界限定。

8.2 父类型限定的使用

上面类型变量的限定中有一个例子:

 public static <T extends Comparable> T getMin(T[] a){
        if (a == null || a.length == 0){
            return null;
        }
        T min = a[0];
        for (int i = 0; i < a.length; i++) {
            if (min.compareTo(a[i]) > 0){
                min = a[i];
            }
        }
        return min;
    }

为了保证min变量有compareTo方法,因此个类型变量T进行了限制,限制它为Comparable类型。

Comparable本身也是一个泛型:Comparable。

上面的程序在类型变量T为String时是没有任何问题的,因为String类实现了Comparable。

但是如果上面的getMin()方法的入参是一个LocalDate数组时,存在这样的问题:LocalDate实现了ChronoLocalDate接口,而ChronoLocalDate继承了Comparable,因此LocalDate实际上实现了Comparable而不是Comparable,此时就可以使用父类型限定来解决:

public static <T extends Comparable<? super T>> T getMin(T[] a){
        if (a == null || a.length == 0){
            return null;
        }
        T min = a[0];
        for (int i = 0; i < a.length; i++) {
            if (min.compareTo(a[i]) > 0){
                min = a[i];
            }
        }
        return min;
    }

这样写的话CompareTo方法的入参就可以传入一个T类型或者T类型的超(父)类型。

另一个常见的用法是作为一个函数式接口的参数类型。例如Collection接口有一个方法:

defatlt boolean removeIf(Predicate<? super E> filter);

这个方法会删除满足过滤条件的元素。

ArrayList<Person> list = ...;
Predicate<Object> filter = obj -> obj.hasCode() % 2 != 0;
list.removeIf(filter);// 使用?super E就可以让这里不止可以传入Predicate<Person>,还可以传入Predicate<Object>

在生活中,生产力有一个上限,消费也有一个消费下限。

为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符,那么规则就是:生产者有上界,消费者有下界,即:

  • 如果参数化类型表示一个T的生产者,使用<?extends T>;
  • 如果它表示一个T的消费者,使用 <? super T>;
  • 如果既是消费者又是生产者,那么通配符就失去了意义,应该用精确地参数类型。

8.3 生产有上限,消费有下限的理解

看一下这些常见的泛型接口:Comparable,Predicate,Function<T,U>,Consumer;

Comparable:这个接口通常是把T用来进行比较,可以理解为消费,消费有下限,所以用下界限定,所以经常在程序中看到Comparable<? super T>;

Predicate:四大函数式接口之一,断言接口,主要是对T进行判断,同样可以理解为消费,消费有下限,所以用下界限定,所以经常在程序中看到某些方法的入参是:Predicate<? super T>;

Function<T,U>:四大函数式接口之一,通常传入T,返回U,那么可以理解为T是用来消费,U是生产而来的,所以经常在程序中看到某些方法的入参是这样的:Function<? super T,? extends U>;

Consumer:四大函数式接口之一,消费者接口,消费T,所以经常看到:Consumer<? super T>;

Supplier:四大函数式接口之一,生产者接口,生产T,所以经常看到:Supplier<? extends T>

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值