如何深入理解Java泛型

一、泛型的作用与定义

1.1泛型的作用

  • 使用泛型能写出更加灵活通用的代码
    泛型的设计主要参照了C++的模板,旨在能让人写出更加通用化,更加灵活的代码。模板/泛型代码,就好像做雕塑时的模板,有了模板,需要生产的时候就只管向里面注入具体的材料就行,不同的材料可以产生不同的效果,这便是泛型最初的设计宗旨。

  • 泛型将代码安全性检查提前到编译期
    泛型被加入Java语法中,还有一个最大的原因:解决容器的类型安全,使用泛型后,能让编译器在编译的时候借助传入的类型参数检查对容器的插入,获取操作是否合法,从而将运行时ClassCastException转移到编译时比如:

List dogs =new ArrayList();
dogs.add(new Cat());
在没有泛型之前,这种代码除非运行,否则你永远找不到它的错误。但是加入泛型后

List<Dog> dogs=new ArrayList<>();
dogs.add(new Cat());//Error Compile
会在编译的时候就检查出来。

  • 泛型能够省去类型强制转换
    在JDK1.5之前,Java容器都是通过将类型向上转型为Object类型来实现的,因此在从容器中取出来的时候需要手动的强制转换。

Dog dog=(Dog)dogs.get(1);
加入泛型后,由于编译器知道了具体的类型,因此编译期会自动进行强制转换,使得代码更加优雅。
1.2泛型的定义
自JDK 1.5 之后,Java 通过泛型解决了容器类型安全这一问题,而几乎所有人接触泛型也是通过Java的容器。那么泛型究竟是什么?
泛型的本质是参数化类型
也就是说,泛型就是将所操作的数据类型作为参数的一种语法。

public class Paly<T>{
    T play(){}
}

其中T就是作为一个类型参数在Play被实例化的时候所传递来的参数,比如:

Play<Integer> playInteger=new Play<>();

这里T就会被实例化为Integer

二、通配符与嵌套

2.1通配符
?:表示类型不确定,只能用于声明变量或者形参上,不能用在创建泛型类,泛型方法和接口上

public static void main(String[] args) {
    List<?> list=new ArrayList<Integer>();
    list=new ArrayList<String>();
    test(list);
}
    
public static void test(List<?> list){
        
}

2.2泛型嵌套

定义两个泛型类,Myclass类的泛型就是student类,而student类的泛型是String类

class Student<T>{
    private T score;
    public T getScore(){
        return score;
    }
    public void setScore(T score){
        this.score=score;
    }
}
 
class MyClass<T>{
    private T cls;
    public T getCls(){
        return cls;
    }
    public void setCls(T cls){
        this.cls=cls;
    }
}
 
public static void main(String[] args) {
    Student<String> stu=new Student<String>();
    stu.setScore("great");
 
    //泛型嵌套
    MyClass<Student<String>> cls=new MyClass<Student<String>>();
    cls.setCls(stu);
    Student<String> stu2=new Student<String>();
    stu2=cls.getCls();
    System.out.println(stu2.getScore());//great
 
}

如上就实现了泛型的嵌套,在HsahMap中对键值对进行便利的时候,也利用了泛型的嵌套

public static void main(String[] args) {
        
    Map<String,String> map=new HashMap<String,String>();
    map.put("a", "java300");
    map.put("b", "马士兵javase");
        
    Set<Entry<String,String>> entrySet=map.entrySet();
    for(Entry<String,String> entry:entrySet){
        String key=entry.getKey();
        String value=entry.getValue();
    }
        
}

三、泛型的上下边界

