Scala 泛型编程

news2024/11/24 20:27:25

1. 泛型

Scala 支持类型参数化,使得我们能够编写泛型程序。

1.1 泛型类

Java 中使用 `<>` 符号来包含定义的类型参数,Scala 则使用 `[]`。

class Pair[T, S](val first: T, val second: S) {
  override def toString: String = first + ":" + second
}
object ScalaApp extends App {

  // 使用时候你直接指定参数类型,也可以不指定,由程序自动推断
  val pair01 = new Pair("TomJack01", 22)
  val pair02 = new Pair[String,Int]("TomJack02", 33)

  println(pair01)
  println(pair02)
}

1.2 泛型方法

函数和方法也支持类型参数。

object Utils {
  def getHalf[T](a: Array[T]): Int = a.length / 2
}

2. 类型限定

2.1 类型上界限定

Scala 和 Java 一样,对于对象之间进行大小比较,要求被比较的对象实现 `java.lang.Comparable` 接口。所以如果想对泛型进行比较,需要限定类型上界为 `java.lang.Comparable`,语法为 ` S <: T`,代表类型 S 是类型 T 的子类或其本身。示例如下:

// 使用 <: 符号,限定 T 必须是 Comparable[T]的子类型
class Pair[<: Comparable[T]](val first: T, val second: T) {
  // 返回较小的值
  def smaller: T = if (first.compareTo(second) < 0) first else second
}

// 测试代码
val pair = new Pair("abc", "abcd")
println(pair.smaller) // 输出 abc

扩展:如果你想要在 Java 中实现类型变量限定,需要使用关键字 extends 来实现,等价的 Java 代码如下:

public class Pair<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 等类型进行测试,点击运行后,你会发现程序根本无法通过编译:

val pair1 = new Pair(10, 12)
val pair2 = new Pair(10.0, 12.0)

之所以出现这样的问题,是因为 Scala 中的 Int 类并没有实现 Comparable 接口。在 Scala 中直接继承 Comparable 接口的是特质 Ordered,它在继承 compareTo 方法的基础上,额外定义了关系符方法,源码如下:

// 除了 compareTo 方法外,还提供了额外的关系符方法
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 特质,所以能够进行比较。

// Predef.scala
@inline implicit def intWrapper(x: Int)   = new runtime.RichInt(x)

要想解决传入数值无法进行比较的问题,可以使用视图界定。语法为 `T <% U`,代表 T 能够通过隐式转换转为 U,即允许 Int 型参数在无法进行比较的时候转换为 RichInt 类型。示例如下:

// 视图界定符号 <%
class Pair[<% Comparable[T]](val first: T, val second: T) {
  // 返回较小的值
  def smaller: T = if (first.compareTo(second) < 0) first else second
}

 注:由于直接继承 Java 中 Comparable 接口的是特质 Ordered,所以如下的视图界定和上面是等效的:

 // 隐式转换为 Ordered[T]
    class Pair[<% 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.使用隐式参数隐式转换为 Comparable[T]
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
}

// 2.由于直接继承 Java 中 Comparable 接口的是特质 Ordered,所以也可以隐式转换为 Ordered[T]
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
}

当然,隐式参数转换也可以运用在具体的方法上:

object PairUtils{
  def smaller[T](a: T, b: T)(implicit order: T => Ordered[T]) = if (< b) a else b
}

2.4 上下文界定

上下文界定的形式为 `T:M`,其中 M 是一个泛型,它要求必须存在一个类型为 M[T]的隐式值,当你声明一个带隐式参数的方法时,需要定义一个隐式默认值。所以上面的程序也可以使用上下文界定进行改写:

class Pair[T](val first: T, val second: T) {
  // 请注意 这个地方用的是 Ordering[T],而上面视图界定和类型约束,用的是 Ordered[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)  //输出:66

在上面的示例中,我们无需手动添加隐式默认值就可以完成转换,这是因为 Scala 自动引入了 Ordering[Int]这个隐式值。为了更好的说明上下文界定,下面给出一个自定义类型的比较示例:

// 1.定义一个人员类
class Person(val name: String, val age: Int) {
  override def toString: String = name + ":" + age
}

// 2.继承 Ordering[T],实现自定义比较器,按照自己的规则重写比较方法
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("Tom", 88), new Person("Jack", 66))
  // 3.定义隐式默认值,如果不定义,则下一行代码无法通过编译
  implicit val ImpPersonOrdering = new PersonOrdering
  println(pair.smaller) //输出: Jack:66
}

