[关闭]
@xtccc 2016-08-11T16:45:08.000000Z 字数 8856 阅读 3107

Generic (范型)

给我写信
GitHub

此处输入图片的描述

Scala



参考链接:
ClassTag, Class and war stories…



范型方法


需求1:希望写一个方法CloneInstance,能够复制输入的任何类型的对象,并构造一个新的实例输出。
假设,现在有Student和Teacher两个Java class,定义如下:

  1. // Student.java文件
  2. package cn.gridx.scala.example.generics;
  3. /**
  4. * Created by tao on 11/18/15.
  5. */
  6. public class Student {
  7. public String name;
  8. public String city;
  9. private String age;
  10. public Student() {
  11. name = null;
  12. city = null;
  13. age = null;
  14. }
  15. public Student(String name, String city, String age) {
  16. this.name = name;
  17. this.city = city;
  18. this.age = age;
  19. }
  20. @Override
  21. public String toString() {
  22. return name + ", " + city + ", " + age;
  23. }
  24. }


  1. // Teacher.java文件
  2. package cn.gridx.scala.example.generics;
  3. /**
  4. * Created by tao on 11/18/15.
  5. */
  6. public class Teacher {
  7. public String name;
  8. public String home;
  9. public String gender;
  10. public Teacher() {
  11. name = null;
  12. home = null;
  13. gender = null;
  14. }
  15. public Teacher(String name, String home, String gender) {
  16. this.name = name;
  17. this.home = home;
  18. this.gender = gender;
  19. }
  20. @Override
  21. public String toString() {
  22. return name + ", " + home + ", " + gender;
  23. }
  24. }



我们首先从直觉出发,编写一个如下的函数:

  1. package cn.gridx.scala.example.generics
  2. /**
  3. * Created by tao on 11/18/15.
  4. *
  5. * 范型方法
  6. */
  7. object GenericMethods {
  8. def CloneInstance[T](obj:T):T = {
  9. val instance = new T()
  10. val fields = classOf[T].getDeclaredFields
  11. for (f <- fields) {
  12. f.setAccessible(true) // 防止不能访问private field
  13. f.set(instance, f.get(obj))
  14. }
  15. instance
  16. }
  17. }

编译时会报错:

Error:(32, 28) class type required but T found
val instance = new T()

这说明编译器并不认为T是一个class type,既然用到了new T(),那么T就必须是一个class type。

所以,我们需要借助于 ClassTag 这个关键字,如下实现:

  1. package cn.gridx.scala.example.generics
  2. import scala.reflect.ClassTag
  3. /**
  4. * Created by tao on 11/18/15.
  5. *
  6. * 范型方法
  7. */
  8. object GenericMethods {
  9. def main(args: Array[String]): Unit = {
  10. val s = new Student("Jack", "Nanjing", "22")
  11. val t = new Teacher("Tom", "Shanghai", "30")
  12. // 必须用classOf[Student], 不能用s.getClass
  13. println(CloneInstance(classOf[Student], s))
  14. println(CloneInstance(classOf[Teacher], t))
  15. }
  16. def CloneInstance[T:ClassTag](clz:Class[T], obj:T): T = {
  17. val instance = clz.newInstance()
  18. val fields = clz.getDeclaredFields
  19. for (f <- fields) {
  20. f.setAccessible(true) // 防止不能访问private field
  21. f.set(instance, f.get(obj))
  22. }
  23. instance
  24. }
  25. }

输出如下:

Jack, Nanjing, 22
Tom, Shanghai, 30



需求2:根据输入的class,创建相应的类的实例

例如:输入 classOf[Student] ,则输出一个 Student 实例;输入 classOf[Teacher] ,则输出一个 Teacher 实例。

从直觉上,我们可以借助于 match ... case 来实现,并写出如下代码:

  1. def CreateInstanceByClass[T: ClassTag](clz:Class[T]): Any = {
  2. val obj = clz match {
  3. case classOf[Student] => new Student("n", "c", "a")
  4. case classOf[Teacher] => new Teacher("N", "H", "G")
  5. case _ =>
  6. }
  7. obj
  8. }

但是编译器会报错

Error:(53, 18)   not found:   type classOf
                                  case classOf[Student] => new Student("n", "c", "a")
                                        ^

分析:



换一种写法:

  1. def CreateInstanceByClass[T: ClassTag](clz:Class[T]): Any = {
  2. val studentClzName = classOf[Student].getCanonicalName
  3. val teacherClzName = classOf[Teacher].getCanonicalName
  4. val obj = clz.getCanonicalName match {
  5. case studentClzName => new Student("n", "c", "a")
  6. case teacherClzName => new Teacher("N", "H", "G")
  7. case _ =>
  8. }
  9. obj
  10. }

编译器会给出警告

Warning:(47, 18) patterns after a variable pattern cannot match (SLS 8.1.1)
                                    case studentClzName => new Student("n", "c", "a")
                                        ^

Warning:(48, 36) unreachable code due to variable pattern 'studentClzName' on line 47
                                    case teacherClzName => new Teacher("N", "H", "G")
                                        ^

