通俗易懂讲泛型

由于博客园的 markdown 语法有点坑,格式如果阅读中遇到问题,可以异步本人语雀文档查看:https://www.yuque.com/docs/share/57b89afd-91d8-4e64-82a0-243c74304004?# 《泛型》

泛型是什么

传统编程大多数都是面向对象类型编程,比如方法参数传入一个指定的类型,这类代码比较难复用,通常新增一个类型时就得增或者改代码。当然除了面对特定的类型编程还有面向基类和接口编程的多态编程,这类的代码会通用一些,方法入参传入一个基类或者接口,那么该方法就能适用于基类的所有派生类和接口的实现者,新增派生类也无需更改代码,但是这样就增加了代码的耦合度,必须继承指定基类或者接口才行。泛型就更加通用了,泛型实现了参数化类型,这样我们编写的通用方法就可以适用于多种类型,而不是一个具体的接口或者类。

个人理解泛型的主要作用是为了复用组件代码,当然在泛型出现之前也是可以编写通用的组件代码的,但是这样有点不安全。

泛型出来之前,集合内存储的元素是 Object 类型的,Object 是所有类的基类,因此,可以往集合内部添加任意类型,如上例子就往 listOld 集合内添加了:字符串,整形,对象,这三种类型。这样会导致一种问题,我们无法得知集合内的元素究竟是什么类型的,只能知道他们都是 Object 的子类。这样在使用的时候就得进行强制类型转化。使用的不当的话很容易报 “ClassCastException” 异常。

java 5 泛型出来之前,集合的使用方法:

public class Main {
    public static void main(String[] args) {
        List listOld = new ArrayList();
        listOld.add("string");
        listOld.add(123);
        listOld.add(new Main());

        for (Object o : listOld) {
            if (o instanceof String) {
                String str = (String) o;
                System.out.println("this is string type");
            }
            if (o instanceof Integer) {
                Integer i = (Integer) o;
                System.out.println("this is Integer type");
            }
            if (o instanceof Main) {
                Main m = (Main) o;
                System.out.println("this is Object");
            }
        }
    }
}

输出:

this is string type

this is Integer type

this is Object

泛型出来之后,我们就可以为集合表明一个确定的类型,这样就可以往集合内添加该类型或者该类型的子类。如果添加的类型不正确,那么编译期就会报错。
通过在集合引入泛型,那么编译器就会在编译器进行类型校验,如果往一个指定了类型的集合内部添加了错误类型,编译器就会报错。
而在使用元素的时候,也会自动的将集合内的元素转化为指定类型,这种转化是安全的,因为编译器确保了只能往集合内添加指定类型的元素。

java 5 泛型出来之后,集合的使用方法:

public class Main {
    public static void main(String[] args) {
        List listNew = new ArrayList<>();
        listNew.add("string");
//        listNew.add(123);// 编译报错
        for (String s : listNew) {
            System.out.println(s);
        }
    }
}

输出:

string

那么,泛型这一套是怎么实现的呢,这就很有意思,jdk 有个传统,就是向上兼容,也就是说每发行一个版本,老版本的代码必定能够在新版本的 jdk 上运行。因此,为了兼容 java 5 之前的集合使用方式,jdk 的研发人员,采用了 “泛型擦除”的方式进行设计。

泛型擦除

老实说,刚开始知道泛型擦除这个概念的时候个人觉得有点拉闸..由于没使用过 c++ 和 python(我承认我菜..目前还没有去学习其他编程语言的想法),因此便不知道其他语言是怎么实现泛型的。

那么,什么是泛型擦除呢,泛型擦除是一种面向编译期设计的方法。所有的我们平时见到的如:

List
List
List list;
List list;
List list;

这些泛型语法,经过编译期,到运行期的时候,统统变成了:

List list

也就是指定的类型统统向上转型成了 Object,所以我说泛型擦除是一种面向编译期设计的方法。

那么知道了泛型是什么,理解了泛型前后的编程规范,并且知道了泛型的实现后,让咱们来了解了解泛型怎么使用吧。

泛型怎么使用

如下例子,很简单,在创建类的时候在类名右边使用 尖括号括起来,然后里面随便定义个字母即可,这个字母就代表你可传进来的类型。

public class GenericClass {

    private T obj;

    public T getObj() {
        return obj;
    }

    public void setObj(T obj) {
        this.obj = obj;
    }

    @Override
    public String toString() {
        return "GenericClass{" +
                "obj=" + obj +
                '}';
    }

    public static void main(String[] args) {
        GenericClass demo1 = new GenericClass<>();
        demo1.setObj("demo1");
        String obj = demo1.getObj();
        System.out.println(obj);
        System.out.println(demo1);

        GenericClass demo2 = new GenericClass<>();
        demo2.setObj(1);
        Integer obj1 = demo2.getObj();
        System.out.println(obj);
        System.out.println(demo2);
    }
}

