淘先锋技术网

首页 1 2 3 4 5 6 7

前言

在开发中,我们有时候会使用Object来表示接受的参数可以是任意的参数,如果我们能够保证绝对安全,我们可以去使用,但是客户端的调用通常是不可预测的,一个糟糕的程序员通常重心不会放在重点上。所以在代码中经常可以看到

public void xxx(Object e) {
 this.obj = e;
}

强制转换可能会在运行时失败。但是很可惜,没有人看到这些,我没有任何攻击任何人的意思。只是借助这个失败的案例,警告自己。

反省和自我批评

因为源代码过于拉跨和多余,所以后文的所有代码我都引用书上的,不然可能让他人觉得我的攻击性比较强,可是我本无此意(和如果我只是发了一段蔡徐坤跳舞的视频,他的粉丝就说我是小黑子同理),我只在乎我个人能力的成长,当然了也感谢这些代码,让我开始注意到一些细节,从而提升整体码能力,在过去的三年里所以我不断的执着于框架的源码和个人的实力的提升,却忽略了一些基本的代码规范和需要注意的点,这些点和规范的忽略让我相当长的一段时间之内都在漫无目的的寻找错误,发现别人的错误是容易的,我在开发中看别人写的代码的时候,我才恍然大悟,一时间修复了不少的问题。以前觉得莫名其妙的问题。对我自己展开了深刻的个人批评,希望能在注重主流框架和扩展实力的同时,能重视细节和规范以提升代码的效率,不要自己埋下暗坑还自鸣得意。这是弱者的表现。

使用泛型

不太正确的使用

public class Stack1 {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack1() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop(){
        if (size == 0){
            throw new EmptyStackException();
        }
        // pop it
        Object element = elements[--size];
        elements[size] = null;
        return element;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}

代码看起来没有什么问题。我们可以进行一个客户端调用尝试:

    public static void main(String[] args) {
        Stack1 stack1 = new Stack1();
        String integer = "10xxx";
        Double  d = 20D;
        stack1.push(integer);
        stack1.push(d);
        Double pop = (Double) stack1.pop();
        Double pop1 = (Double) stack1.pop();
        System.out.println(pop);
        System.out.println(pop1);
    }

报错了,所以这种写法并不推荐,你并不知道客户端的方式。

Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Double
	at com.effective.generic.Stack1Test.main(Stack1Test.java:11)

使用泛型

public class Stack<E> {
    private  E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    // The elements array will contain only E instances from push(E).
// This is sufficient to ensure type safety, but the runtime
// type of the array won't be E[]; it will always be Object[]!
    @SuppressWarnings("unchecked")
    public Stack() {
        elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(E e) {
        ensureCapacity();
        elements[size++] = e;
    }


    public E pop() {
        if (size == 0)
            throw new EmptyStackException();
        E result = elements[--size];
        elements[size] = null; // Eliminate obsolete reference
        return result;
    }

    public boolean isEmpty() {
        return size == 0;
    }


    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }

    public void pushAll(Iterable<E> src) {
        for (E e : src)
            push(e);
    }



    public static void main(String[] args) {
        Stack<String> stack = new Stack<>();
        String[] strings = new String[3];
        strings[0] = "1";
        strings[1] = "2";
        strings[2] = "3";
        for (String arg : strings)
            stack.push(arg);
        while (!stack.isEmpty())
            System.out.println(stack.pop().toUpperCase());
    }
}

修改代码之后,使用了泛型,如果使用了泛型,那么如果你添加double,编译直接通不过的,所以比起Object,泛型更加安全。

增加通配和可变性

String 是 Object的子类
但是List是List的子类吗,不是!这是违反直觉的,可以将任何对象放入 List中,但是只能将字符串放入 List 中。 由于 List 不能做 List 所能做的所有事情,所以它不是一个子类型。

幸运的是,有对应的解决方法。 该语言提供了一种特殊的参数化类型来调用一个限定通配符类型来处理这种情况。 pushAll 的输入参数的类型不应该是「E 的 Iterable 接口」,而应该是「E 的某个子类型的 Iterable 接口」,并且有一个通配符类型,这意味着: Iterable<? extends E> 。

  public void pushAll(Iterable<? extends E> src) {
        for (E e : src)
            push(e);
    }

我们将pushAll的方法改装,发现编译和运行没有问题。
我们实现了pushAll的方法,通常还有一个popAll,我们先这么写

    public void popAll(Collection<E> dest) {
         while (!isEmpty()){
             dest.add(pop());
         }
    }

这样在调用的过程中,IDEA直接报红,无法通过编译,我们稍作修改后:

    public void popAll(Collection<? super E> dest) {
         while (!isEmpty()){
             dest.add(pop());
         }
    }

这样就可以通过编译并且灵活性得到了提升,这个结论很清楚。 为了获得最大的灵活性,对代表生产者或消费者的输入参数使用通配符类型。如果一个输入参数既是一个生产者又是一个消费者,那么通配符类型对你没有好处:你需要一个精确的
类型匹配,这就是没有任何通配符的情况。PECS : producer-extends,
consumer-super。换句话说,如果一个参数化类型代表一个 T 生产者,使用 <? extends T> ;如果它代表 T 消费者,则使用 <? super T> 。 在我们的 Stack 示例中, pushAll 方法的 src 参数生成栈使用的E 实例,因此 src 的合适类型为Iterable<? extends E> ; popAll 方法的 dst 参数消费Stack 中的 E 实例,因此 dst 的合适类型是 Collection <? super E> 。 PECS 助记符抓住了使用通配符类型的基本原则。 Naftalin 和 Wadler 称之为获取和放置原则(Get and Put Principle)。

结束语

一个API,性能良好是基本标配,还应该适用于一切原本就应该可以的参数,如果报错,只能说明参数不对,而不应该去代码中找寻问题,这就是一个合格的API,我们往往为了获取更好的性能(关注点)而忘记了这些基本的东西。事实上,只要是合格的代码性能基本不会出现太大问题,但是一些细节的忽视可能让API调用者觉得无从下手。 总之,在你的 API 中使用通配符类型,虽然棘手,但使得 API 更加灵活。 如果编写一个将被广泛使用的类库,正确使用通配符类型应该被认为是强制性的。 记住基本规则: producer-extends,consumer-super(PECS)。 还要记住,所有 Comparable 和 Comparator 都是消费者。