这是我参与「第四届青训营 」笔记创作活动的第5天
谈谈泛型
写在前面
(1)文章摘要
- 泛型类型使用
- 泛型类型的注意点
- 泛型方法
- 限制类型参数 & 通配符
- 不能使用泛型的一些情况
(2)读前须知
- 本文主要是从Java来谈泛型
- 例子比较简单
- 知识点也比较简单,但是也有很多细节
一、泛型类型
(1)泛型类型引入
①第一个简单的例子
public class Main {
static Integer sum(Integer num1, Integer num2) {
return num1 + num2;
}
public static void main(String[] args) {
sum("string", "字符串"); // 调用一
sum(); // 调用二
}
}
- 不用运行上面的两次调用,你也知道,肯定会报错
- 这只是很简单的两个异常。你看,是不是都提到了类型有误?
②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、解答①
- 如上图所示,只能说左边的继承关系是成立的
- 右边是不构成继承关系的
- 所以,上面的代码是不能赋值成功的
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、解答②
-
类或接口本身有继承关系
-
泛型参数一致的时候,是继承关系
-
即使泛型的参数类型,本身具有继承关系,最终也不能构成继承关系
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、解答③
- 因为
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是吧,但是这是不是有点 ~~~ 哈哈哈