美文网首页
RxJava2中的Observable.just函数的调查

RxJava2中的Observable.just函数的调查

作者: andyXiao24 | 来源:发表于2018-03-18 20:42 被阅读0次

    最近在研究RxJava和Java 8中的Stream的区别,具体的研究结果在下一篇文章中给出。但是在研究的过程中,发现Observable.just函数比较奇怪,如下所示:


    image.png

    可以看到just函数是一个泛型函数,而且提供了多个函数重载,而且仅仅是参数的个数不同,深入源码后我发现参数最多的函数有十个参数:

    public static <T> Observable<T> just(T item1, T item2, T item3, T item4, T item5, T item6, T item7, T item8, T item9, T item10) {
            ObjectHelper.requireNonNull(item1, "The first item is null");
            ObjectHelper.requireNonNull(item2, "The second item is null");
            ObjectHelper.requireNonNull(item3, "The third item is null");
            ObjectHelper.requireNonNull(item4, "The fourth item is null");
            ObjectHelper.requireNonNull(item5, "The fifth item is null");
            ObjectHelper.requireNonNull(item6, "The sixth item is null");
            ObjectHelper.requireNonNull(item7, "The seventh item is null");
            ObjectHelper.requireNonNull(item8, "The eighth item is null");
            ObjectHelper.requireNonNull(item9, "The ninth item is null");
            ObjectHelper.requireNonNull(item10, "The tenth item is null");
    
            return fromArray(item1, item2, item3, item4, item5, item6, item7, item8, item9, item10);
        }
    

    而且每个just函数的实现逻辑都是类似的,首先判断传入的item是不是null,然后调用fromArray函数。

     public static <T> Observable<T> fromArray(T... items) 
    

    其实完全可以不用这样实现,直接把just函数申明为:

     public static <T> Observable<T> just(T... items) 
    

    然后在实现的时候用for循环判断传入的参数是否是null,最后调用fromArray函数就可以满足可变参数的要求,完全可以不用这么繁琐。可是为什么Jack大神这么去实现呢?

    网上关于这个方面的讨论特别少,在stackoverflow上面找到了这个问题:just methods in RxJava,可惜没有回复,但是有人提供了这个github链接。在这个链接里面,Jack大神亲自回复了,但是没有实质性的内容,另一个回答者提到了:

    First, those methods exist so you don't have to suppress the unchecked exception from using varargs on a generic type for typical 1-9 argument cases. Second, if items is long, you have two loops over it instead of one. Third, the proposed method doesn't tell which element was null. Fourth, that just(...) would cause ambiguity problems with the other existing ones; this is why fromArray has been introduced.

    也就是说,

    1. 这些方法可能会导致warning,你需要手动suppress warning,这样比较麻烦;
    2. 如果方法太长,我们需要做两次for循环,可能这样对于一个类库来说会有性能问题;
    3. 如果用for循环,不能够给出详尽的报错信息;
    4. just函数可能会二义。

    首先,第4点不成立,因为我们可以随时换一个函数名字;关于第2,3点,我写了如下代码:

    public static <T> void justTen(T item1, T item2, T item3, T item4, T item5, T item6, T item7, T item8, T item9, T item10) {
            ObjectHelper.requireNonNull(item1, "The first item is null");
            ObjectHelper.requireNonNull(item2, "The second item is null");
            ObjectHelper.requireNonNull(item3, "The third item is null");
            ObjectHelper.requireNonNull(item4, "The fourth item is null");
            ObjectHelper.requireNonNull(item5, "The fifth item is null");
            ObjectHelper.requireNonNull(item6, "The sixth item is null");
            ObjectHelper.requireNonNull(item7, "The seventh item is null");
            ObjectHelper.requireNonNull(item8, "The eighth item is null");
            ObjectHelper.requireNonNull(item9, "The ninth item is null");
            ObjectHelper.requireNonNull(item10, "The tenth item is null");
        }
        private static final String[] errors = new String[] {
                "The first item is null",
                "The second item is null",
                "The third item is null",
                "The fourth item is null",
                "The fifth item is null",
                "The sixth item is null",
                "The seventh item is null",
                "The eighth item is null",
                "The ninth item is null",
                "The tenth item is null"};
    
        public static <T> void just(T ... items) {
            ObjectHelper.requireNonNull(items, "Items should not be null");
            int itemsLength = items.length > 10 ? 10 : items.length;
            for(int i = 0; i < itemsLength; ++i) {
                ObjectHelper.requireNonNull(items[i], errors[i]);
            }
        }
    
        public static void testJustTen() {
            StopWatch stopWatch = new StopWatch();
            stopWatch.start();
            for (int i = 0; i < 10000; ++i) {
                justTen(1,2,3,4,5,6,7,8,9,0);
            }
            stopWatch.stop();
            System.out.println(stopWatch.getTotalTimeMillis());
        }
    
        public static void testJust() {
            StopWatch stopWatch = new StopWatch();
            stopWatch.start();
            for (int i = 0; i < 10000; ++i) {
                just(1,2,3,4,5,6,7,8,9,0);
            }
            stopWatch.stop();
            System.out.println(stopWatch.getTotalTimeMillis());
        }
    
        public static void main(String []args) {
            testJust();
            testJustTen();
        }
    
    callTimes testJust testJustTen
    1000 4 1
    100000 25 8
    10000000 191 12
    1000000000 9618 12

    从而可以看出,性能问题是考虑的一个方面,使用循环明显会带来性能问题,但是只有调用量到10万次左右才会出现,不知道是不是Observable的大数据量的使用情况比较多,我认为这是just函数使用10个重载的原因之一。

    下面找点讨论一下第1点:you don't have to suppress the unchecked exception from using varargs on a generic type for typical 1-9 argument cases, 直白的翻译就是“当你使用泛型的可变参数的时候,不需要suppress unchecked exception", "unchecked exception"是一种类型的warning,那么什么样的代码才会给出这样的warning呢?

    image.png

    但是下面代码就不会有这样的warning

    Observable.just(1,2,3);
    

    我认为这就是just函数写了十个重载函数的原因之二:避免了不必要的warning,让调用者觉得不舒服。

    那么问题来了,为什么编译器会给出一个warning,为什么编译器觉得创建一个泛型的数组是需要提醒程序员的?

    代码如下:

     public static <T extends List<Integer>> void varArgGenericFunction(T ... items) {
            Object[] tempObjArr = items;
            tempObjArr[0] = Lists.newArrayList("Good");
            Integer tempElement = items[0].get(0);
            System.out.println(tempElement);//ClassCastException
        }
    
        public static void main(String []args) {
            varArgGenericFunction(Lists.newArrayList(1));
        }
    

    运行这个函数可以得到以下Exception:

    Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
        at aab.rxjava.vs.stream.Application.varArgGenericFunction(Application.java:16)
        at aab.rxjava.vs.stream.Application.main(Application.java:21)
    

    其实Java本身不允许创建带泛型类型的数组对象(上面的代码也说明了为什么不允许创建泛型的数组,因为及其容易导致ClassCastException),items是编译器为了实现变长参数而帮你创建的一个泛型数组。
    也许你会说没有人会这么傻写出这样的代码吧,但是实际上Java的调用链可能很长,谁也不能够预测到会出现什么样的代码,但是这个异常的起点就是varArgGenericFunction函数的调用,因此编译器给出这样的报警也是可以理解的。

    其实你也可以认为这是Java设计的bug,为什么Java要允许像

     Object[] tempObjArr = items;
    

    这样的代码存在?明明items是一个List<Integer>类型的数组,为什么允许一个Object类型的数组指向它?
    如果不是Java允许这个赋值存在,那也就不会引起接下来的所有的问题了。

    这个其实在程序语言设计中有专门的名词,叫做协变。

    那么问题来了,什么叫做协变?

    详细的解析可以参见这篇文章。协变(Co-variance)属于形变(Variance)的一种,形变包括协变,逆变和不变。简而言之,就是如果A是B的子类,那么f(A)也是f(B)的子类,就叫做协变;如果f(B)是f(A)的子类,就叫做逆变;如果f(A)和f(B)没有关系就叫做不变。
    现在我们有这样的继承关系

      class Fruit {}
      class SweetFruit extends Fruit {}
      class Apple extends SweetFruit{}
    

    在Java中,数组关系是协变的,也就是说可以这样写

    Fruit[] fruitArray = new Apple[10];
    

    而泛型对象则是不变的,也就是说,如果这样写:

    ArrayList<Fruit> fruitList = new ArrayList<Apple>();
    

    编译器会报错,因为在编译器看来ArrayList<Fruit>和ArrayList<Apple>是没有关系的两个类型。但是对于人的常识来说这是违背的:我一个装水果的list,不可以用来放苹果?(我是苹果,我又不是番茄。。。),因此给大家带来一个小插曲。

    PECS(Producer extends consumer super)原则

    正如上面说的,我一个装水果的list不可以用来放苹果?!因此Java推出了下面的写法:

    List<? extends Fruit> fruitList = new ArrayList<Apple>();
    List<? super Apple> appleList = new ArrayList<Apple>();
    

    extends的意思是,这个list中的东西是什么,我不知道,但是肯定是Fruit的子类,因此JDK对它的限制是:只能够从里面那东西出来,而且只能将取出来的内容赋值给Fruit及其父类;而且不能够往里面放东西,因为里面可能存放的是Apple,但是你却可能放一个Banana进来,所以干脆不让放东西;
    super的意思是,这里list中的东西是什么,我也不知道,但是肯定是Fruit及其父类,因此JDK对它的限制:只能够向里面放东西,而且只能够放Fruit及其子类;不能够从里面拿东西,因为里面是Fruit的父类,Fruit的父类多了去了,我也不知道是什么类型,除非你拿出来给一个Object。
    这篇文章解释得比较全面,尤其是最后一个JDK中的super,extends的例子:

    public static <T> void copy(List<? super T> dest,List<? extends T> src)
    // 调用方式如下:
    Collections.copy(new ArrayList<Number>(), new ArrayList<Number());
    Collections.copy(new ArrayList<Number>(), new ArrayList<Integer());
    Collections.copy(new ArrayList<Object>(), new ArrayList<Number>());
    Collections.copy(new ArrayList<Object>(), new ArrayList<Double());
    

    从copy函数的签名我们就可以看出来,src只允许取东西出来,而且取出来的一定是T或者T的子类,我可以把它交给T temp;dest里面是T或者T的父类,所以只允许放东西进去,而且只可以放T或者T的子类,刚好把temp放进去,这就是PECS原则:src是用来“拿出来”的(produce),所以用extends,而dest是用来“放进去”的(consume),所以用super。
    这里所说的取出来和放进去只是一种比较形象的说法,如果我们自己实现了一个泛型类,那么取出来和放进去指的是什么呢?
    如下代码所示:

        public class MyContainer<T> {
            T getThingsOutofContainer(){return null;}
            void putThingsIntoContainer(T thing){}
            T getAndPut(T thing){return thing;}
        }
    

    getThingsOutofContainer函数没有入参(或者更加精确地说,没有T类型的入参),而且出参类型是T,因此这个函数就可以被视为“取出来”,putThingsIntoContainer函数有T类型的入参,没有T类型的返回值,因此这个函数可以被视为“放进去”,最后一个函数getAndPut,无论是在super还是extends的情况下都不可以使用。

    讲完了PECS原则,我们继续我们的讨论。

    为什么允许数组协变?

    因为早在SE5的年代,Java还没有泛型的概念,因此有许多概念是妥协的结果,就比如说是数组的协变。知乎上面也有讨论:java中,数组为什么要设计为协变?
    至于为什么首先引入了协变来解决来妥协,而不是引入泛型来妥协,这个暂时还没有找到相关文献。
    票数最高的答案提到,数组的协变也会带来不可预期的结果,如下代码:

    Object [] objects = new Integer[10];
    objects[0] = new Object();
    

    错误如下:

    Exception in thread "main" java.lang.ArrayStoreException: java.lang.Object
        at Main.main(Main.java:19)
    

    这是得益于Java对数组的严格控制,每个数组在申明之初就规定了放进去的元素的类型,如果不满足这个条件,就会抛出ArrayStoreException
    其实可以看出来这个exception和我们文章开始时提到的“不允许建立泛型数组是因为潜在的ClassCastException”是一个性质,为什么Java允许ArrayStoreException而坚决杜绝潜在的ClassCastException呢?这个我也没有相关的文献,不过从代码上面可以看出,ArrayStoreException出现的比ClassCastException提前更多,从而更加容易发现错误。

    至此,这个调查就结束啦。总结一下:

    1. RxJava2的just函数签名很奇怪,调查之后发现是原因之一在于:如果按照通常的写法,会出现“unchecked generic array creation for varargs parameter" warning。
    2. 不允许创建泛型数组的原因在于”泛型数组“+”数组的协变性“会引起ClassCastException。
    3. Java引入数组的协变性是SE5没有泛型的妥协。
    4. Java数组的协变性也会带来一些潜在的问题,但是较之泛型数组而言还是更加安全一些。所以允许数组协变而不允许泛型数组应该是语言设计上面的哲学考虑。

    相关文章

      网友评论

          本文标题:RxJava2中的Observable.just函数的调查

          本文链接:https://www.haomeiwen.com/subject/pnxeqftx.html