zoukankan      html  css  js  c++  java
  • 【从零单排】关于泛型Generic的一些思考

    相信很多同学都多多少少听过Generic-泛型这个概念。下面就结合实际的例子对泛型进行讲解和剖析。

    为什么要用泛型

    泛,有多的意思。型,即为类型。结合起来,就是可以用泛型来指代“多种类型”,从而可以复用函数或类。

    举例:写一个echo函数用来输出List种某两个位置的值。

    当List里面装的是String,就是这样:

    public static void echo_string(List<String> a, int i, int j) {
    	String tmp_i = a.get(i);
    	String tmp_j = a.get(j);
    	System.out.println(tmp_i);
    	System.out.println(tmp_j);
    }
    

    当List里面装的是Integer,就是这样:

    public static void echo_integer(List<Integer> a, int i, int j) {
    	int tmp_i = a.get(i);
    	int tmp_j = a.get(j);
    	System.out.println(tmp_i);
    	System.out.println(tmp_j);
    }
    

    每次根据List里面值的不同类型,写一个对应的函数,不免有点重复劳动。能不能用一个函数实现呢?这就需要用到泛型。如下:

    public static <T> void echo_generic(List<T> a, int i, int j) {
    	T tmp_i = a.get(i);
    	T tmp_j = a.get(j);
    	System.out.println(tmp_i);
    	System.out.println(tmp_j);
    }
    

    什么地方用泛型

    主要有两个地方:

    • 创建 - 函数
    • 创建 - 类(Class)

    函数的例子上面已经举过了,不再赘述。下面就讲讲类。

    class SomeList<T>{
        private T param;
    
        SomeList(T param) {
            this.param = param;
            System.out.println("constructor for SomeList");
        }
    
        public T getParam() {
            return param;
        }
        public void setParam(T param) {
            this.param = param;
        }
    }
    
    SomeList<String> sl1 = new SomeList<>("123");
    System.out.println(sl1.getParam());
    
    SomeList<Integer> sl2 = new SomeList<>(123);
    System.out.println(sl2.getParam());
    

    在这个例子中,我们定义了一个SomeList类,它可以是String类型的,也可以是Integer类型的,取决于初始化时的传参类型。

    泛型的符号

    在实际使用过程中,我们常常看到<T>,或者是<K,V>这样的符号,被IDE高亮显示为不同的颜色。如下:

    public static <T> List<T> swap(List<T> a, int i, int j) {
    	T tmp_i = a.get(i);
    	T tmp_j = a.get(j);
    	a.set(i, tmp_j);
    	a.set(j, tmp_i);
    	return a;
    }
    
    public static <K,V> V getValue(K key, Map map) {
    	return (V) map.get(key);
    }
    

    我不禁要问:<T>,<K,V>是Java的保留字符吗?经过一些尝试之后,得出结论:不是的。这里用(几乎)任何的字母都可以。方括号之内可以是一个或多个字符,用来告诉编译器这个是我自定义的type。

    比如,上述的代码,可以把T,K,V替换成其它的字母,改写如下:

    public static <M> List<M> swap2(List<M> a, int i, int j) {
    	M tmp_i = a.get(i);
    	M tmp_j = a.get(j);
    	a.set(i, tmp_j);
    	a.set(j, tmp_i);
    	return a;
    }
    
    public static <X,Y> Y getValue2(X key, Map map) {
    	return (Y) map.get(key);
    }
    

    甚至,我们可以定义大于2个的type,比如<X,Y,Z>

    public static <X,Y,Z> Z getValue3(X key, Y key2, Map map1, Map map2) {
    	return (Z) (map1.get(key).toString() + map2.get(key2).toString());
    }
    

    通配符 Wildcards

    关于Wildcards怎么用我思考了很久,结合实际经验,我觉得单独使用Wildcards的情景很少,一般都是使用Bounded Wildcards。

    Wildcards

    单独使用Wildcards的情景,用Generic也可以实现。如下:

    void printCollection1(Collection<?> c) {
          for (Object e : c) {
                System.out.println(e);
          }
    }
    
    <T> void printCollection2(Collection<T> c) {
          for (Object e : c) {
                System.out.println(e);
          }
    }
    

    唯一的区别就是,用Wildcards的话,理论上传入参数Collection可以是多种不同的类型。但是其实Collection在赋值的时候,是没办法装不同类型的值的。

    Collection<?> c = new ArrayList<>();
    c.add(1);
    // 编译时会报错 add(capture<?>) cannot be applied to (int)
    

    所以个人感觉一般编程不太需要单独用到Wildcards,大多数情况可以用Generic覆盖。

    Bounded Wildcards

    而Bounded Wildcards,是有确实的用途的,可以给传入参数限制类型。

    这里分了两种extendssuper

    • (上限) The extends Wildcard Boundary: List<? extends A> means a List of objects that are instances of the class A, or subclasses of A
    • (下限) The super Wildcard Boundary: List<? super A> means that the list is typed to either the A class, or a superclass of A.

    分析extends,举例来说,之前我们定义了一个echo_generic方法,这个方法里面的传入参数可以是任意类型的List。现在,我们想要写一个类似的函数,但是List的类型只能是Number(Integer,Double等等都算)。如下:

    public static void echo_number(List<? extends Number> a, int i, int j) {
    	Object tmp_i = a.get(i);
    	Object tmp_j = a.get(j);
    	System.out.println(tmp_i);
    	System.out.println(tmp_j);
    }
    

    具体使用的时候,我们可以发现,确实只能传入Number或其subclass类型的List,否则编译时就会报错。

    // integer
    List<Integer> list_integer = new ArrayList<>();
    list_integer.add(1);
    list_integer.add(2);
    echo_number(list_integer, 0, 1);
    
    // double
    List<Double> list_double = new ArrayList<>();
    list_double.add(1.1);
    list_double.add(2.2);
    echo_number(list_integer, 0, 1);
    
    // string - compile error
    List<String> list_string = new ArrayList<>();
    list_string.add("1");
    list_string.add("2");
    echo_number(list_string, 0, 1);
    

    编译时的泛型

    问:一个ArrayList<String> collection1和一个ArrayList<Integer> collection2,它们的class是一样的吗?如下:

    ArrayList<String> collection1 = new ArrayList<String>();
    collection1.add("123");
    
    ArrayList<Integer> collection2 = new ArrayList<Integer>();
    collection2.add(456);
    
    System.out.println(collection1.getClass() == collection2.getClass());
    

    答案是:是一样的。原因很简单,我们只创建了一个类class ArrayList<T>collection1collection2的class都是这个。泛型的作用在于,在编译的时候,会限定集合中的值的类型。collection1中的值只能是String类型的,collection2中的值只能是Integer类型的。

    由此,想到一个黑科技,在runtime时,可以往一个ArrayList<String>里面塞Integer吗(即跳过编译过程时的泛型检查)?

    答案是:可以的。

    try {
    	collection1.getClass().getMethod("add", Object.class).invoke(collection1, 789);
    } catch (Exception e) {
    	e.printStackTrace();
    }
    System.out.println(String.valueOf(collection1.get(1)));
    // 789
    

    但是,这样做,首先是污染了collection1这个object;其次是往往需要在取出值的时候特别注意,正确转换数据类型,否则容易报错。所以并不是一个推荐的常规操作。

    链接

  • 相关阅读:
    HTML页面之间跳转传值
    Ajax之三种数据传输格式
    css选择器
    jQuery Validate
    正则表达式
    JSP的九大内置对象,七大动作指令,四个作用域,三个编译指令
    Zooeeper之paxos算法
    ZooKeeper之选举(fastleaderelection算法)
    ZooKeeper之ZAB协议
    ZooKeeper之三阶段提交(3PC)
  • 原文地址:https://www.cnblogs.com/maxstack/p/12973730.html
Copyright © 2011-2022 走看看