Warning:(49, 26) unreachable code due to variable pattern 'studentClzName' on line 47
                                    case _ =>
                                        ^

Warning:(48, 36) unreachable code
                                   case teacherClzName => new Teacher("N", "H", "G")
                                       ^

这意味着,下面两行代码是unreachable code:

  1. case teacherClzName => new Teacher("N", "H", "G")
  2. case _ =>

并且,不论CreateInstanceByClass的输入类型是什么,这个match ... case总是会匹配到第一个case。我们可以验证这一点。

  1. package cn.gridx.scala.example.generics
  2. import scala.reflect.ClassTag
  3. /**
  4. * Created by tao on 11/18/15.
  5. *
  6. * 范型方法
  7. */
  8. object GenericMethods {
  9. def main(args: Array[String]): Unit = {
  10. println(CreateInstanceByClass(classOf[Student]))
  11. println("+"*10)
  12. println(CreateInstanceByClass(classOf[Teacher]))
  13. println("+"*10)
  14. println(CreateInstanceByClass(classOf[String]))
  15. }
  16. def CreateInstanceByClass[T: ClassTag](clz:Class[T]): Any = {
  17. val studentClzName = classOf[Student].getCanonicalName
  18. val teacherClzName = classOf[Teacher].getCanonicalName
  19. val obj = clz.getCanonicalName match {
  20. case studentClzName => new Student("n", "c", "a")
  21. case teacherClzName => new Teacher("N", "H", "G")
  22. case _ =>
  23. }
  24. obj
  25. }
  26. }

输出为

n, c, a
++++++++++
n, c, a
++++++++++
n, c, a

分析:

这是由 match ... case variable pattern 引起的问题,参考这里

解决方法:

  1. // 下面有2种解决方法
  2. def CreateInstanceByClass_1[T: ClassTag](clz:Class[T]): Any = {
  3. val studentClzName = classOf[Student].getCanonicalName
  4. val teacherClzName = classOf[Teacher].getCanonicalName
  5. val obj = clz.getCanonicalName match {
  6. case `studentClzName` => new Student("n", "c", "a")
  7. case `teacherClzName` => new Teacher("N", "H", "G")
  8. case _ =>
  9. }
  10. obj
  11. }
  12. def CreateInstanceByClass_2[T: ClassTag](clz:Class[T]): Any = {
  13. val studentClz = classOf[Student]
  14. val teacherClz = classOf[Teacher]
  15. val obj = clz match {
  16. case `studentClz` => new Student("n", "c", "a")
  17. case `teacherClz` => new Teacher("N", "H", "G")
  18. case _ =>
  19. }
  20. obj
  21. }




Reflection


Java

参考

Generics允许参数化类型(parameterised types),例如,Vector的一个实例可以被声明为Vector<String>。为了避免对运行时环境带来大的改动,范型是通过一种被称为type erasure的技术来实现的,即:擦除关于类型的信息,在必要的时候进行类型转换

但是,数组是一个例外:可以对数组应用paramaterized types,但是数组不会进行类型擦除

Casting with Arrays

  1. String[] vector = new String[3];
  2. vector[0] = "0"; vector[1] = "1"; vector[2] = "2";
  3. Object[] objects = vector;
  4. /**
  5. * objects[0] = new Object(); 编译正常,但是运行异常
  6. * Exception in thread "main" java.lang.ArrayStoreException: java.lang.Object
  7. */
  8. objects[0] = new Object();


Casting with Generic Types

  1. Vector<String> vector = new Vector<String>();
  2. Vector<Object> objects = (Vector<Object>)vector; // 无法转换: inconvertible types


Generic Arrays

  1. public <T> T createGenericArray() {
  2. return new T[10]; // Error: generic array creation
  3. }

上面的方法无法通过编译,会报错:generic array creation。因为,数组不支持type erasure,但是参数化的类型T在运行时是不存在的,编译器无法把一个运行时才有的类型赋给数组。

解决方法

通过java.lang.reflect.Array.newInstance来创建数组。

  1. public class JavaExamples {
  2. public static void main(String[] args) {
  3. GenericArray a = new GenericArray("Hello");
  4. String[] A = (String[])a.createGenericArray();
  5. for (int i=0; i<A.length; i++)
  6. A[i] = String.valueOf(i);
  7. for (int i=0; i<A.length; i++)
  8. System.out.println(A[i]);
  9. }
  10. public static class GenericArray<T> {
  11. private Class<T> tClz;
  12. public GenericArray(T t) {
  13. tClz = (Class<T>)t.getClass();
  14. }
  15. public T[] createGenericArray() {
  16. // `java.lang.reflect.Array.newInstance`返回的类型是`Object[]`
  17. return (T[])java.lang.reflect.Array.newInstance(tClz, 10);
  18. }
  19. }
  20. }


Scala

参考:

