java基础-并不神奇的泛型

前言
开始之前,先给大家来一道测试题。

[Java] 纯文本查看 复制代码
1
2
3
4
List<String> strList = new ArrayList<String>();
List<Integer> integerList = new ArrayList<Integer>();
         
System.out.println(strList.getClass() == integerList.getClass());


请问,上面代码最终结果输出的是什么?熟悉泛型的同学应该能够答出来,而对泛型有所了解,但是了解不深入的同学可能会答错。

带着问题

  • Java中的泛型是什么 ? 使用泛型的好处是什么?
  • 什么是泛型中的限定通配符和无界通配符 ?
  • 你可以把List<String>传递给一个接受List<Object>参数的方法吗?
  • Java的泛型是如何工作的 ? 什么是类型擦除 ?

一.泛型概述
最早的“泛型编程”的概念起源于C++的模板类(Template),Java 借鉴了这种模板理念,只是两者的实现方式不同。C++ 会根据模板类生成不同的类,Java 使用的是类型擦除的方式。

1.1为什么使用泛型
Java1.5 发行版本中增加了泛型(Generic)。
有很多原因促成了泛型的出现,而最引人注意的一个原因,就是为了创建容器类。-- 《Java 编程思想》

容器就是要存放要使用的对象的地方。数组也是如此,只是相比较的话,容器类更加的灵活,具有更多的功能。所有的程序,在运行的时候都要求你持有一大堆的对象,所以容器类算得上最需要具有重用性的类库之一了。
看下面这个例子,
[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class AutoMobile {
}
 
/**
 * 重用性不好的容器类
 */
public class Holder1 {
 
    private AutoMobile a;
 
    public Holder1(AutoMobile a) {
        this.a = a;
    }
    //~~
}
 
/**
 * 想要在java5 之前实现可重用性的容器类
 * @author Richard_yyf
 * @version 1.0 2019/8/29
 */
public class Holder2 {
 
    private Object a;
 
    public Holder2(Object a) {
        this.a = a;
    }
 
    public Object getA() {
        return a;
    }
 
    public void setA(Object a) {
        this.a = a;
    }
 
    public static void main(String[] args) {
        Holder2 h2 = new Holder2(new AutoMobile());
        AutoMobile a = (AutoMobile) h2.getA();
        h2.setA("Not an AutoMobile");
        String s = (String) h2.getA();
        h2.setA(1);
        Integer x = (Integer) h2.getA();
    }
}
 
 
 
/**
 * 通过泛型来实现可重用性
 * 泛型的主要目的是指定容器要持有什么类型的对象
 * 而且由编译器来保证类型的正确性
 *
 * @author Richard_yyf
 * @version 1.0 2019/8/29
 */
public class Holder3WithGeneric<T> {
 
    private T a;
 
    public Holder3WithGeneric(T a) {
        this.a = a;
    }
 
    public T getA() {
        return a;
    }
 
    public void setA(T a) {
        this.a = a;
    }
 
    public static void main(String[] args) {
        Holder3WithGeneric<AutoMobile> h3 = new Holder3WithGeneric<>(new AutoMobile());
        // No class cast needed
        AutoMobile a = h3.getA();
    }
}
通过上述对比,我们应该可以理解类型参数化具体是什么个意思。
在没有泛型之前,从集合中读取到的每一个对象都需要进行转换。如果有人不小心插入了类型错误的对象,在运行时的转换处理就会出错。这显然是不可忍受的。
泛型的出现,给Java带来了不一样的编程体验。
1.2泛型的作用
  • 参数化类型。与普通的 Object 代替一切类型这样简单粗暴而言,泛型使得数据的类别可以像参数一样由外部传递进来。它提供了一种扩展能力。它更符合面向抽象开发的软件编程宗旨。
  • 类型检测。当具体的类型确定后,泛型又提供了一种类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过。所以说,它是一种类型安全检测机制,一定程度上提高了软件的安全性防止出现低级的失误。
  • 提高代码可读性。不必要等到运行的时候才去强制转换,在定义或者实例化阶段,因为 Holder<AutoMobile>这个类型显化的效果,程序员能够一目了然猜测出这个容器类持有的数据类型。
  • 代码重用。泛型合并了同类型对象的处理代码,使得代码重用度变高。

二.泛型的定义和使用

泛型按照使用情况可以分为 3 种。
  • 泛型类
  • 泛型方法
  • 泛型接口

2.1泛型类

  • 概述:把泛型定义在类上
  • 定义格式:
[Java] 纯文本查看 复制代码
1
2
3
public class 类名 <泛型类型1,...> {
    ...
}

 

  • 注意事项:泛型类型必须是引用类型(非基本数据类型)


泛型参数规范
尖括号 <>中的 字母 被称作是类型参数,用于指代任何类型。我们常看到<T> 的写法,事实上,T 只是一种习惯性写法,如果你愿意。你可以这样写。

[Java] 纯文本查看 复制代码
1
2
3
public class Test<Hello> {
    Hello field1;
}

 

但出于规范和可读性的目的,Java 还是建议我们用单个大写字母来代表类型参数。常见的如:
  • T 代表一般的任何类。
  • E 代表 Element 的意思,或者 Exception 异常的意思。
  • K 代表 Key 的意思。
  • V 代表 Value 的意思,通常与 K 一起配合使用。
  • S 代表 Subtype 的意思

2.2 泛型方法

  • 概述:把泛型定义在方法上
  • 定义格式:
[Java] 纯文本查看 复制代码
1
2
3
public <泛型类型> 返回类型 方法名(泛型类型 变量名) {
    ...
}

 

  • 注意事项:
  • 这里的<T> 中的T被称为类型参数,而方法中的 T 被称为参数化类型,它不是运行时真正的参数。
  • 方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。


2.3泛型接口

泛型接口和泛型类差不多。
  • 泛型接口概述:把泛型定义在接口
  • 定义格式:
[Java] 纯文本查看 复制代码
1
2
3
public interface 接口名<泛型类型> {
    ...
}


Demo

[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
public interface GenericInterface<T> {
 
    void show(T t);
}
 
public class GenericInterfaceImpl<String> implements GenericInterface<String>{
 
    @Override
    public void show(String o) {
 
    }
}



三.通配符?

除了用 <T>表示泛型外,还有 <?>这种形式。? 被称为通配符。
为什么要引进这个概念呢?先来看下下面的Demo.
[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public class GenericDemo2 {
 
    class Base{}
 
    class Sub extends Base{}
 
    public void test() {
        // 继承关系
        Sub sub = new Sub();
        Base base = sub;
        List<Sub> lsub = new ArrayList<>();
        // 编译器是不会让下面这行代码通过的,
        // 因为 Sub 是 Base 的子类,不代表 List<Sub>和 List<Base>有继承关系。
        List<Base> lbase = lsub;
    }
}
在现实编码中,确实有这样的需求,希望泛型能够处理某一范围内的数据类型,比如某个类和它的子类,对此 Java 引入了通配符这个概念。
所以,通配符的出现是为了指定泛型中的类型范围。
通配符有 3 种形式。
  • <?>被称作无限定的通配符
  • <? extends T>被称作有上限的通配符
  • <? super T>被称作有下限的通配符



3.1无界通配符<?>
无限定通配符经常与容器类配合使用,它其中的 ? 其实代表的是未知类型,所以涉及到 ? 时的操作,一定与具体类型无关。

[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
// Collection.java
public interface Collection<E> extends Iterable<E> {
    
    boolean add(E e);
}
 
public class GenericDemo3 {
    /**
     * 测试 无限定通配符 <?>
     * @param collection c
     */
    public void testUnBoundedGeneric(Collection<?> collection) {
        collection.add(123);
        collection.add("123");
        collection.add(new Object());
 
        // 你只能调用 Collection 中与类型无关的方法
        collection.iterator().next();
        collection.size();
    }
}

 

无需关注 Collection 中的真实类型,因为它是未知的。所以,你只能调用 Collection 中与类型无关的方法。
有同学可能会想,<?>既然作用这么渺小,那么为什么还要引用它呢? 
个人认为,提高了代码的可读性,程序员看到这段代码时,就能够迅速对此建立极简洁的印象,能够快速推断源码作者的意图。
(用的很少,但是要理解)
为了接下去的说明方便,先定义一下几个类。
[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Food {}
 
    class Fruit extends Food {}
 
    class Apple extends Fruit {}
 
    class Banana extends Fruit {}
 
    // 容器类
    class Plate<T> {
        private T item;
 
        public Plate(T item) {
            this.item = item;
        }
 
        public T getItem() {
            return item;
        }
 
        public void setItem(T item) {
            this.item = item;
        }
    }
3.2 上限通配符<? extends T>
<?>代表着类型未知,但是我们的确需要对于类型的描述再精确一点,我们希望在一个范围内确定类别,比如类型 T 及 类型 T 的子类都可以放入这个容器中。

副作用

边界让Java不同泛型之间的转换更容易了。但不要忘记,这样的转换也有一定的副作用。那就是容器的部分功能可能失效。
[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
public void testUpperBoundedBoundedGeneric() {
       Plate<? extends Fruit> p = new Plate<>(new Apple());
 
       // 不能存入任何元素
        p.setItem(new Fruit()); // error
        p.setItem(new Apple()); // error
 
        // 读出来的元素需要是 Fruit或者Fruit的基类
        Fruit fruit = p.getItem();
        Food food = p.getItem();
//        Apple apple = p.getItem();
    }
<? extends Fruit>会使往盘子里放东西的set( )方法失效。但取东西get( )方法还有效。比如下面例子里两个set()方法,插入Apple和Fruit都报错。
原因是编译器只知道容器内是Fruit或者它的派生类,但具体是什么类型不知道。可能是Fruit?可能是Apple?也可能是Banana,RedApple,GreenApple?
如果你需要一个只读容器,用它来produce T,那么使用<? extends T> 。

 

3.3下限通配符 <? super T>
相对应的,还有下限通配符 <? super T>

副作用因为下界规定了元素的最小粒度的下限,实际上是放松了容器元素的类型控制。既然元素是Fruit的基类,往里面存比Fruit粒度小的类都可以。但是往外读取的话就费劲了,只有所有类的基类Object可以装下。但这样一来元素类型信息就都丢失了。

[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
public void testLowerBoundedBoundedGeneric() {
//        Plate<? super Fruit> p = new Plate<>(new Food());
        Plate<? super Fruit> p = new Plate<>(new Fruit());
 
        // 存入元素正常
        p.setItem(new Fruit());
        p.setItem(new Apple());
 
        // 读取出来的东西,只能放在Object中
        Apple apple = p.getItem(); // error
        Object o = p.getItem();
    }



3.4 PECS原则

PECS - Producer Extends Consumer Super
  • “Producer Extends” – 如果你需要一个只读容器,用它来produce T,那么使用<? extends T> 。
  • “Consumer Super” – 如果你需要一个只写容器,用它来consume T,那么使用<? super T>。
  • 如果需要同时读取以及写入,那么我们就不能使用通配符了。


更多免费技术资料可关注:annalin1203

posted @ 2020-05-26 08:51  幽暗森林之猪大屁  阅读(111)  评论(0编辑  收藏  举报