前言
我一直都认为泛型是程序语言设计中一个非常基础, 重要的概念, Java 中的泛型到底是怎么样的, 为什么会有泛型, 泛型怎么发展出来的. 通透理解泛型是学好基础里面中非常重要的. 于是, 我对《Java 编程思想》这本书中泛型章节进行了研读. 可惜遗憾的是, 自己没有太多的经验, 有些东西看了几次也是有点懵. 只能以后有机会, 再进行学习了. 但是自己也理解了挺多的. 下面就是自己对于泛型的理解与感悟. 如有不对, 望指出.
概念
由来: Java 一开始设计之初是没有泛型这个特性的, 直到 jdk 1.5 中引入了这个特性. Java 的泛型是由擦除来实现的. 要知道擦除是什么? 往下看.
概念: 一般的类和方法, 只能使用具体的类型; 要么是基本类型, 要么是自定义的类. 如果要编写可以应用于多种类型的代码, 这种刻板的限制对代码的束缚就会很大. 泛型实现了参数化类型的概念, 使代码应用于多个类型. 泛型在编程语言中出现时, 其最初的目的是希望类和方法具有广泛的表达能力.
简单泛型
有很多原因促成泛型的出现, 其中最重要的一个原因就是为了创造容器类. 我们暂时不指定类型, 而是稍后再决定具体使用什么类型. 要达到这个目的, 需要使用类型参数, 用尖括号括住, 放在类名的后面. 然后使用这个类的时候, 再用实际的类型替换此类型参数. 在下面例子中, T 就是类型参数. 代码如下:
- public class Holder<T> {
- private T a;
- public Holder(T a) {
- this.a = a;
- }
- public void set(T a) {
- this.a = a;
- }
- public T get(){
- return a;
- }
- public static void main(String[] args) {
- Holder<String> h = new Holder<>();
- h3.set("hello");
- String a = h3.get();
- }
- }
- class Automobile{}
但是往往很多的源码中一些通用的类都是有多个泛型参数, 譬如 java.util.function.BiFunction 中就有三个类型参数 T,U,R .
接口泛型
泛型在接口上的运用是非常多的, 例如迭代器 (Iterable) 中的 Iterator .
- public interface Itreator<T>{
- // 判断是否还有元素
- boolean hasNext();
- // 洗一个元素
- E next();
- ....
- }
这个是我们都很常用的吧, 其实接口使用泛型和类使用泛型没什么区别.
泛型方法
泛型方法, 在返回参数类型前面添加泛型参数列表, 由 <> 括着
- eg:
- // 泛型方法 两个泛型参数, T,R 返回类型为 R
- public <T, R> R test(T t, R r){
- return r;
- }
泛型方法使得该方法独立于类而产生变化. 在需要编写泛型代码的时候, 基本的的指导原则是: 无论何时, 只要你能做到, 就尽量使用泛型方法. 意思是如果使用泛型方法可以代替整个类的泛型化, 那就用泛型方法, 因为它可以使事情更加清楚明白. 另外对于 static 的方法而言, 无法访问泛型类的类型参数, 所以如果 static 方法需要使用泛化能力, 就必须使其成为泛型方法.
泛型的擦除
在看 《Java 编程思想》 中泛型章节中 '擦除的神秘之处' 这一小节的时候, 看的我特别的晕晕乎乎的, 然后再往下面看时就越来越混了. 特别是看到'边界','通配符' 这块了就有点懵了. 首先看下什么是擦除. 在泛型代码内部, 无法获得有关泛型参数类型的信息. Java 的泛型是由擦拭来实现的, 这意味着当你使用泛型的时, 任何具体的类型都被擦除了, 你唯一知道的就是你在使用一个对象. 由于 Java 一开始没有引入 泛型这个特性, 在为了兼容 JDK 老版本的情况下. 擦除是 Java 泛型实现的一种折中. 因此你在运行时 List 和 List 是一样的, 注意是在运行时, 但是在编译时, List 表示这个 String 类型的 List 容器, List 表示这个时 Integer 类型的 List 容器. 举个例子, 例子来源于 Java 编程思想
- #include <iostream>
- using namespace std;
- temple<class T> class Manipulator{
- T obj;
- public :
- Manipulator(T x){obj = x;}
- void manipylate() {obj.f();}
- };
- class HasF{
- public:
- void f(){cout<<"HasF::f()"<<endl;}
- };
- int main(){
- HasF hf;
- Manipulator<HasF> manipulator(hf);
- manipulator.manipylate();
- }
输出 HasF:f()
Manipulator 类存储了一个类型 T 的对象, 在 manipylate 方法里, 他在 obj 上调用了方法 f() ; 它是怎么知道 f() 是类型参数 T 中有的方法呢? 当你实例化这个模板的时, c++ 编译器将进行检查, 如果他在 Manipulator 被实例化的这刻, 它看到 HasF 如果拥有这个方法 f(), 就编译通过, 否则不通过. 这个代码就算不会 c++ 的人应该也看的懂吧. 我们看下 Java 的版本:
- public class HasF{
- public void f(){
- System.out.println("HasF::f()");
- }
- }
- class Manipulator<T>{
- private T obj;
- public Manipulator(T obj){
- this.obj = obj;
- }
- public void manipulator(){
- // 错误: 这个是不可以调用 f() 这个方法的.
- // obj.f();
- }
- public static void main(String[] args){
- HasF hf = new HasF();
- Manipulator<HasF> manipulator = new Manipulator<>(hf);
- manipulator.manipulator();
- }
- }
看到没有, Java 中有了擦除, Java 编译器不知道 obj 中有没有 f() 这个方法的事情.
泛型的边界
T extends Class
Java 中的泛型, 在编译时, T 代表一种类型, 如果没有指定任何的边界, 那么他就相当于 Object . 我们可以通过 extends 关键字, 给泛型指定边界. 上面代码我们为了能够调用 f(), 我们可以协助泛型类, 给定泛型类的边界, 告诉编译器必须接受遵循这个边界的类型. 这里用 extends 关键字. 将上面代码改成
- public class HasF{
- public void f(){
- System.out.println("HasF::f()");
- }
- }
- class Manipulator<T extends HasF>{
- private T obj;
- public Manipulator(T obj){
- this.obj = obj;
- }
- public void manipulator(){
- obj.f();
- }
- public static void main(String[] args){
- HasF hf = new HasF();
- Manipulator<HasF> manipulator = new Manipulator<>(hf);
- manipulator.manipulator();
- }
- }
输出: HasF::f()
这样子在编译的时候就相当告诉编译器 Manipulator 类中 obj 的参数类型 为 HasF 或着它的子类.
? extends T
? 是泛型表达式中的通配符. ? extends T 表示: T 数据类型或着 T 的子类数据类型. 举个例子
- class Vegetables{}
- // 白菜
- class Cabbage extends Vegetables{}
- // 小白菜
- class Pakchoi extends Cabbage{}
- // 大蒜
- class Garlic extends Vegetables{}
- public class Main{
- public static void main(String[] args) {
- // 这个是会报错的.
- // ArrayList<Vegetables> vegetables = new ArrayList<Cabbage>();
- }
- }
上面的中 main 方法里面是报错的因为, ArrayList 表示这个 ArrayList 容器中只能存放蔬菜, new ArrayList() 表示 这个使一个只能放白菜的容器. 这两个容器是不可以相等的, 类型不一样. 但是我们可以通过 ? extends T 来解决这个问题, 代码如下:
- public class Main{
- public static void main(String[] args) {
- ArrayList<? extends Vegetables> vegetables = new ArrayList<Cabbage>();
- // 报错 不能添加白菜进去
- // vegetables.add(new Cabbage());
- }
- }
我们可以用 vegetables 表示 new ArrayList(), 这是向上转型. 但是, 为什么 vegetables.add(new Cabbage()) ; 会报错, 因为 ArrayList<? extends Vegetables> 表示这个 ArrayList 容器中能够存放任何蔬菜. 但是 ArrayList 具体是什么容器, 完全不知道, add 的时候, 你添加什么东西进去到这个容器中都是不安全的. 这个时候, 我们可以用 ? super T 来进行操作, 具体往下看.
? super T
? super T 表示: T 数据类型 或着 T 的超类数据类型, super 表示超类通配符. 上面代码可以用以下表示
- public class Main{
- public static void main(String[] args) {
- ArrayList<? super Cabbage> cabbages = new ArrayList<Vegetables>();
- cabbages.add(new Cabbage());
- cabbages.add(new Pakchoi());
- // cabbages.add(new Vegetables());
- System.out.println(cabbages);
- }
- }
上面 ArrayList<? super Cabbage> 表示 ArrayList 这个容器中怎么都可以存放 Cabbage 以及 Cabbage 子类的数据类型. cabbages 指向的是 蔬菜的容器类. add 进去的是白菜以及白菜的子类型数据. 这个当然是支持的.
? extends T VS ? super T
? extends T ,? super T 一般用于方法参数列表.
- public class Main{
- public static void main(String[] args) {
- List<Cabbage> cabbages = new ArrayList<>();
- cabbages.add(new Cabbage());
- cabbages.add(new Cabbage());
- cabbages.add(new Cabbage());
- extendsTest(cabbages);
- List<? super Cabbage> list = superTest(new ArrayList<Vegetables>());
- System.out.println(list);
- }
- public static void extendsTest(List<? extends Vegetables> list){
- for (Vegetables t : list){
- System.out.println(t);
- }
- }
- public static List<? super Cabbage> superTest(List<? super Cabbage> list){
- list.add(new Cabbage());
- list.add(new Pakchoi());
- return list;
- }
- }
? extends T 表示消费者 list 然后把里面的数据消费掉.
? super T 表示生产者 传入一个 list 然后往里面添加数据, 进行其他操作.
总结
对于 ? extends Class ,? extends T,? super T, 不是很理解的, 可以自己把例子写一下, 然后想一想. Java 泛型的特性在很多开源的框架上是用的非常多的. 这快需要深入的理解一下, 我想随着敲代码的年限上, 应该到了后面会有不一样得理解吧. 现在通过书上能够知道, 理解得就只有这么多了.
来源: https://www.cnblogs.com/Krloypower/p/10454507.html