泛型概述

泛型程序设计 意味着 **编写的代码可以被很多不同类型的对象所重用**!

Java泛型这个特性是从JDK 1.5开始的。因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。【类型擦除之后会讲解】。

其实,引入泛型的意义就是为了代码复用

为什么要使用泛型?

泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。

  • 保证了类型的安全性【类型转换问题】

    在没有泛型之前,从集合中读取到的每一个对象都必须进行类型转换,如果不小心插入了错误的类型对象,在运行时的转换处理就会出错。

    1
    2
    3
    4
    5
    6
    7
    ArrayList arrayList = new ArrayList();//没有指定泛型,可以添加任何类型

    for (Object o : arrayList) {
    //那当我们取的时候只能使用Object类型接收,那我们要是使用某个类型的方法时,我们需要进行强制类型转换
    String s = (String) o; //你确定一定可以强制类型转换成功吗?
    System.out.println(s.length());
    }

    泛型中参数化类型不考虑继承关系的原因也就是因为如果考虑了继承关系,也就违背了泛型设计的初衷。

    【详解见Java泛型机制(二)深入理解泛型——类型擦除】

  • 消除强制转换

    消除源代码中的许多强制类型转换,这使得代码更加可读,并且减少了出错机会。

  • 避免了不必要的装箱、拆箱操作,提高程序的性能

    在非泛型编程中,将筒单类型作为Object传递时会引起Boxing(装箱)和Unboxing(拆箱)操作,这两个过程都是具有很大开销的。引入泛型后,就不必进行Boxing和Unboxing操作了,所以运行效率相对较高,特别在对集合操作非常频繁的系统中,这个特点带来的性能提升更加明显。

  • 提高了代码的重用性。

泛型的基本使用

泛型有三种使用方式,分别为:

  • 泛型类
  • 泛型接口
  • 泛型方法。

泛型类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Generi<T> {							//此处可以随便写标识符号,T是type的简称 
private T str;

public T getStr() {
return str;
}

public void setStr(T str) {
this.str = str;
}

public void show(T aa){
System.out.println(aa);
}
}
class Pair<T,U>{ //多元泛型
private T first;
private U second;
....
}

泛型接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Info<T>{        // 在接口上定义泛型  
public T getVar() ; // 定义抽象方法,抽象方法的返回值就是泛型类型
}
class InfoImpl<T> implements Info<T>{ // 定义泛型接口的子类
private T var ; // 定义属性
public InfoImpl(T var){ // 通过构造方法设置属性内容
this.setVar(var) ;
}
public void setVar(T var){
this.var = var ;
}
public T getVar(){
return this.var ;
}
}

泛型方法

1
2
3
4
5
6
7
8
class Generi2 {//这个类并不是泛型类
//泛型方法
public static <T> void show(T t){
System.out.println(t);
}
}
Generi2.<String>show("asda");
Generi2.show("asda");//<String> 可以省略

关于泛型方法定义:泛型方法可以定义在普通类中,也可以定义在泛型类中。

关于泛型方法调用:在调用泛型方法时,可以指定泛型,也可以不指定泛型:

先定义一个简单的泛型方法,通过演示看看指定泛型不指定泛型的区别

1
2
3
4
//这是一个简单的泛型方法  
public static <T> T add(T x,T y){
return y;
}
  • 在不指定泛型的情况下,可以使用任意类型,但泛型变量的类型为该方法中的**几种类型的同一父类的最小级**,直到Object

    1
    2
    3
    int i = Test.add(1, 2); //这两个参数都是Integer,所以T为Integer类型  
    Number f = Test.add(1, 1.2); //这两个参数一个是Integer,一个是Float,所以取同一父类的最小级,为Number
    Object o = Test.add(1, "asd"); //这两个参数一个是Integer,一个是String,所以取同一父类的最小级,为Object
  • 在指定泛型的情况下,该方法的几种类型必须是**该泛型的实例的类型或者其子类**

    1
    2
    3
    int a = Test.<Integer>add(1, 2); //指定了Integer,所以只能为Integer类型或者其子类  
    int b = Test.<Integer>add(1, 2.2); //编译错误,指定了Integer,不能为Float
    Number c = Test.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Float

