前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >再次理解泛型

再次理解泛型

作者头像
对话、
发布2022-02-22 14:37:09
4380
发布2022-02-22 14:37:09
举报
文章被收录于专栏:Android-XjAndroid-Xj

文章目录

前言

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

定义泛型类、泛型接口

代码语言:javascript
复制
//泛型类
public class Apple<T>{

    private T info;
    Apple(T info){
        this.info = info;
    }

    public T getInfo() {
        return info;
    }

    public void setInfo(T info) {
        this.info = info;
    }

    public static void main(String[] args){
        Apple<String> apple = new Apple<>("苹果");
        System.out.println(apple.getInfo());
        Apple<Integer> intapp = new Apple<>(52);
        System.out.println(intapp.getInfo());
    }

}


//泛型接口
public interface List<E> {
    void add(E e);
}

上面定义了一个带泛型的Apple类,在使用的时候,为T传入实际的值,这样就可以生成如Apple,Apple形式的逻辑子类(物理上并不存在)

怎么派生带泛型的子类?

当创建了带泛型声明的接口、类之后,可以为该接口或者类创建实现类,或者是派生子类,当使用这些接口、父类时不能再包含形参:例如下面的做法就是错误的:

代码语言:javascript
复制
//定义类A继承Apple类,Apple类不能跟泛型形参
public class A extends Apple<T>{}

如果想从Apple类派生一个子类则可以修改为如下代码:

代码语言:javascript
复制
//使用Apple类时为T穿融入String类型
public class A extends Apple<String>{}

调用方法时必须为所有的数据形参传入参数值,与调用方法不同的是,使用接口、类时也可以不为泛型形参传入实际的参数参数,即下面的代码也是正确的

代码语言:javascript
复制
//使用Apple类时,没有为T形参传入实际的参数
public class A extends Apple{}

像这种使用Apple类时省略泛型的形式被称为原始类型(raw type)

如果从Apple 类派生子类1,则在Apple类中所有使用T类型的地方都将被替换成String类型,如果子类需要重写它的方法,就必须注意这一点。如下所示:

代码语言:javascript
复制
public class A extends Apple<String> {
    public A(String info) {
        super(info);
    }

    //正确的重写了父类的方法,返回值
    //与父类Apple<String>的返回值完全相同
    @Override
    public String getInfo() {
        return "重写父类的方法";
    }

    @Override
    public void setInfo(String info) {
        super.setInfo(info);
    }
}

如果使用Apple时没有传入实际的类型(即原始的类型),Java编译器可能发出警告:使用了未检查或不安全的操作----这就是泛型的警告。此时会把形参T当做为Object类型处理。如下所示:

代码语言:javascript
复制
//没有使用泛型,默认为Object类型
public class A extends Apple {
    public A(String info) {
        super(info);
    }
    
    @Override
    public Object getInfo() {
        return super.getInfo();
    }
    @Override
    public void setInfo(Object info) {
        super.setInfo(info);
    }
}

类型通配符

当我们是用一个泛型类时,都应该为这个泛型类传入一个类型实参。如果没有传入类型实际参数编译器就会提出泛型警告。如下所示:

代码语言:javascript
复制
  public void test(List list){
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }

上面程序当然没有问题,这是一段最普通的遍历集合的代码,问题是List是一个有泛型声明的接口,此处使用没有传入实际的参数,这将引起泛型的警告。所以要为List接口传入时间的类型参数,因为List集合中的元素类型是不确定的,所以讲上面方法改为如下形式。

代码语言:javascript
复制
 public void test(List<Object> list){
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }

表明看起来没有问题,这个方法的声明确实没有任何问题。问题是调用该方法传入的实际参数值可能不是我们期望的。如下:

代码语言:javascript
复制
 public static void main(String[] args){
        List<String> list = new ArrayList<>();
        test(list);
    }

编译上面的程序,将发生如下错误:

代码语言:javascript
复制
不兼容的类型: List<String>无法转换为List<Object>

表明List对象不能被当成List来使用。

使用类型通配符

为了表示各种泛型List的父类可以使用类型通配符<?>,将一个问号作为他的参数类型传给list集合,写作List<?>,意思是元素类型为未知的Object,这个问号被称为 通配符 ,他的元素可以匹配任何类型。如上面的可以写成如下形式:

代码语言:javascript
复制
 public static void test(List<?> list){
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }

现在使用任何类型的List来调用它,都可以访问集合list中得元素,其类型时候Object,这个永远是安全的,不管List的真实数据是什么,它包含的都是Object。这种写法可以适应于任何支持泛型声明的接口和类。

但这种带通配符的List仅表示他是各种泛型的List的父类,并不能将元素加入到其中,如下就会引起编译错误:

代码语言:javascript
复制
  List<?> list = new ArrayList<>();
        //下面将会引起编译错误
        list.add(new Object());

因为程序无法确定list集合中的元素类型,所以不能向其中添加对象。

类型通配符的上限

当我们定义某个方法的时候,如果想要让这个方法被特定的类调用,就可以使用这种形式。如下所示:

代码语言:javascript
复制
     public static void test(List<? extends Apple<String>> list) {
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }

当我们调用这个方法的时候,传入的参数必须是继承自Apple的,

代码语言:javascript
复制
public class A extends Apple<String> {
    public A(String info) {
        super(info);
    }
    
    public static void test(List<? extends Apple<String>> list) {
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }
    public static void main(String[] args) {
        List<A> list = new ArrayList<>();
        test(list);
    }
}

如上,list<? extends Apple>是一个受限制的通配符的例子,此处的 ?代表是一个未知的类型,就想前面看到的通配符一样,但此处的未知类型一定是Apple的子类型(也可以是 Apple本身),因此把这种称为通配符的上限。