? extends E 是 泛型 的上边界 , ? super T 是 泛型的下边界
3.1 < ? extends E >
List < ? extends A > 表示 这个list里面存的是A的子类,具体是啥不知道,只知道范围!
那可以设定分两个范围: A子类,A父类。
对于add:
A子类情况:如果你允许add的元素是A的子类,那么因为泛型参数限定? extends A,所以ArrayList可以指定的类型也是A的子类,那么无法保证add的对象一定ArrayList指定类型的子类对象,比如: ArrayList指定C, list却add了A()对象–这就是非法的!(因为list并不知道指定了C,它可以确定的范围就是可以add所有A的子类。)所以A子类该范围不行!
A父类情况:ArrayList指定类型肯定实在(?,A]范围内,所以这种情况肯定不行!
对于get:
list不知道你存入ArrayList是什么类型,但是我可以确定使用A的引用肯定可以接受ArrayList中的元素(无论ArrayList设定什么类型),因为A肯定是里面元素的父类!但是你使用其他类型接受就不行了,因为如果ArrayList的类型是你指定的那个类型的父类,是接受不了的。

例子:BaseStudent extends Student 。

代码如下:

package com.demo.main;
 
import java.util.ArrayList;
import java.util.TreeSet;
 
public class Main {
 
    public static void main(String[] args) {
 
        ArrayList<Student> list1 = new ArrayList<>() ;
        list1.add(new Student("1",1));
        ArrayList<Student> list2 = new ArrayList<>(); 
        list2.add(new Student("2",2));
        list1.addAll(list2);
        System.out.println(list1);//[Student [name=1, age=1], Student [name=2, age=2]]
        ArrayList<BaseStudent> list3 = new ArrayList<>() ;
        list1.add(new Student("3",3));
        list1.addAll(list3);
        System.out.println(list1);//[Student [name=1, age=1], Student [name=2, age=2], Student [name=3, age=3]]
    }
 
}

list3<BaseStudent> 和 list2<Student> 的 对象元素 都添加到了 list1<Student>中了。
3.2< ? super E >
List < ? super C > 表示list里面存的是C和其父类,具体是啥不确定,只知道范围。

  • 同样的分为:C子类,C父类。(其实应该发现了没必要分为两类,有一类是肯定不行的)。
  • add:
  • C子类:如果允许add,就允许list来add任何C的子类元素,因为ArrayList指定的范围是C和其父类,所以ArrayList可以准确接受该假设范围的所有对象。 所以,C子类这个范围可行,这样就有一个标准的类型判断依据,不像extends,根本无法确定判断标准,编译器就不会知道怎么做。
  • C父类:显然和上面A子类情况是一样的!因为list无法知道ArrayList指定类型 ,所以无法确定判断标准。
  • get:
  • 因为list只允许add C的子子类对象,你或许也在想:那我直接用C不就可以接受ArrayList数据吗?并不是的,ArrayList在赋给list之前可能里面已经有值了,这个值的类型如果是ArrayList指定类型,并且是C的父类,那么C就无法接受! 而且list不知道ArrayList会指定哪个类型,只知道范围,所以无法确定是哪个父类,所以干脆用Object那肯定就能接受了,所以返回的其实只有Object能接受,否则就得强转。

四、 RxJava中深入理解泛型

<meta charset="utf-8">

4.1 响应式编程:

与我们传统编码(函数式编程)不一样,传统编码是做完这件事之后做另外一件事,给人的感觉都是单线程的,可能会开新线程去
处理耗时操作,在处理完成之后通过回调去处理之后的事情
而响应式编程提供给我们的是一种不一样的思想,在响应式编程的世界中一切执行流程都是基于事件的,已事件为驱动

4.2观察者模式:

观察者模式是这样子的,我先举个例子看大家能不能理解
老师在讲台上讲课,而所有的学生都会观察着老师的一举一动,而老师每产生一个事件(比如说在黑板上写下一串公式),则对应着所有的学生都观察到了老师的这一举动,自己则在自己的笔记本中记录,大脑中进行思考.而老师却不关心自己的学生对这一举动做什么事.
好了,例子就是这样的,我们来分析一下这个例子跟观察者模式有个什么关系?
这个例子中,老师可以产生事件,学生观察着老师,而老师在产生事件之后咳嗽一下,通知所有的学生,我刚才做了什么事,你们应该也需要做点自己的事情
而这就产生了几个概念,观察者,被观察者,事件,事件的处理与消费
被观察者中存在观察者的引用,即教师知道自己要通知的学生都有谁
被观察者在产生事件之后通知观察者,即教师产生事件之后通知每一位观察着自己的学生