2.5 ClassTag上下文界定

这里先看一个例子:下面这段代码,没有任何语法错误,但是在运行时会抛出异常:`Error: cannot find class tag for element type T`, 这是由于 Scala 和 Java 一样,都存在类型擦除,即泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉。对于下面的代码,在运行阶段创建 Array 时,你必须明确指明其类型,但是此时泛型信息已经被擦除,导致出现找不到类型的异常。

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`,示例如下:

import scala.reflect._
object ScalaApp extends App {
  def makePair[: 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 的超类或本身。

// 首席执行官
class CEO

// 部门经理
class Manager extends CEO

// 本公司普通员工
class Employee extends Manager

// 其他公司人员
class OtherCompany

object ScalaApp extends App {

  // 限定:只有本公司部门经理以上人员才能获取权限
  def Check[>: 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)
  /*
   * 以下两条语句无法通过编译,异常信息为: 
   * do not conform to method Check's type parameter bounds(不符合方法 Check 的类型参数边界)
   * 这种情况就完成了下界限制,即只有本公司经理及以上的人员才能获得审核权限
   */
  Check[Employee](new Employee)
  Check[OtherCompany](new OtherCompany)
}

2.7 多重界定

类型变量可以同时有上界和下界。 写法为 :`T > : Lower <: Upper`;

不能同时有多个上界或多个下界 。但可以要求一个类型实现多个特质,写法为 :

  `T < : Comparable[T] with Serializable with Cloneable`;

你可以有多个上下文界定,写法为 `T : Ordering : ClassTag` 。

3. Ordering & Ordered

上文中使用到 Ordering 和 Ordered 特质,它们最主要的区别在于分别继承自不同的 Java 接口:Comparable 和 Comparator:

Comparable:可以理解为内置的比较器,实现此接口的对象可以与自身进行比较;

Comparator:可以理解为外置的比较器;当对象自身并没有定义比较规则的时候,可以传入外部比较器进行比较。

为什么 Java 中要同时给出这两个比较接口,这是因为你要比较的对象不一定实现了 Comparable 接口,而你又想对其进行比较,这时候当然你可以修改代码实现 Comparable,但是如果这个类你无法修改 (如源码中的类),这时候就可以使用外置的比较器。同样的问题在 Scala 中当然也会出现,所以 Scala 分别使用了 Ordering 和 Ordered 来继承它们。

下面分别给出 Java 中 Comparable 和 Comparator 接口的使用示例:

3.1 Comparable

import java.util.Arrays;
// 实现 Comparable 接口
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("Tom", 66), new Person("Jack", 55), new Person("Lucy", 77)};
        Arrays.sort(peoples);
        Arrays.stream(peoples).forEach(System.out::println);
    }
}

输出:
Jack:55
Tom:66
Lucy:77

3.2 Comparator

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("Tom", 66), new Person("Jack", 55), new Person("Lucy", 77)};
        // 这里为了直观直接使用匿名内部类,实现 Comparator 接口
        //如果是 Java8 你也可以写成 Arrays.sort(peoples, Comparator.comparingInt(o -> o.age));
        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);
    }
}

使用外置比较器还有一个好处,就是你可以随时定义其排序规则:

// 按照年龄大小排序
Arrays.sort(peoples, Comparator.comparingInt(-> o.age));
Arrays.stream(peoples).forEach(System.out::println);
// 按照名字长度倒序排列
Arrays.sort(peoples, Comparator.comparingInt(-> -o.name.length()));
Arrays.stream(peoples).forEach(System.out::println);

3.3 上下文界定的优点

这里再次给出上下文界定中的示例代码作为回顾:

// 1.定义一个人员类
class Person(val name: String, val age: Int) {
  override def toString: String = name + ":" + age
}

// 2.继承 Ordering[T],实现自定义比较器,这个比较器就是一个外置比较器
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("Tom", 88), new Person("Jack", 66))
  // 3.在当前上下文定义隐式默认值,这就相当于传入了外置比较器
  implicit val ImpPersonOrdering = new PersonOrdering
  println(pair.smaller) //输出: Jack:66
}

使用上下文界定和 Ordering 带来的好处是:传入 `Pair` 中的参数不一定需要可比较,只要在比较时传入外置比较器即可。

需要注意的是由于隐式默认值二义性的限制,你不能像上面 Java 代码一样,在同一个上下文作用域中传入两个外置比较器,即下面的代码是无法通过编译的。但是你可以在不同的上下文作用域中引入不同的隐式默认值,即使用不同的外置比较器。

implicit val ImpPersonOrdering = new PersonOrdering
println(pair.smaller) 
implicit val ImpPersonOrdering2 = new PersonOrdering
println(pair.smaller)

4. 通配符

在实际编码中,通常需要把泛型限定在某个范围内,比如限定为某个类及其子类。因此 Scala 和 Java 一样引入了通配符这个概念,用于限定泛型的范围。不同的是 Java 使用 `?` 表示通配符,Scala 使用 `_` 表示通配符。

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("TomJack"), new Manager("Lucy")))
}

目前 Scala 中的通配符在某些复杂情况下还不完善,如下面的语句在 Scala 2.12 中并不能通过编译:

def min[<: Comparable[>: T]](p: Pair[T]) ={}

可以使用以下语法代替:

type SuperComparable[T] = Comparable[>: T]
def min[<: SuperComparable[T]](p: Pair[T]) = {}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1163011.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

高压放大器的作用和优势是什么

高压放大器是一种专门用于放大高电压信号的设备&#xff0c;它具有许多重要的作用和优势。在以下的文章中&#xff0c;我们将详细介绍高压放大器的作用和优势。 高压放大器的作用之一是在实验室和工程应用中提供对高压信号的放大和控制。许多应用领域&#xff0c;如实验物理、电…

C/C++数据结构-链表-链表合并排序输出

文章目录 前言例题题解带头结点不带头结点 前言 这个小例题涵盖的知识点还是非常多的。包括链表的定义&#xff0c;链表的尾插法&#xff0c;链表的遍历&#xff0c;冒泡排序用链表实现。链表可以使用带头结点和不带头结点的形式&#xff0c;各有千秋。 本文完整版使用带头结点…

美团圈圈私域加群app拉新 单价高一手渠道整理

美团圈圈为我们现在主推产品 可以通过”聚量推客“ 申请推广 一手数据&#xff0c;外面很多其它的数据都会阉割的很厉害&#xff0c;可以对比 下面介绍下圈圈的优势&#xff0c;我们目前圈圈对外结算时间为11天&#xff08;因为美团官方有个要求 10天内退群无效&#xff09;&…

技能证里的天花板—阿里云云计算架构师ACE认证!

在当今的社会中&#xff0c;想要获得一份好工作、得到丰厚的报酬&#xff0c;唯一的方法就是证明自己优秀&#xff0c;能给公司创造价值&#xff0c;是大多数公司想要看到的。 那么在面试过程中&#xff0c;怎么样才能让面试官一眼就记住呢&#xff1f;那一定是有一份足够优秀…

Java生成二维码并打印二维码和文字信息

目录 1、生成二维码&#xff0c;并调用画布进行二维码和文字的描绘。 2、主程序&#xff1a;获取打印机&#xff0c;并打印内容 3、打印效果 参考文献&#xff1a; 前期工作是安装好打印机驱动&#xff0c;可连接打印机。 添加三个依赖jar&#xff1a; 具体见文底的参考文献…

什么情况会造成电表不走数?

电表是衡量用电量的重要设备&#xff0c;一旦出现不走数的情况&#xff0c;不仅会影响用户对用电量的准确计算&#xff0c;还可能造成电费纠纷。那么&#xff0c;究竟什么情况下会造成电表不走数呢&#xff1f;接下来&#xff0c;小编来为大家介绍下&#xff0c;一起来看下吧&a…

学习使用php实现汉字验证码

学习使用php实现汉字验证码 <?php //开启session &#xff0c;方便验证 session_start(); //创建背景画布 $image imagecreatetruecolor(200, 60); $background imagecolorallocate($image, 255, 255, 255); imagefill($image, 0, 0, $background);//创建背景画布 for ($…

【腾讯云HAI域探秘】AI让我变成灵魂画手

【腾讯云HAI域探秘】AI让我变成灵魂画手 文章目录 【腾讯云HAI域探秘】AI让我变成灵魂画手前言环境搭建进入 StableDiffusionWebUI使用 高性能应用服务HAI 部署的 StableDiffusionWebUI 快速进行AI绘画总结 前言 最近有幸参与了腾讯云举办的 腾讯云 HAI 产品体验活动。在这个过…

解决VS中文编译时出现:常量中有换行符

错误如下 首先确定文件编码格式已经是utf-8了&#xff0c;然后在有中文的情况的下&#xff0c;编译时会报错&#xff0c;真的很恶心 tabWidget.addTab(widget, tr("材料库"));tabWidget.addTab(widget1, tr("B"));解决方案 通过在项目里设置编译选项: /utf…

【docker使用Jar自定义镜像:基于windows】

在一个空文件夹中创建Dockerfile 将jar包复制到该路径下 在Dockerfile中添加以下内容 # 指定基础镜像 FROM java:8-alpine# 和java项目的包 COPY ./study_dockerfile-1.0.0.jar /tmp/app.jar# 暴露端口 EXPOSE 8081# 入口&#xff0c;java项目的启动命令 ENTRYPOINT java -jar…

开发ros一定要买树莓派吗?

结论&#xff1a;开发ros不一定要买树莓派&#xff0c;但是买一个会更方便&#xff0c;市场价410元/块&#xff1b; 开发ros机器人的时候&#xff0c;分为上位机和下位机2个控制中心&#xff0c; 其中下位机基本就是用STM32控制一个驱动电机。负责这个运动方向和速度。&#…

算法通关村第五关-白银挑战实现队列

大纲 队列基础队列的基本概念和基本特征实现队列队列的基本操作Java中的队列 队列基础 队列的基本概念和基本特征 队列的特点是节点的排队次序和出队次序按入队时间先后确定&#xff0c;即先入队者先出队&#xff0c;后入队者后出队&#xff0c;即我们常说的FIFO(first in fi…

感受野(Receptive Field)理解和计算

[Toc](感受野(Receptive Filed)的理解和计算) 感受野定义 在卷积神经网络中&#xff0c;感受野(Receptive Filed)是经过神经网络后生成的指特征图上的某个像素点所对应输入图像的区域。 针对上面的图&#xff0c;经过conv2 卷积神经网络后&#xff0c;生成了22 的特征图&…

微软曝光!ChatGPT 真实参数只有 200 亿?大模型评测基准已经失去意义?丨 RTE 开发者日报 Vol.76

开发者朋友们大家好&#xff1a; 这里是 「RTE 开发者日报」 &#xff0c;每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE &#xff08;Real Time Engagement&#xff09; 领域内「有话题的 新闻 」、「有态度的 观点 」、「有意思的 数据 」、「有思考的 文…

【Cheat Engine7.5】基础教程第一关(STEP3)

文章目录 一、STEP3练习1、打开文件2、简介&#xff08;2种解决方法&#xff09;2.1、第一种解决方法(未知的初始值)2.1.1、未知的初始值2.1.2、点击打我&#xff0c;发现数值减少2&#xff0c;值在变小2.1.3、选择数值减少了多少&#xff0c;针对性搜索2.1.4、继续上一次反复操…

竞赛 深度学习火车票识别系统

文章目录 0 前言1 课题意义课题难点&#xff1a; 2 实现方法2.1 图像预处理2.2 字符分割2.3 字符识别部分实现代码 3 实现效果4 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 图像识别 火车票识别系统 该项目较为新颖&#xff0c;适…

springboot+vue跨域请求使用方法

这里写目录标题 一、所谓跨域:二、不做任何处理三、解决跨域请求案例 一案例 二 一、所谓跨域: 在前后端分离的项目中&#xff0c;前台一个服务&#xff0c;后台一个服务。 前台的一个Axios请求打进来&#xff0c;要访问后台Tomcat服务器Restful接口 浏览器出于安全的考虑&…

“泰山众筹:引爆全球的财富狂潮!“

想象一下&#xff0c;你手中的白酒不再只是简单的饮品&#xff0c;而是一份珍贵的投资&#xff0c;一份充满惊喜的冒险&#xff0c;一份财富的种子&#xff01;在这个神奇的时刻&#xff0c;让我们一起探索泰山众筹模式的魅力&#xff01; 在传统的投资领域&#xff0c;你是否…

跨国传输的常见问题与对应解决方案

在今天的全球化时代&#xff0c;跨国数据传输已经成为一个不可或缺的需求。不论是个人还是企业&#xff0c;都需要通过网络将文件或数据从一个国家传输到另一个国家&#xff0c;以实现信息共享、协作、备份等目的。然而&#xff0c;跨国数据传输并不是一项容易的任务&#xff0…