作为JVM上的语言,Scala也是具有 type erasure at run-time 这个特性的。例如,如果我们希望在运行时对参数的类型进行比较,如下:

  1. def meth1[T](xs: List[T]): Unit = {
  2. xs match {
  3. case _: List[String] => println("It is List[String]")
  4. case _: List[Int] => println("It is List[Int]")
  5. }
  6. }

那么,meth1(List(1,2,3))meth1("1","2","3") 都是会打印出 "It is List[String]"这句话的,因为在运行时,List[String]中的String 和 List[Int]中的Int都会被擦除,因为无法区分。

在2.10版本之后,Scala引入了一个新的reflection library,使得Scala同时具备了compile-time reflection和run-time reflection的能力。

Runtime Reflection

什么是runtime reflection?在运行时,对于给定的一个type或者instance,通过reflection可以:
1. 获取该instance/type的类型(包括generic type)
2. 实例化一个新的object
3. 访问或者调用该object的成员


TypeTag and typeOf

对于meth1遇到的类型擦除问题,可以通过scala.reflect.runtime.universe.TypeTagscala.reflect.runtime.universe.typeOf来解决:

  1. def meth2[T: TypeTag](xs: List[T]) = {
  2. typeOf[T] match {
  3. case t if t =:= typeOf[String] => println("It is List[String]")
  4. case t if t =:= typeOf[Int] => println("It is List[Int]")
  5. }
  6. }

这里,操作符=:=用于比较两个type是否相等。

可以把TypeTags 看操作是这样的对象:它包含了关于type的所有信息(包括compile-time和run-time)。TypeTag总是是由编译器生成的(只要用到了要求TypeTage的implicit parameter或者context bound,就会触发对TypeTage的生成)。这意味着:要想获得一个TypeTag,必须使用implicit parameter或者context bound。

下面是一个使用context bound的例子。

  1. scala> import scala.reflect.runtime.universe
  2. import scala.reflect.runtime.universe
  3. scala> val L = List(1,2,3)
  4. L: List[Int] = List(1, 2, 3)
  5. scala> def getTypeTag[T: universe.TypeTag](obj: T) = universe.typeTag[T]
  6. getTypeTag: [T](obj: T)(implicit evidence$1: reflect.runtime.universe.TypeTag[T])reflect.runtime.universe.TypeTag[T]
  7. scala> val theType = getTypeTag(L).tpe
  8. theType: reflect.runtime.universe.Type = List[Int]

这里,在定义getTypeTag方法时,类型参数T是有context bound的(从REPL可以看出,这相当于定义了一个隐式参数evidence,因而导致编译器为类型T生成了一个TypeTag)。而TypeTag的方法tpe返回的是TypeTag包含的类型。可以看到,对于generic type,通过TypeTag也能获取到它的真实类型。


ClassTag

通过ClassTag,我们可以创建范型数组(generic array),如下:

  1. def createGenericArray[T: scala.reflect.ClassTag](seq: T*): Array[T] = Array[T](seq: _*)
  2. val A = createGenericArray(1,2,3,4)
  3. val B = createGenericArray("X", "Y", "Z")
  4. println(A.getClass + " -> " + A.mkString("[", ",", "]"))
  5. println(B.getClass + " -> " + B.mkString("[", ",", "]"))

运行结果为

class [I -> [1,2,3,4]
class [Ljava.lang.String; -> [X,Y,Z]

如果对上述代码中的类型T不用scala.reflect.ClassTag来声明的话,是不能创建范型数组的。因为parameterized type在运行时遭遇到了type erasure,而创建数组时要求明确提供数组元素的类型。ClassTag的作用正是运行时提供创建类型所需的有关信息。




Parameterized Type and Variances


参考:

Parameterized Types

  1. abstract class Fruit {
  2. def name: String
  3. }
  4. class Orange extends Fruit {
  5. def name: String = "orange"
  6. }
  7. class Apple extends Fruit {
  8. def name: String = "apple"
  9. }
  10. class Box[F <: Fruit](f: Fruit) {
  11. def getFruit = f
  12. def contains(aFruit: Fruit): Boolean = f.name == aFruit.name
  13. }

Box[Fruit], Box[Apple], Box[Orange]是三个class,这三个class之间实际上是没有任何关系的。虽然Apple是Fruit的子类,但是Box[Apple]并不是Box[Fruit]的子类,因此下面的语句是无法通过编译的:

  1. val box: Box[Fruit] = new Box[Apple](new Apple)

Covariant and Contravariant

如果没有声明variance annotation,那么parameterized type就是invariant,例如Box[Friut]与Box[Apple]之间并不存在任何继承关系。

通过variance annotation可以将parameterized type声明为covariant或者contravariant


covariant
通过符号 +,可以让Box[Apple]成为Box[Fruit]的子类,如下声明Box即可:

  1. class Box[+F <: Fruit](f: Fruit) {
  2. ... ...
  3. }



contravariant
通过符号 -, 可以让Box[Apple]成为Box[Fruit]的父类,如下声明Box即可:

  1. class Box[-F <: Fruit](f: Fruit) {
  2. ... ...
  3. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注