认识泛型 | 青训营笔记

237 阅读8分钟

这是我参与「第四届青训营 」笔记创作活动的第5天

谈谈泛型

写在前面

(1)文章摘要

  1. 泛型类型使用
  2. 泛型类型的注意点
  3. 泛型方法
  4. 限制类型参数 & 通配符
  5. 不能使用泛型的一些情况

(2)读前须知

  1. 本文主要是从Java来谈泛型
  2. 例子比较简单
  3. 知识点也比较简单,但是也有很多细节

一、泛型类型

(1)泛型类型引入

①第一个简单的例子

public class Main {
    static Integer sum(Integer num1, Integer num2) {
        return num1 + num2;
    }
    public static void main(String[] args) {
        sum("string", "字符串"); // 调用一
        sum(); // 调用二
    }
}
  • 不用运行上面的两次调用,你也知道,肯定会报错

image-20220818163657364

  • 这只是很简单的两个异常。你看,是不是都提到了类型有误?

②API的可重用性

  • 在谈类型之前,我们先来看看软件工程主要目的。

软件工程的目标是:在给定成本、进度的前提下,开发出具有适用性、有效性、可修改性、可靠性、可理解性、可维护性可重用性、可移植性、可追踪性、可互操作性和满足用户需求的软件产品。 追求这些目标有助于提高软件产品的质量和开发效率,减少维护的困难。

  • 里面有几个关键词:可修改、可维护、可重用
  • 这就告诉我们,我们编写的程序,不仅仅是要构建明确和一致的接口。还得让你的代码具有很强的可重用性
  • 你可能在想,说这些,和我们今天谈的泛型,有什么关系呢?不用着急,不妨先听我细细说来。

③参数可重用

  • 还是来看这个简单的求和函数
// 【函数二】无参数
public int sum() {
    return 20 + 30;
}
// 【函数一】有参数
public int sum(int n1, int n2) {
    return n1 + n2;
}
  • 我们可以这样调用sum();【result = 50】
  • 也这样调用sum(20, 30);【result = 50】
  • 还可以这样调用sum(20, 500);【result = 520】
  • 你看,这样一个简单的函数,你是不是发现
  • 函数一的功能很单一。就是返回 20 + 30 的结果
  • 函数二明显具有一定的可重用性,当你传入不同参数,返回的结果可能也是不同的
  • 这里,我们关注了一个函数,参数的一些意义

④第二个简单的例子

  • 继续往下,再来一个简单的需求:
    • 想要一个函数,你传入的参数是什么类型。返回值就是什么类型
  • 你可能会想,这还不简单嘛?
  • 拿起键盘就写了几个重载函数
public class Example {
    public int foo(int param) { return param; }
    public People foo(People param) { return param; }
    public String foo(String param) { return param; }
}
  • 可是,类型那么多,还有很多你自定义的引用类型。你这样写
  • 是不是也并没有那么可重用

⑤类型可重用

  • 刚刚说到函数参数可重用的时候,我们是不是看到了可以利用参数,进行一定的重用
  • 那么,我们想要类型变得可重用,是不是也可以利用这种思想
  • 将**【类型参数化】**,那么我们先利用要说的泛型类型,来实现一下刚刚的简单需求
public class Example<T> {
    public T foo(T param) {return param}
}

// 使用
Example<Integer> ep1 = new Example<>(); ep1.foo();
Example<String> ep2 = new Example<>(); ep2.foo();
Example<People> ep3 = new Example<>(); ep3.foo();
  • 你看,这比起上面,是不是要好一些了
  • 注:我这里所使用的,是泛型类型,并不是泛型方法
  • 其实泛型,就是将类型当参数一样传入**【类型参数化】**,提高了代码的复用性

(2)泛型类型注意点

①泛型类型的注意点

  • 刚刚提到了泛型类型,就是使用了泛型的类或者接口
  • 就可以在类、接口中,使用该类型
  • java.util.List
  • 既然我们说**【类型参数化】**,那么用<>扩起来的就是参数
  • 名字可以随意起,建议的类型参数名称
    • T :Type
    • E :Element
    • K :Key
    • N :Number
    • V :Value
  • 不推荐写全,(如class Example <Type> {})这样写可读性就变低了

②使用 & 多个类型参数

// 定义
public interface Example <T, E> {
   T test1();
   E test2(T param1, E param2);
}
// 使用
public class ExampleImpl implements Example<String, Integer> {
    @Override
    public String test1() { return null; }
    @Override
    public Integer test2(String param1, Integer param2) { return null; }
}
  • 多个类型参数间,用 , 隔开
  • 在使用的时候,必须传入对应个数的类型
  • 注:Java中的泛型类型,是不支持默认值的