4.3RxJava是对观察者模式的一种高级运用,或者说是一种升级,他把观察者模式具体化,更加明确了各个对象之间的关系

四个基本概念:
Observable (可观察者,即被观察者)、
Observer (观察者)、 
subscribe (订阅)、事件。
Observable 和 Observer 通过 subscribe() 方法实现订阅关系,从而 Observable 可以在需要的时候发出事件来通知 Observer。

谈完了响应式的一些东西,我觉得既然要讨论学习泛型的使用,我们就把泛型的一些概念也揪出来瞅一下

泛型分为:
    1 : 自定义泛型接口   interface Observer<T>
    2 : 泛型类           class ImplObserver<T> implements Observer<T>
    3 : 泛型方法         <T> Observer<T> call(T t) 

说一下泛型的作用域
如果将泛型声明放在泛型接口,泛型类上,则该泛型在该类中就是确定的了,如果将泛型声明放在了泛型方法上,则该泛型只在该方法中有效,如果泛型方法上声明的泛型类型和类或接口中声明的泛型一致,则会在该方法中隐藏类或接口上的泛型

贴个代码看一下

将泛型声明放在接口
public interface Observable<T> {
    public T call();
}
将泛型声明放在方法
public interface Observable2 {
    <T> T call(T t);
}
泛型声明在接口或类上,则类或接口中的方法均可使用T类型
public class ImplObservable<T> implements Observable<T>{
    @Override
    public T call() {
        // TODO Auto-generated method stub
        return null;
    }
}
泛型声明在方法上,则除去该声明有T泛型的方法之外,其他方法不识别T类型
public class ImplObservable2 implements Observable2{
    @Override
    public <T> T call(T t) {
        // TODO Auto-generated method stub
        return null;
    }
}

public static void main(String[] args) {
    //将泛型声明在接口上或声明在类上
    Observable<Student> observer = new ImplObservable<Student>();
    Student student = observer.call();
    //将泛型声明在方法上
    ImplObserver2 Observable2 = new ImplObservable2();
    Student student2 = observer2.call(new Student());
}

4961238-5b44641c89c668df.png
image

大概了解一下泛型的作用域和泛型的类型之后,我们现在有这么一个需求
我给你一个对象,你能够观察着该对象,即一个观察者中存在着该对象的引用,并且将该观察者返回给我
我刚开始是这么想的,我们看一下有没有什么问题

public class ImplObservable<T> implements Observable<T>{
    T t;
    public ImplObservable(T t){
        this.t = t;
    }
}

看代码的话好像确实也没什么问题,我把泛型的声明放在了类上,那我这个类中都是可以识别T类型的,那我在创建对象的时候传入T好像也没什么不对,一样完成了需求,我们回到创建该对象的main方法中去看一看,创建方法变成了这样

ImplObservable<Student> observer = new ImplObservable<>(new Student());
如果我把<>删除掉,则编译器会给我们这样一个警告
Type safety: The expression of type ImplObservable needs unchecked conversion to conform to ImplObservable<Student>
类型不安全?怎么会不安全?并没有报错啊..
事情是这样的,在ImplObserver中,我们将泛型声明放在了类上,在该类中都可以识别T类型了,但是,构造方法接受一个T类型,如果你在创建该对象的时候,没有向该类声明T类型究竟属于哪种类型,就直接传递了一个实际类型过去,问题就像这样,教室接受所有类型过来,可能是教师,也可能是学生,但是,你在创建该教室的时候,你对教室接受的类型进行了限制,但是你又没有通知教室说教室准确的要接受哪种类型的对象,这就会造成泛型不安全

