一、泛型 Scala支持类型参数化,使得我们能够编写泛型程序。
1.1 泛型类 Java中使用<>
符号来包含定义的类型参数,Scala则使用[]
。
1 2 3 class Pair [T , S ](val first: T , val second: S ) { override def toString : String = first + ":" + second }
1 2 3 4 5 6 7 8 9 object ScalaApp extends App { val pair01 = new Pair ("heibai01" , 22 ) val pair02 = new Pair [String ,Int ]("heibai02" , 33 ) println(pair01) println(pair02) }
1.2 泛型方法 函数和方法也支持类型参数。
1 2 3 object Utils { def getHalf [T ](a: Array [T ]): Int = a.length / 2 }
二、类型限定 2.1 类型上界限定 Scala和Java一样,对于对象之间进行大小比较,要求被比较的对象实现java.lang.Comparable
接口。所以如果想对泛型进行比较,需要限定类型上界为java.lang.Comparable
,语法为S <: T
,代表类型S是类型T的子类或其本身。示例如下:
1 2 3 4 5 class Pair [T <: Comparable [T ]](val first: T , val second: T ) { def smaller : T = if (first.compareTo(second) < 0 ) first else second }
1 2 3 val pair = new Pair ("abc" , "abcd" )println(pair.smaller)
扩展:如果你想要在Java中实现类型变量限定,需要使用关键字extends来实现,等价的Java代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 >public class Pair <T extends Comparable <T >> { > private T first; > private T second; > Pair(T first, T second) { > this .first = first; > this .second = second; > } > public T smaller () { > return first.compareTo(second) < 0 ? first : second; > } >} >
2.2 视图界定 在上面的例子中,如果你使用Int类型或者Double等类型进行测试,点击运行后,你会发现程序根本无法通过编译:
1 2 val pair1 = new Pair (10 , 12 )val pair2 = new Pair (10.0 , 12.0 )
之所以出现这样的问题,是因为Scala中的Int类并没有实现Comparable接口。在Scala中直接继承Comparable接口的是特质Ordered,它在继承compareTo方法的基础上,额外定义了关系符方法,源码如下:
1 2 3 4 5 6 7 8 9 trait Ordered [A ] extends Any with java .lang .Comparable [A ] { def compare (that: A ): Int def < (that: A ): Boolean = (this compare that) < 0 def > (that: A ): Boolean = (this compare that) > 0 def <= (that: A ): Boolean = (this compare that) <= 0 def >= (that: A ): Boolean = (this compare that) >= 0 def compareTo (that: A ): Int = compare(that) }
之所以在日常的编程中之所以你能够执行3>2
这样的判断操作,是因为程序执行了定义在Predef
中的隐式转换方法intWrapper(x: Int)
,将Int类型转换为RichInt类型,而RichInt间接混入了Ordered特质,所以能够进行比较。
1 2 @inline implicit def intWrapper (x: Int ) = new runtime.RichInt (x)
要想解决传入数值无法进行比较的问题,可以使用视图界定。语法为T <% U
,代表T能够通过隐式转换转为U,即允许Int型参数在无法进行比较的时候转换为RichInt类型。示例如下:
1 2 3 4 5 class Pair [T <% Comparable [T ]](val first: T , val second: T ) { def smaller : T = if (first.compareTo(second) < 0 ) first else second }
注:由于直接继承Java中Comparable接口的是特质Ordered,所以如下的视图界定和上面是等效的:
1 2 3 4 5 > > class Pair [T <% Ordered [T ]](val first: T , val second: T ) { > def smaller : T = if (first.compareTo(second) < 0 ) first else second > } >
2.3 类型约束 如果你用的Scala是2.11+,会发现视图界定已被标识为废弃。官方推荐使用类型约束(type constraint)来实现同样的功能,其本质是使用隐式参数进行隐式转换,示例如下:
1 2 3 4 5 6 7 8 9 class Pair [T ](val first: T , val second: T )(implicit ev: T => Comparable [T ] ) def smaller : T = if (first.compareTo(second) < 0 ) first else second} class Pair [T ](val first: T , val second: T )(implicit ev: T => Ordered [T ] ) { def smaller : T = if (first.compareTo(second) < 0 ) first else second }
当然,隐式参数转换也可以运用在具体的方法上:
1 2 3 object PairUtils { def smaller [T ](a: T , b: T )(implicit order: T => Ordered [T ]) = if (a < b) a else b }
2.4 上下文界定 上下文界定的形式为T:M
,其中M是一个泛型,它要求必须存在一个类型为M[T]的隐式值,当你声明一个带隐式参数的方法时,需要定义一个隐式默认值。所以上面的程序也可以使用上下文界定进行改写:
1 2 3 4 5 6 7 8 class Pair [T ](val first: T , val second: T ) { def smaller (implicit ord: Ordering [T ]): T = if (ord.compare(first, second) < 0 ) first else second } val pair= new Pair (88 , 66 )println(pair.smaller)
在上面的示例中,我们无需手动添加隐式默认值就可以完成转换,这是因为Scala自动引入了Ordering[Int]这个隐式值。为了更好的说明上下文界定,下面给出一个自定义类型的比较示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Person (val name: String , val age: Int ) { override def toString : String = name + ":" + age } class PersonOrdering extends Ordering [Person ] { override def compare (x: Person , y: Person ): Int = if (x.age > y.age) 1 else -1 } class Pair [T ](val first: T , val second: T ) { def smaller (implicit ord: Ordering [T ]): T = if (ord.compare(first, second) < 0 ) first else second } object ScalaApp extends App { val pair = new Pair (new Person ("hei" , 88 ), new Person ("bai" , 66 )) implicit val ImpPersonOrdering = new PersonOrdering println(pair.smaller) }
2.5 ClassTag上下文界定 这里先看一个例子:下面这段代码,没有任何语法错误,但是在运行时会抛出异常:Error: cannot find class tag for element type T
, 这是由于Scala和Java一样,都存在类型擦除,即泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉 。对于下面的代码,在运行阶段创建Array时,你必须明确指明其类型,但是此时泛型信息已经被擦除,导致出现找不到类型的异常。
1 2 3 4 5 6 object ScalaApp extends App { def makePair [T ](first: T , second: T ) = { val r = new Array [T ](2 ); r(0 ) = first; r(1 ) = second; r } }
Scala针对这个问题,提供了ClassTag上下文界定,即把泛型的信息存储在ClassTag中,这样在运行阶段需要时,只需要从ClassTag中进行获取即可。其语法为T : ClassTag
,示例如下:
1 2 3 4 5 6 import scala.reflect._object ScalaApp extends App { def makePair [T : ClassTag ](first: T , second: T ) = { val r = new Array [T ](2 ); r(0 ) = first; r(1 ) = second; r } }
2.6 类型下界限定 2.1小节介绍了类型上界的限定,Scala同时也支持下界的限定,语法为:U >: T
,即U必须是类型T的超类或本身。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class CEO // 部门经理 class Manager extends CEO // 本公司普通员工 class Employee extends Manager // 其他公司人员 class OtherCompany object ScalaApp extends App { def Check [T >: Manager ](t: T ): T = { println("获得审核权限" ) t } Check (new CEO ) Check (new Manager ) Check (new Employee ) Check (new OtherCompany ) Check [CEO ](new CEO ) Check [Manager ](new Manager ) Check [Employee ](new Employee ) Check [OtherCompany ](new OtherCompany ) }
2.7 多重界定
类型变量可以同时有上界和下界。 写法为 :T > : Lower <: Upper
;
不能同时有多个上界或多个下界 。但可以要求一个类型实现多个特质,写法为 :
T < : Comparable[T] with Serializable with Cloneable
;
你可以有多个上下文界定,写法为T : Ordering : ClassTag
。
三、Ordering & Ordered 上文中使用到Ordering和Ordered特质,它们最主要的区别在于分别继承自不同的Java接口:Comparable和Comparator:
Comparable :可以理解为内置的比较器,实现此接口的对象可以与自身进行比较;
Comparator :可以理解为外置的比较器;当对象自身并没有定义比较规则的时候,可以传入外部比较器进行比较。
为什么Java中要同时给出这两个比较接口,这是因为你要比较的对象不一定实现了Comparable接口,而你又想对其进行比较,这时候当然你可以修改代码实现Comparable,但是如果这个类你无法修改(如源码中的类),这时候就可以使用外置的比较器。同样的问题在Scala中当然也会出现,所以Scala分别使用了Ordering和Ordered来继承它们。
下面分别给出Java中Comparable和Comparator接口的使用示例:
3.1 Comparable 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import java.util.Arrays;public class Person implements Comparable <Person > { private String name; private int age; Person(String name,int age) {this .name=name;this .age=age;} @Override public String toString () { return name+":" +age; } @Override public int compareTo (Person person) { return this .age - person.age; } public static void main (String[] args) { Person[] peoples= {new Person("hei" , 66 ), new Person("bai" , 55 ), new Person("ying" , 77 )}; Arrays.sort(peoples); Arrays.stream(peoples).forEach(System.out::println); } } 输出: bai:55 hei:66 ying:77
3.2 Comparator 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import java.util.Arrays;import java.util.Comparator;public class Person { private String name; private int age; Person(String name,int age) {this .name=name;this .age=age;} @Override public String toString () { return name+":" +age; } public static void main (String[] args) { Person[] peoples= {new Person("hei" , 66 ), new Person("bai" , 55 ), new Person("ying" , 77 )}; Arrays.sort(peoples, new Comparator<Person>() { @Override public int compare (Person o1, Person o2) { return o1.age-o2.age; } }); Arrays.stream(peoples).forEach(System.out::println); } }
使用外置比较器还有一个好处,就是你可以随时定义其排序规则:
1 2 3 4 5 6 Arrays .sort(peoples, Comparator .comparingInt(o -> o.age));Arrays .stream(peoples).forEach(System .out::println);Arrays .sort(peoples, Comparator .comparingInt(o -> -o.name.length()));Arrays .stream(peoples).forEach(System .out::println);
3.3 上下文界定的优点 这里再次给出上下文界定中的示例代码作为回顾:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Person (val name: String , val age: Int ) { override def toString : String = name + ":" + age } class PersonOrdering extends Ordering [Person ] { override def compare (x: Person , y: Person ): Int = if (x.age > y.age) 1 else -1 } class Pair [T ](val first: T , val second: T ) { def smaller (implicit ord: Ordering [T ]): T = if (ord.compare(first, second) < 0 ) first else second } object ScalaApp extends App { val pair = new Pair (new Person ("hei" , 88 ), new Person ("bai" , 66 )) implicit val ImpPersonOrdering = new PersonOrdering println(pair.smaller) }
使用上下文界定和Ordering带来的好处是:传入Pair
中的参数不一定需要可比较,只要在比较时传入外置比较器即可。
需要注意的是由于隐式默认值二义性的限制,你不能像上面Java代码一样,在同一个上下文作用域中传入两个外置比较器,即下面的代码是无法通过编译的。但是你可以在不同的上下文作用域中引入不同的隐式默认值,即使用不同的外置比较器。
1 2 3 4 implicit val ImpPersonOrdering = new PersonOrdering println(pair.smaller) implicit val ImpPersonOrdering2 = new PersonOrdering println(pair.smaller)
四、通配符 在实际编码中,通常需要把泛型限定在某个范围内,比如限定为某个类及其子类。因此Scala和Java一样引入了通配符这个概念,用于限定泛型的范围。不同的是Java使用?
表示通配符,Scala使用_
表示通配符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Ceo (val name: String ) { override def toString : String = name } class Manager (name: String ) extends Ceo (name )class Employee (name: String ) extends Manager (name )class Pair [T ](val first: T , val second: T ) { override def toString : String = "first:" + first + ", second: " + second } object ScalaApp extends App { def makePair (p: Pair [_ <: Manager ]): Unit = {println(p)} makePair(new Pair (new Employee ("heibai" ), new Manager ("ying" ))) }
目前Scala中的通配符在某些复杂情况下还不完善,如下面的语句在Scala 2.12 中并不能通过编译:
1 def min [T <: Comparable [_ >: T ]](p: Pair [T ]) ={}
可以使用以下语法代替:
1 2 type SuperComparable [T ] = Comparable [_ >: T ]def min [T <: SuperComparable [T ]](p: Pair [T ]) = {}
参考资料
Martin Odersky . Scala编程(第3版)[M] . 电子工业出版社 . 2018-1-1
凯.S.霍斯特曼 . 快学Scala(第2版)[M] . 电子工业出版社 . 2017-7