【从零单排】关于泛型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;其次是往往需要在取出值的时候特别注意,正确转换数据类型,否则容易报错。所以并不是一个推荐的常规操作。

链接

posted @ 2020-05-27 16:12  MaxStack  阅读(185)  评论(0编辑  收藏  举报