我去翻了翻Rxjava的源码,他将Observable这个对象的构造函数的访问权限降低了,不在他包下都不可以创建这个对象,但是他提供了一个create方法去创建,我们也来模仿一下

public class ImplObservable<T> implements Observable<T>{
    T t;
    private ImplObservable(T t){
        this.t = t;
    }
    public static <T> Observable<T> create(T t) {
        return new ImplObservable<T>(t);
    }
}

创建方法变成了这样

Observable<Student> create = ImplObservable.create(new Student());

这样我们在使用ImplObserver的时候就没有对这个类的泛型进行明确说明,而是在create方法中进行了声明,怎么声明的? 这里面还有点门道,我们将create方法定义成了静态方法,并且在该方法上声明了T类型,这样该方法的T类型就会隐藏掉类上的T类型,但是,我们的create方法做了这么一件事,将静态方法的泛型,传递给了ImplObservable类上的泛型,并且返回创建好的ImplObservable泛型对象,此处的泛型类型为create方法声明的泛型类型

是不是有点晕了?我当时也是晕的不行,迷糊过来之后也就那样吧..如果有迷糊的朋友在下方评论吧,指出你的问题,我们一起讨论

现在来考虑Rxjava写代码舒服的原因,全链式,全链式啊有木有,一条道走到黑,就是在不停的调用调用调用,不需要我们去考虑返回的对象是什么对象,只需要进行一系列操作就可以了,因为泛型已经帮助我们做了太多太多.

链式?哇,链式调用好像是很牛逼的,我们也来实现一下.

先说一下需求:

现在我给你一个student对象,你把这个对象给我通过某种规则给转换成teacher对象,并且!
你要给我返回的观察者不在是观察学生了,而是,你刚才转换成的teacher对象,并且!
我要求这些都是链式操作,起码我看起来比较舒服,写起来也比较开心!

说实话我是在学习泛型,研究Rxjava,我为啥非得给自己找不自在,提出的需求比较恶心就算了,还并且,俩并且,完成功能不就行了吗?追求那么点的链式可能会给我的工作,我的业余时间带来什么呢?
好了,我们来分析一下需求:

现在给一个student对象,要返回一个观察着student的观察者,我们通过上面的代码可以这样创建
ImplObservable.create(new Student());
现在要把这个学生通过某种规则转换成teacher
做一个接口回调,传递学生类型进去,返回老师类型,但是这俩类型不明确,应该用泛型
我们模仿Rxjava的命名,也叫作Func1,
public interface Func1<T,R> {
    R call(T t);
}
接口做好了,我们现在要在Observer中去定义一个方法,将T类型转换成R类型,为了保持和Rxjava的一致,我们也叫作map
并且该方法要接受一种规则,一种能够将T转成R的规则
方法声明也有了
<R> Observer<R> map(Func1<T,R> fun1);
我们要在ImplObserver中去实现该方法了
@Override
public <R> Observer<R> map(Func1<T, R> fun1) {
    // TODO Auto-generated method stub
    Observer<R> ob = ImplObservable.create(fun1.call(t));
    return ob;
}
实现完了是这样子的...

可能你看这点代码会比较恶心,甚至会吐..

先喝杯水,起来晃悠一下,放松一会,希望你待会能打起十二分精神来读接下来的一丁点篇幅
我会认真将自己的理解全部写出来.

1:
    创建被观察者即ImplObservable.create(new Student());这时候我们要把Student这个对象存储起来方便之后使用,但是create是静态方法,
有声明泛型T,但是ImplObservable又是被泛型声明的泛型类,在create的时候去创建真正的被观察者,并且将create方法携带的泛型类型带过去,即被观察者中的泛型来自于create方法的泛型.
而ImplObservable的构造方法要求传入一个T类型,并且该类中存在一个T t的引用,即保存create方法传递过来的实际对象的引用

现在我们搞清楚了一个被观察者中的实际对象(T对象)究竟存储在了哪,一个成员变量T t中