输出:

demo1

GenericClass{obj=demo1}

demo1

GenericClass{obj=1}

通配符

泛型有个概念是通配符,泛型通配符有三种

  • 无界通配符,接受任意类型,等同于
  • :上界通配符,这里的 Object 可以是任意类,代表的意思是接受任意继承自 Object 的类型
  • :下界通配符,这里的 Object 可以是任意类,代表的意思是接受任意 Object 的父类

无界通配符

无界通配符 看起来意味着”任何事物”,因此使用无界通配符好像等价于使用原生类型,但是它仍旧是很有价值的,因为,实际上它是在声明:”我是想用 Java 的泛型来编写这段代码,我在这里并不是要用原生类型,但是在当前这种情况下,泛型参数可以持有任何类型。

public class Main {
    public static void main(String[] args) {
        Map map1 = new HashMap();
        Map map2 = new HashMap();
        Map map3 = new HashMap();
        Map map4 = new HashMap();//编译失败
        Map map5 = new HashMap();
        Map map6 = new HashMap();
    }
}

上界通配符

上界通配符很有意思,如下代码,在 1 处其实会产生编译报错,因为上界通配符不允许 set 和 add 值,为什么呢,上界通配符的意思是我允许存放所有父类的子类,但是泛型代表的是具体类型,这里的具体类型画重点,就像 2 和 3的用法,虽然采用的上界通配符,但是里面的类型是确定的。

如果你想使用不确定类型,那么直接采用多态特性,比如 4 那样即可。

public class Main {
    public static void main(String[] args) {
        List sonList = new ArrayList<>();    // 1
//        sonList.add(new Son());  编译报错
        sonList = Arrays.asList(new Son(), new Son());          // 2
        for (Father father : sonList) {
            System.out.println(father.getClass().getName());
        }
        List daughterList = Arrays.asList(new Daughter(), new Daughter()); // 3

        List list1 = new ArrayList<>();             // 4
        list1.add(new Son());
    }
}

上界通配符也有一个有意思的点,由于 add 方法的入参是一个泛型,如下图:

通俗易懂讲泛型

由于编译器无法得知这里需要 Father的儿子还是女儿,因此它不会接受任何类型的 Father。如果你先把 Son 向上转型为 Father,也没有关系——编译器仅仅会拒绝调用像 add() 这样参数列表中涉及通配符的方法。

但是!对于入参是 Object 类型的方法,比如 contains(Object o),编译器允许调用他们。

通俗易懂讲泛型

下界通配符

下界通配符也很有意思,上界通配符其实已经指定了具体的类型,在下面的代码就是 Father,所以这个 list 可以随意的 add 值。因为这里 Son 和 Daughter 都是 Father 的子类,所以允许 add,但是 get 的时候就无法拿到具体值了。

public class Main {
    public static void main(String[] args) {
        List list = new ArrayList<>();
        list.add(new Son());
        list.add(new Daughter());
        for (Object o : list) {

        }
    }
}

泛型业界内有两句总结:

  • 频繁往外读取内容的,适合用上界Extends。
  • 经常往里插入的,适合用下界Super。

其他问题

在学习泛型的过程中,遇到一个很有意思的关于数组的点也记录一下。

如下例子(这个例子是 java 编程思想里面的):

在 1 和 2 处竟然会抛出异常!!我之前都不知道的,一直以为这样能塞值成功。

这是因为,虽然定义的时候将 Apple 数组向上转型成了 Fruit 数组,但是运行时的数组机制知道它处理的是 Apple[],因此会在向数组中放置异构类型时抛出异常。

class Fruit {
}

class Apple extends Fruit {
}

class Jonathan extends Apple {
}

class Orange extends Fruit {
}

public class CovariantArrays {
    public static void main(String[] args) {
        Fruit[] fruit = new Apple[10];
        fruit[0] = new Apple(); // OK
        fruit[1] = new Jonathan(); // OK
        fruit[2] = new Orange(); // 1
        // Runtime type is Apple[], not Fruit[] or Orange[]:
        try {
            // Compiler allows you to add Fruit:
            fruit[0] = new Fruit(); // 2
        } catch (Exception e) {
            System.out.println(e);
        }
        try {
            // Compiler allows you to add Oranges:
            fruit[0] = new Orange(); // 3
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}

文章为本人学习过程中的一些个人见解,漏洞是必不可少的,希望各位大佬多多指教,帮忙修复修复漏洞!

Original: https://www.cnblogs.com/cyrus-s/p/15455744.html
Author: 三木同学
Title: 通俗易懂讲泛型

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/715181/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球