Java泛型总结之定义泛型接口、类和类型通配符

Java泛型总结之定义泛型接口、类和类型通配符

  • 前言:在前面总结Collection接口时常常会用到泛型知识,以及在之前对Okhttp3进行封装时用上了泛型,封装需要泛型是因为工具类需要有通用性,适合各种自定义类的数据传入,所以需要有泛型思想。所以,我决心把泛型知识都梳理一遍,方便自己也尽量给同仁们一些帮助。
(一)初识泛型

(一).为什么要使用泛型?我们看下面这个例子:

public class GenericTest {

    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("Hello");
        list.add("Hi");
        list.add(100);

        for (int i = 0; i < list.size(); i++) {
            String name = (String) list.get(i); // 出现异常
            System.out.println("name:" + name);
        }
    }
}
  • 对于集合List,里面元素默认为Object类,但是在循环过程中,List对加入的元素原始类型全给忘记了,只记得它们是Object类。所以在String name = (String) list.get(i);这行代码中,将Integer类型的数据错转为String,引发了ClassCastException,类型转换异常。
  • 由此我们发现:当我们将一个对象放入集合中,集合不会记住此对象的类型,当再次从集合中取出此对象时,改对象的编译类型变成了Object类型,但其运行时类型仍然为其本身类型。

(二).使用泛型

  • 从Java5以后,Java引入了参数化类型,允许程序在创建集合时指定集合元素的类型。
public class GenericTest {

    public static void main(String[] args) {
        /*
        List list = new ArrayList();
        list.add("Hello");
        list.add("Hi");
        list.add(100);
        */

        List<String> list = new ArrayList<String>();
        list.add("Hello");
        list.add("Hi");
        //list.add(100);   // 1  提示编译错误

        for (int i = 0; i < list.size(); i++) {
            String name = list.get(i); // 2
            System.out.println("name:" + name);
        }
    }
}
  • 采用泛型写法后,在//1处想加入一个Integer类型的对象时会出现编译错误,通过List,直接限定了list集合中只能含有String类型的元素,从而在//2处无须进行强制类型转换,因为此时,集合能够记住元素的类型信息,编译器已经能够确认它是String类型了。

(三).定义泛型接口、类

  • 我们来看一下List接口、Iterator接口、Map的源码片段
//定义接口时指定一个类型形参,该形参名为E
public interface List<E>{
//在该接口中,E可作为类型使用
//下面方法可以使用E作为参数类型

  boolean add(E e);
  Iterator<E> iterator();

}

//定义该接口时指定了两个类型形参,其形参名为K,V
public interface Map<K,V>{

    //在该接口中K,V完全可以作为类型使用
    Set<K> keySet();
    V put(K key,V value);

}


  • 我们看一个最简单的泛型小例子:
public class GenericTest {

    public static void main(String[] args) {

        Box<String> name = new Box<String>("corn");
        System.out.println("name:" + name.getData());
    }

}

class Box<T> {

    private T data;

    public Box() {

    }

    public Box(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }

}
  • 由上面例子可知,允许在定义接口、类时声明类型形参,类型形参在整个接口,类体内都可当成类型使用。

(四).从泛型类派生子类

  • 当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从该父类派生子类。但是当用这些接口、父类时不能再包含类型参数。如下面代码为错:
//Apple类不能跟类型形参
public class A extends Apple<T>{}
  • 必须改为如下代码:
//使用Apple类时,没有为T形参传入实际的类型参数
public class A extends Apple<String>{}

(五)并不存在泛型类

  • 先看一个简单例子:
public class GenericTest {

    public static void main(String[] args) {

        Box<String> name = new Box<String>("corn");
        Box<Integer> age = new Box<Integer>(712);

        System.out.println("name class:" + name.getClass());      // com.qqyumidi.Box
        System.out.println("age class:" + age.getClass());        // com.qqyumidi.Box
        System.out.println(name.getClass() == age.getClass());    // true

    }

}
  • 由此,我们发现,在使用泛型类时,虽然传入了不同的泛型实参,但并没有真正意义上生成不同的类型,传入不同泛型实参的泛型类在内存上只有一个,即还是原来的最基本的类型(本实例中为Box),当然,在逻辑上我们可以理解成多个不同的泛型类型。

  • 究其原因,在于Java中的泛型这一概念提出的目的,导致其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦出,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。

【对此总结成一句话】:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型,不管泛型的类型形参传入哪一种类型实参,对于java来说,它们依然被当做一个类来处理,在内存中也只占用一块内存来处理。

(六).类型通配符

  • 我们先来看这样一个片段

public void test(List<Object> c){

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

}
  • 上面代码看似没问题:
List<String> strList = new ArrayList<>();
test(strList);
  • 上面程序发生编译错误,说明List《String 》对象无法被当成List《String》对象,也就是说List《String 》并不是List《String》对象的子类。
  • 为了表示各种泛型List的父类,可以使用类型通配符。类型通配符是一个问号。类型通配符一般是使用 ? 代替具体的类型实参。注意了,此处是类型实参,而不是类型形参!
public class GenericTest {

    public static void main(String[] args) {

        Box<String> name = new Box<String>("corn");
        Box<Integer> age = new Box<Integer>(712);
        Box<Number> number = new Box<Number>(314);

        getData(name);
        getData(age);
        getData(number);
    }

    public static void getData(Box<?> data) {
        System.out.println("data :" + data.getData());
    }

}

(七).设定类型通配符的上限

  • 在上面的例子中,如果需要定义一个功能类似于getData()的方法,但对类型实参又有进一步的限制:只能是Number类及其子类。此时,需要用到类型通配符上限。
public class GenericTest {

    public static void main(String[] args) {

        Box<String> name = new Box<String>("corn");
        Box<Integer> age = new Box<Integer>(712);
        Box<Number> number = new Box<Number>(314);

        getData(name);
        getData(age);
        getData(number);

        //getUpperNumberData(name); // 1
        getUpperNumberData(age);    // 2
        getUpperNumberData(number); // 3
    }

    public static void getData(Box<?> data) {
        System.out.println("data :" + data.getData());
    }

    public static void getUpperNumberData(Box<? extends Number> data){
        System.out.println("data :" + data.getData());
    }

}

(八).设定类型形参的上限

  • Java不仅允许在使用通配符形参时设定上限,而且可以在定义类型形参时设定上限,用于传给该类型形参的实际类型要么是该上限类型,要么是该上限类型的子类。
public class Apple<T extends Number>{

T col;
public void static main(String[] args){
    Apple<Integer> ai = new Apple<>();
    Apple<Double> ad = new Apple<>();
    //下面代码出现编译错误,下面代码试图把String类型传给T参数
    Apple<String> as = new Apple<>();

    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值