③原始类型

ExampleImpl rawEp = new ExampleImpl(); // 【原始类型】警告 Raw use of parameterized class 'ExampleImpl'
@SuppressWarnings("rawtypes")
ExampleImpl rawEp2 = new ExampleImpl(); // 可以使用 @SuppressWarnings("rawtypes")抑制警告
ExampleImpl<String> strEp = new ExampleImpl<>();
ExampleImpl<Object> objEp = new ExampleImpl<>();
  • 没有传递具体类型给泛型的类型参数称为原始类型【Raw Type】
  • 使用原始类型的时候,编译器会给出警告。可以用 @SuppressWarnings("rawtypes")抑制
  • 将原始类型赋值给非原始类型时,会报出警告 Unchecked assignment
  • 将非原始类型赋值给原始类型时,不会有任何警告和错误
rawEp = strEp;
rawEp = objEp;
strEp = rawEp; // Unchecked assignment
objEp = rawEp; // Unchecked assignment

④泛型的继承问题

1、问题①
ExampleImpl<Object> objEp = new ExampleImpl<>();
ExampleImpl<Number> numEp = new ExampleImpl<>();
ExampleImpl<Integer> intEp = new ExampleImpl<>();
numEp = intEp;
objEp = numEp;
  • 我们都知道,Integer继承自Number, Number又继承Object
  • 那么上述代码,能否运行成功呢?
2、解答①

image-20220823093620407

  • 如上图所示,只能说左边的继承关系是成立的
  • 右边是不构成继承关系的
  • 所以,上面的代码是不能赋值成功的
3、问题②
  • 看过了上面的思考,下面

  • 我们直接来看几个官方的类接口

public interface Collection<E> extends Iterable<E> {}
public interface List<E> extends Collection<E> {}
public class ArrayList<E> extends AbstractList<E> implements List<E> {} 
  • 那么这些代码,能否运行成功呢?
Iterable<String> iterable = null;
Collection<String> collection = null;
List<String> list = null;
ArrayList<String> arrayList = null;
iterable = collection;
collection = list;
list = arrayList;
4、解答②

image-20220823100155386

  • 类或接口本身有继承关系

  • 泛型参数一致的时候,是继承关系

  • 即使泛型的参数类型,本身具有继承关系,最终也不能构成继承关系

5、问题③
  • 那么下面的代码又会是怎样呢?
public interface MyList<T, E> extends List<T> { }

List<String> list = null;
MyList<String, Integer> myList1 = null;
MyList<String, String> myList2 = null;
MyList<String, Double> myList3 = null;
list = myList1;
list = myList2;
list = myList3;
6、解答③

image-20220823102416638

  • 因为MyList<T, E> 继承自 List<T>
  • 并且将类型参数 T 传递给了父接口
  • 所以,如果第一个类型参数一样,那么就可以构成继承关系

二、泛型方法

  • 使用了泛型的方法(实例方法、静态方法、构造方法)

(1)泛型方法引入

①回顾第二个简单例子

  • 还记得刚刚的第二个例子吗?

  • 要求书写一个函数,传入什么类型的参数,返回值就是什么类型

  • 我们现在用泛型方法来实现一下。

    public <T> T foo(T param) {
        return param;
    }
  • 上面我们用的是泛型类型来实现的,可以比一下
  • 我们发现,刚刚的泛型类型,必须得依赖类或者接口
  • 而泛型方法。不依赖类或者接口,我这里说的是不需要在类上面加 <>尖括号
  • 直接在对应的方法的修饰符与返回值之间使用<>,将类型参数化
  • 那么在此方法中用到类型的地方,都可以使用此类型参数

②第三个简单例子

  • 有一个Person类,它本身使用了泛型类型
public class Person<N, A> {
    private N name;
    private A age;
    
    // 并且实现了set、get、toString方法
}
  • 要求用泛型方法写一个工具函数,来给Person同时设置name 和 age
public class Main {

    // 目标方法
    static <N, A> void setPerson(Person<N, A> person, N name, A age) {
        person.setName(name);
        person.setAge(age);
    }
    // 使用
    public static void main(String[] args) {
        Person<String, Integer> p1 = new Person<>();
        setPerson(p1, "Ciusyan", 21);
        System.out.println(p1); // Person{name=Ciusyan, age=21}
	
        Person<String, String> p2 = new Person<>();
        setPerson(p2, "Ciusyan", "21岁");
        System.out.println(p2); // Person{name=Ciusyan, age=21岁}
    }
}
  • 可以看到。这就是使用泛型方法。
  • 和泛型类型无关,可以单独使用,也可以配合使用