完整示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test {  
public static void main(String[] args) {

/**不指定泛型的时候*/
int i = Test.add(1, 2); //这两个参数都是Integer,所以T为Integer类型
Number f = Test.add(1, 1.2); //这两个参数一个是Integer,一个是Float,所以取同一父类的最小级,为Number
Object o = Test.add(1, "asd"); //这两个参数一个是Integer,一个是String,所以取同一父类的最小级,为Object

/**指定泛型的时候*/
int a = Test.<Integer>add(1, 2); //指定了Integer,所以只能为Integer类型或者其子类
int b = Test.<Integer>add(1, 2.2); //编译错误,指定了Integer,不能为Float
Number c = Test.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Float
}

//这是一个简单的泛型方法
public static <T> T add(T x,T y){
return y;
}
}

类型限定

先看一个例子

1
2
class A{}
class B extends A {}

B是A的子类。然后定义一个方法funA(A a)

1
2
3
4
5
public static void funA(A a) {
// ...
}
funA(A);
funA(B);

以上两个调用都不会报错。那再定义一个方法funA(List<A> listA)

1
2
3
4
5
public static void funC(List<A> listA) {
// ...
}
funA(new List<A>);
funA(new List<B>); //很明显会报错 为什么呢?先说如何解决

如何解决?

泛型设计初衷之一就是为了解决类型转换问题,泛型中参数化类型不考虑继承关系【不允许类型转换】,但有的时候也确实需要进行类型转换。

为了解决泛型中隐含的类型转换问题,Java泛型加入了类型参数的==上下边界机制==。【注意是类型参数】,为类型参数添加限制的同时,对类和对象的使用也添加了限制ˋ( ° ▽、° )。详情见下

<? extends A>表示 该类型参数可以是A(上边界)或者A的子类类型。你可以这样理解,【对于? extends A,该类型参数须是继承了A。】编译时擦除到类型A,即用A类型代替类型参数。

使用<? extends A>可以解决以上的问题

​ 编译器知道了类型参数的范围 —> 如果传入的实例类型B是在这个范围内的话 —> 允许转换

​ 这时只要一次类型转换就可以了,运行时会把对象当做A的实例看待。

1
2
3
4
5
public static void funC(List<? extends A> listA) {
// ...
}
funA(new List<A>);
funA(new List<B>); //这样就不会报错

为什么会报错?

类B是类A的子类。A是B的父类

1
2
3
4
public static void funC(List<A> listA) {
// ...
}
funA(new List<B>); //报错

这相当于

1
List<A> listA = new List<B>; 

假如这样不会报错···· 当我们从listA取值时,都是A类型。B类型转换为A类型【子类向上转型为父类,这是Java允许的】。

1
B element = (B)listA.get(0);

但这样做是没有意义的,这违背了泛型的初衷——解决类型转换问题。就像

1
2
ArrayList<Object> list = new ArrayList<String>;//这是Java不允许的。
String str = (String)list.get(0);

我们使用了泛型,到头来,还是要自己强转,违背了泛型设计的初衷。所以java不允许这么干。

通配符

常用的通配符有: T,E,K,V,?

其实也可以是A、B、C、D、E等的字母代替。使用 T,E,K,V,?只不过是约定俗成而已。

T,E,K,V,? 的约定如下:

T:(type) 表示具体的一个java类型。

E:代表Element。

K、V :分别代表java键值中的Key Value。

? :无界通配符,表示不确定的 java 类型

上下限

在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类

  • <?> 无限制通配符
  • <? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是==此类型的子类==
  • <? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是==此类型的父类==