由于这个程序无法确定这个受限通配符的类型是什么,所以不能把Apple对象或者他的子类加入到这个泛型的集合中去,例如,下面的代码就是错误的:

代码语言:javascript
复制
public static void test(List<? extends Apple<String>> list) {
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
        //下面引起编译错误
        list.add(new A(""));
    }

简而言之,这种指定类型通配符上限的集合,只能从集合中取元素(取出的元素是总上限的类型),不能像集合添加元素。因为编译器无法确定这集合中的元素是那种类型。

类型通配符的下限

通配符下限用<? super 类型> 的方式来指定,和通配符上限的作用恰好相反.

代码语言:javascript
复制
public class A extends Apple<String> {
    public A(String info) {
        super(info);
    }

    public static void test(List<? super A> list) {
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }
    public static void main(String[] args) {
        List<Apple<String>> list = new ArrayList<>();
        test(list);
    }
}

修改一下text方法,改为 List<? super A> ,这个表示这个list集合里面存的只能是A,或者是A的父类。向下限定可以向其中添加元素,但是添加的类型必须是 super 后面跟的类型,如下所示:

代码语言:javascript
复制
public class A extends Apple {
    public A(String info) {
        super(info);
    }
    public static void test(List<? super A> list) {
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
        //可以添加
        list.add(new A(""));
        //编译报错
//        list.add(new Apple<String>(""));
    }
    public static void main(String[] args) {
        List<Apple> list = new ArrayList<>();
        test(list);
    }
}

泛型形参的上限

java泛型不仅允许在通配符上使用上限,而且可以在定义泛型形参的时候设定上限,表示传给该泛型形参必须是该类上限类型,要不就是该上限类型的子类,如下所示:

代码语言:javascript
复制
public class Apple<T extends Number>{
    private T info;
    Apple(T info){
        this.info = info;
    }

    public T getInfo() {
        return info;
    }

    public void setInfo(T info) {
        this.info = info;
    }
    public static void main(String[] args){
        
        Apple<Double> dou = new Apple<>(2.2);
        Apple<Integer> integer = new Apple<>(2);
        
        //下面将编译失败,视图将String类型传给形参T
        //是String不是Number的子类,所以编译错误
        Apple<String> apple = new Apple<>("苹果");
        
    }
}

String既不是Number类型,也不是他的子类,所以报错.

在一种更极端的情况下,程序需要为泛型设置多个上限(至少有一个父类上限,可一个有多个接口上限),表明该泛型形参必须是父类的子类(父类也行),并且实现多个上限的接口,如下所示:

代码语言:javascript
复制
//表明T类型必须是Number 或 是其子类,并且必须实现 Runnable接口
public class Apple<T extends Number & Runnable>{}

与类同时继承父类,实现接口类似的是,为泛型形参设置多个上限的时候,所有的接口上限必须位于类上限之后。也就是说,如果需要为泛型形参指定类上限,类上限必须放在第一位。

泛型方法

如下所示:

代码语言:javascript
复制
 //泛型方法,该方法中带有一个T泛型形参 
    public static <T> void copy(List<T> from, List<T> to) {
        for (T t : from) {
            to.add(t);
        }
    }
    public static void main(String[] args){
        
        List<Integer> list1 = new ArrayList<>();
        List<Number> list2 = new ArrayList<>();
        copy(list1,list2);//报错:编译器无法确定泛型T的类型。
        
        List<String> list3 = new ArrayList<>();
        List<String> list4 = new ArrayList<>();
        copy(list3,list4);//泛型T 为String 类型
        
    }

copy方法中带有一个带T的泛型形参,但是在调用的时候 传的泛型参数为String,Integer类型,编译器无法准确的推断出泛型方法中泛型形参的类型,所以会报错.

为了避免上面的错误,可将代码改为如下格式.

代码语言:javascript
复制
    //泛型方法,该方法中带有一个T泛型形参
    public static <T> void copy(List<? extends T> from, List<T> to) {
        for (T t : from) {
            to.add(t);
        }
    }
    public static void main(String[] args){

        List<Integer> list1 = new ArrayList<>();
        List<Number> list2 = new ArrayList<>();
        copy(list1,list2);//不报错:编译器确定了泛型T的类型。

        List<String> list3 = new ArrayList<>();
        List<String> list4 = new ArrayList<>();
        copy(list3,list4);//泛型T 为String 类型

    }

这种采用了类型通配符的方式,只要copy方法中的前一个集合的元素类型是最后一个集合元素的子类即可。

带泛型的返回值

代码语言:javascript
复制
    //该返回值表明这个方法的返回值是一个list集合,并且元素的类型为T
   public static <T>  List<T> copy( List<T> to) {
        List<T> list = new ArrayList<>();
        for (int i = 0; i < to.size(); i++) {
            list.add(to.get(i));
        }
        return list;
    }

    //返回值为T
    public static <T extends Number> T  test(T t){
        return t;
    }

    public static void main(String[] args){

        List<Integer> list1 = new ArrayList<>();
        //该方法复制了一个新的集合,返回值为list集合 返回值为泛型
        List<Integer> copy = copy(list1);

        
       Double d = new Double(2.0);
       Double test1 = test(d);//返回值为传入的类型

    }

总结

无法确定编译器不知道的就会报错,一切为了安全考虑。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章目录
  • 前言
    • 定义泛型类、泛型接口
      • 怎么派生带泛型的子类?
        • 类型通配符
          • 使用类型通配符
          • 类型通配符的上限
          • 类型通配符的下限
          • 泛型形参的上限
          • 泛型方法
          • 带泛型的返回值
      • 总结
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档