(2)泛型方法的注意点

①泛型方法的类型推断

  • 你可能会想,我们以前使用泛型类型的时候

  • 不传入类型参数,是原始类型,会报出警告

  • Person<String, Integer> p1 = new Person<>();

  • 而使用泛型方法的时候

  • 我们没有传入类型,为什么也可以呢?也不报警告

  • setPerson(p1, "Ciusyan", 21);

  • 上面这样使用,是因为编译器,能够自动推断出类型

  • 其实完整的写法,应该是

// 因为 setPerson 是Main里面的一个静态方法。所以直接用类名调用即可
Main.<String, Integer>setPerson(p1, "Ciusyan", 21);

②泛型构造方法

  • 泛型方法还可以使用在构造方法上
    public <T> Person(N name, A age, T param) {
        this.name = name;
        this.age = age;
        System.out.println(param);
    }

三、限制类型参数

(1)限制类型引入

  • 泛型意味着,什么引用类型都可以传递

  • 【基本数据类型,不可以当类型参数传递。若需要用基本数据类型,请使用对应的包装类(如:int —— Integer)】

  • 可是有时候,我们不想要那么宽泛这个时候该如何

  • 我们可以通过关键字 extends关键字,对类型增加一些限制条件,

// 定义T只能是
public class Person<T extends Number> {
    private T age;
}
  • extends后面可以跟类名、接口名,代表 T必须是 A类型,或者继承、实现A类型

(2)第四个简单例子

  • 最经典的就是。让一个类型,必须是具有可比较性的
  • 一般就可以用官方自带的接口 Comparable<T>
/*
 1、泛型的参数,必须药具有可比较性
 2、Person本身 实现了 Comparable,那么他自身也具有可比较性
*/
public class Person<T extends Comparable<T>> implements Comparable<T> {
    private T age;
    public Person() {}
    public Person(T age) { this.age = age; }

    @Override
    public int compareTo(T o) {
        return o.compareTo(age);
    }
}

四、通配符

  • 在泛型中,<?>被称为是通配符
  • 通常用作变量的类型、返回值类型的类型参数
  • 不能作为泛型方法调用、泛型类型的实例化、泛型类型定义的参数类型
class Person<T> { }

// 无界
Person<?> p1 = null;
// 有上界
Person<? extends Number> p2 = null;
p2 = new Person<Integer>();
p2 = new Person<Double>();
// 有下界
Person<? super List<Integer>> p3 = null;
p3 = new Person<Collection<Integer>>();
  • 无界意味着无限制,什么类型都可以传给泛型
  • 上界意味着,该类型最大不能超过 extends的类型
  • 下界意味着,该类型,最小不能低于 super的类型

五、泛型类型注意点

  • 基本类型不能作为类型参数
Person<int> p = null; // error
  • 不能创建类型参数的实例
  • 不能用类型参数定义静态变量
class Person <T> { private static T age; // error }
  • 泛型类型的类型,不能用在静态方法上
class Person <T> { 
    public static T test(T param) { return param; } // error 
}
  • 类型参数不能跟 instanceof一起使用
List<String> list = new ArrayList<>();
if (list instanceof List<String>) { } // error
  • 不能创建带有类型参数的数组
Person<Integer>[] people = new Person<Integer>[3]; // error
Person<String>[] people = new Person<>[4]; // error
Person<Integer>[] people = new Person[4]; // error
  • 不能定义泛型的异常类
public class Person<T> extends Exception { } // error
public class Person<T extends Exception> { } // ok。可以用作类型继承
  • cache的异常类型不能用类型参数
// error
public class Person<T> {
    public void test() {
        try { } catch (T e) { }
    }
}

写在后面

(1)个人观点

  • 仅从语法上看,泛型其实很简单
  • 只要是有泛型的编程语言
  • 基本上都离不开 类型参数化
  • 可我认为,泛型难的点,不是这些语法
  • 而是一种用于封装的思想,也有叫做泛型编程的
  • 回到我们刚刚谈的软件工程的主要目的
  • 我们怎么能构建出一个扩展性高、复用性较好的软件
  • 这时候,你可能很大概率会使用到泛型这种技术
  • 所以,抽离开,可复用性、可扩展性后
  • 你完全也可以不抽取、不封装,更别谈用到泛型这种技术了
  • 毕竟我们还可以 CV是吧,但是这是不是有点 ~~~ 哈哈哈