2: 
    现在我们要想办法把一个存储有t对象的被观察者转换成一个存储有另外一个t对象的被观察者,我们提供一个map操作,代表类型的转换操作
    map要怎么实现是我们现在重点思考的问题
    既然ImplObservable中可以存储t对象,一个ImplObservable对应一个T类型,也就意味着一个ImplObservable存储的这个t对象的类型已经确定,
    那么我们要怎么把一个T对象转换成R对象,转换规则是怎么样的
    public interface Func1<T,R> {
        R call(T t);
    }
    定义这么一个接口,接受一个T类型,返回一个R类型,在call方法中编写转换规则.
    那么map方法就必然要接受一个接口了,即转换规则
    我们暂且这样定义map方法
    <R> Observable<R> map(Func1<T,R> fun1);
    既然map方法也有了转换的规则
    map的实现就这样了
    @Override
    public <R> Observable<R> map(Func1<T, R> fun1) {
    Observable<R> ob = ImplObservable.create(fun1.call(t));
    return ob;
    }
    至于为什么这么做?
    现在我们知道ImplObservable.create方法接受一个T类型,并且把T类型存储到当前对象中去,叫做t,这里是没毛病的
    我们来回想一下Func1这个接口的泛型声明,接受T,返回R.
    call方法接受T,返回R
    这就意味着我们的ImplObservable.create方法接受的就是一个R类型!!!
    并且ob对象中存储的那个T t类型,实际上就应该是R r对象,即Teacher对象
    这时候我们返回了ob,一个存储有R(teacher)对象的被观察者
    至此,student转换为teacher才真正结束.

4961238-5f9cbca6c8a3a5e8.jpg
image
    好像是有点晕,好吧,回头我画个图在说一下....

好了放松一下吧...确实比较恶心,也有点绕口,烧脑,但是想通了也就是那么一回事...

现在再来定义一个操作符,我们就结束今天这篇文章了

需求是这样的

我需要在被观察者的执行过程中改一下被观察者中存在的对象的属性
并且不能破坏链式
我只是修改属性,我要的还是该被观察者

分析一下:

一个接口回调,需要把被观察者保存的对象给传递回来,返回的结果不关心,即(void)

代码实现:

//声明下一步做的事
Observable<T> doOnNext(Action<T> action);
//定义泛型接口
public interface Action<T> {
 void callAction(T t);
}

实现doOnNext方法
@Override
public Observable<T> next(Action<T> action) {
   action.callAction(t);
   return this;
}
解释一下
当前被观察者中已经存在T对象的引用即t,只需要将t回调过去,在外部类中进行修改,
但是被观察者是不改变的,直接返回this就可以了.

最后上一下测试代码

public static void main(String[] args) {

    Student student = new Student();
    System.out.println("创建好student : " + student);
    final Teacher teacher = new Teacher();
    System.out.println("创建好teacher : " + teacher);

    ImplObservable.create(student)
    .map(new Func1<Student, Teacher>() {

        @Override
        public Teacher call(Student t) {
            // TODO Auto-generated method stub
            System.out.println("student hashcode : " + t);
            System.out.println("teacher hashcode : " + teacher);
            return teacher;
        }
    })
    .doOnNext(new Action<Teacher>() {

        @Override
        public void callAction(Teacher t) {
            // TODO Auto-generated method stub
            System.out.println("teacher hashcode2 : " + t);
        }
    });
}

输出结果

    创建好student : com.lxkj.learn.Student@95cfbe
    创建好teacher : com.lxkj.learn.Teacher@1950198
    student hashcode : com.lxkj.learn.Student@95cfbe
    teacher hashcode : com.lxkj.learn.Teacher@1950198
    teacher hashcode2 : com.lxkj.learn.Teacher@1950198

参考:https://www.jianshu.com/p/6130ff181d48
https://blog.csdn.net/Justin_zhao/article/details/77750440
https://blog.csdn.net/Ameir_yang/article/details/81514078
https://blog.csdn.net/Bazingaea/article/details/51150380

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值