使用原则《Effictive Java》
为了获得最大限度的灵活性,要在表示 生产者或者消费者 的==输入参数==上使用通配符,使用的规则就是:生产者有上限、消费者有下限

  1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
  2. 如果它表示一个 T 的消费者,就使用 < ? super T>;
  3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。

上边界限定通配符 < ? extends E>

<T extends BoundingType>表示T应该是BoundingType的子类型。T和BoundingType可以是类,也可以是接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class demo{
public static void main(String[] args) {
List<Double> doubleList = new ArrayList<>();
doubleList.add(1.2);
doubleList.add(3.13);
show(doubleList);
}

public static void show(List<? extends Number> list){//参数类型 必须是Number的子类
// list.add(10.1);
for (Number number : list) {
System.out.println(number);
}
}
}

从以上代码可以看出,参数被限定为泛型必须是Number的子类。可以取出,但是不能添加

image-20220809181958588

  • 为什么不能添加呢?

    因为,List中的元素都是Number的子类。

    如果list的泛型是Integer类型,此时你去添加Integer类型的没问题。

    如果list的泛型是Double类型,此时你去添加Double类型的没问题。

    但是,现在不确定是什么类型的,只知道他是Number的子类,就能添加。

    如果list是Integer类型的,那就不能添加除Integer之外的,那如果是Double呢,也一样,所以就全都不能添加啦。

  • 那为什么可以取出呢?

    因为我们从list中拿出来的必定是Number类型的,毕竟Integer等都去继承Number了,可以自动向上转型

下边界限定通配符 < ? super E>

又叫超类型通配符。与extends特性完全相反。

1
2
3
4
5
6
7
8
9
class demo{
public static void main(String[] args) {
List<Integer> integerList = new ArrayList<>();
}

public static void add(List<? super Integer> list){
list.add(100);
}
}

与上限相反,可以添加,但是不能取出

  • 为什么可以添加?

    这里定义了下限是Integer,也就是说这个list里面的类型都是Integer的父类,所以我们只能添加Integer和他的父类。

  • 为什么不能取出?

    因为取的时候没法确实是Interger的哪个父类【没法向上转型】,最后都只能获取我们的根类Object

还要说一句的是,如果既没有上限也没有下限,就既不能取出也不能添加。取出只能用根类Object接收,因为取出根本不知道什么类型的。里面是什么类型不知道,所以就不能放进去。

实际例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private  <E extends Comparable<? super E>> E max(List<? extends E> e1) {
if (e1 == null){
return null;
}
//迭代器返回的元素属于 E 的某个子类型
Iterator<? extends E> iterator = e1.iterator();
E result = iterator.next();
while (iterator.hasNext()){
E next = iterator.next();
if (next.compareTo(result) > 0){
result = next;
}
}
return result;
}

有三处类型限定泛型:

  • Comparable<? super E>

    要对 E 进行比较,即 E 的消费者,所以需要用 super

  • List<? extends E>

    我们要操作的数据的E,需要取出进行比较,所以使用extends

  • <E extends Comparable<? super E>>

    要进行比较,所以 E 需要是可比较的类,因此需要 extends Comparable<…>

多个限制

使用&符号

1
2
3
4
5
6
7
8
9
10
11
public class Client {
//工资低于2500元的上斑族并且站立的乘客车票打8折
public static <T extends Staff & Passenger> void discount(T t){
if(t.getSalary()<2500 && t.isStanding()){
System.out.println("恭喜你!您的车票打八折!");
}
}
public static void main(String[] args) {
discount(new Me());
}
}

参考:

https://pdai.tech/md/java/basic/java-basic-x-generic.html

https://blog.csdn.net/weixin_40251892/article/details/109063161

https://www.cnblogs.com/qinjunlin/p/14721362.html

https://blog.csdn.net/qq_41701956/article/details/123473592

《Java核心技术卷一》

__END__