Scala的类层次(1) - Any、AnyVal、AnyRef
Scala的类型层次与Java的相应部分是非常相似的。首先,Scala中存在单一的根类Any,所有类型都直接或间接地由Any类继承而来。这与Java中的所有引用类型的根类是java.lang.Object是一样的。
另一方面也有与Java不同的部分。首先Scala不存在类似于Java中的基础(Primitive)类型。Scala中所有与基础类型相当的类型都成为了类,并且这些类都是继承了Any的AnyVal类的子类。
另外,所有的引用类型都成AnyRef类的间接或直接子类。前面说了Any类类似于Java中的java.lang.Object类,但是从实际意义上来看,应该说对应于java.lang.Object的应该是AnyRef。
不过离题一下,对于Scala中相当于基础类型的类,可以把整数的(Byte、Short、Int、Long)和浮点数的(Float、Double)看作是相邻的兄弟类关系,那Java中可以默认转换的比如byte->int,在Scala中是否需要显示的类型转换呢,如果需要的话那大家会觉得好麻烦呀。实际上Scala中具有使用户能够定义自己的默认类型转换功能的隐式转换(implicit
conversion)功能。对于类似于Java中的byte->int、float->double等默认转换功能,在Scala标准库中定义了与此相当的隐式转换功能,所以用户是不需要显示转换的。
Scala的类层次(2) - Nothing、Null
与Java不同的是,Scala中存在所谓的底(bottom)类型,那就是Nothing类。Nothing是所有类型的子类,也就是说可以将Nothing类型赋值给任意类型的变量,但是Nothing类型的值并不存在。
大家可能认为“没有值的类型有什么用呢?”。但是Nothing类型绝对在,表示没有返回值的函数的返回类型,或者在后述的范型中表示空的集等方面发挥着重要的作用。
另外还存在是所有的引用类型(AnyRef)的子类,可以赋值给所有引用类型变量的类型Null。Null类型的值只有null(实际上Java中也有Null类型,担负着与Scala中Null类型相似的任务。与Scala不同的是,Java中没有显示定义Null类型的机会,所以基本上没有人会意识得到的)。(図
3-1的b)表示了上述类型间的关系。
泛型基础
一句话来说,泛型就是定义以类型为参数的类或接口(Scala中为特征)的功能。Java里从JDK5开始就有了泛型,想必知道的人应该比较多了,下面就简单举例说明一下。
例如,假设有如下的代码片段。这里java.util.List是泛型接口,String就是赋给它的类型参数。
- java.util.List< String> strs = new java.util.ArrayList< String>();
这样,就可以用如下方法将String类型(或子类型)的对象加入List中了。
- strs.add("hoge");
如下所示,如将String以外的对象加入List则会发生编译错误。
- strs.add(new java.util.Date());
这样一来,就可以开发类型安全的通用集(collection)库了。在Java5之前的集合是用Object来实现的。但是向集合中加入元素时并没有进行正确的类型检查,而且从集中取出元素时还要做强制的类型转换,导致旧的集合在类型安全方面有一些问题。进一步来说,光从类型定义看不出该集合包含的是何种元素,所以在可读性方面也有不足。
Scala的泛型与Java是非常相似的,基本上可以同样地使用,只是在标记方法上有些区别。以下是同刚才Java代码基本相同的Scala代码。
- var strs: java.util.List[String] = new java.util.ArrayList
Scala中用[..]来代替了Java中的< ..>来表现类型参数表。附带提一下,与Java有一点小的不同,Scala在new
ArrayList时不需要指定String类型参数,这是编译器的类型推断起了效用(显示指定也是可以的)。
Scala中定义泛型类的方法也基本与Java相同。下面是通过范型用Java定义的不可变单方向列表类。这里在类名Link后声明了用<
>括着的类型参数T。这个类型参数T在Link类的定义中可以像一般类型那样使用。
- class Link< T> {
- final T head;
- final Link< T> tail;
- Link(T head, Link< T> tail) {
- this.head = head;
- this.tail = tail;
- }
- }
同样可以用Scala来定义与上述完全相同的泛型列表。
- class Link[T](val head: T, val tail: Link[T])
从此可知,除了一些细微的标识差别,Scala中也可以方便地使用泛型。
泛型的协变与逆变
光从到此为止的说明来看,可能有人会以为Scala是仅仅把Java中的泛型改变了一下标识符号。但是Scala中的泛型有几个与Java不同的明显差异,其中之一就是这里提到的协变与逆变。
协变
泛型中所谓的协变大致来说是这样的东西。首先假设有类G(或者接口和特征)和类型T1、T2。在T1是T2的子类的情况下如果G<
T1>也是G< T2>的子类,那么类G就是协变的。
仅如此说明的话比较难以理解,那就举例说明一下。如下所示,假设有类型为java.util.List<
Object>的变量s1和类型为java.util.List< String>的变量s2。
- java.util.List< Object> s1 = ...;
- java.util.List< String> s2 = ...;
String是Object的子类,Java中并不允许将s2赋值给s1,将会产生编译错误。因此,虽然String是Object的子类,但是java.util.List<
String>并不是java.util.List<
Object>的子类,所以用Java的泛型所定义的类或接口并不是协变的。这并不是由于Java泛型的灵活性不好,而是因为协变的泛型在保证类型的安全性上有一些问题。
假定允许s1=s2;。s1是容纳Object类型的元素的,所以如下所示可以加入java.util.Date类型的对象。
- s1.add(new java.util.Date());
但是由于语句s1=s2;,s1被指向了s2,这样容纳String元素的List变量s2就可以加入java.util.Date对象了。这样好不容易通过范型来保证的类型安全性(java.util.List<
String>里只有String)就被破坏了。正因为有如此问题所以Java的泛型不是协变的。
附带提一下,对于Java5之前就存在的数组来说,数组的元素类型A如果是数组元素类型B的子类,那么A的数组类型也是B的数组类型的子类,也就是说Java中的数组是协变的。这样一来,如下所示即使是违背了类型安全性的数组之间的赋值(没有强制类型转换)代码也能通过编译器检查。
- String[] s2 = new String[1];
- Object[] s1 = s2;
- s1[0] = new java.util.Date(); //执行时抛出ArrayStoreException异常
如上所述,Java中的泛型不是协变的是有理由的,但是有些情况下这种限制表现得过于强了。比如,以使用前述的不可变Link类为例。这种情况下,一旦创建不可变Link的实例之后,与Java的List不同,对于该实例是不能进行写操作(如add)的,这样的话将Link<
String>赋值给Link< Object>也就可以认为没有问题了,但是在Java中这是不允许的。
Scala的泛型,在没有特定指定的情况下也是和Java一样,是非协变的。例如使用前述的Link类编写如下代码后将会出现编译错误。
- val link: Link[Any] = new Link[String]("FOO", null)
- ...
错误提示如下。叙述的错误原因是: 在应该出现Ling[Any]的地方却出现了Link[String],而这正是Link不是协变的结果。
- fragment of Link.scala):2: error: type mismatch;
- found : this.Link[String]
- required: this.Link[Any]
- val link: Link[Any] = new Link[String]("FOO", null)
但是,Scala的类或特征的泛型定义中,如果在类型参数前面加入+符号,就可以使类或特征变为协变了。下面是在Scala中定义协变类的实验。题材是前述的Link类,在类型参数T前加了一个+符号。
- class Link[+T](val head: T, val tail: Link[T])
把Link类如此定义之后,前面出现编译错误的代码就可以顺利通过编译了。另外,如果试图定义不能保证类型安全的协变泛型将会出现编译错误。例如在定义非可变的数据结构时,这种限制就会带来一些问题。例如对于前面的Link类,追加一个将作为参数传入的元素放在列表头并返回新列表的方法prepend。
- class Link[+T](val head: T, val tail: Link[T]) {
- def prepend(newHead: T): Link[T] = new Link(newHead, this)
- }
prepend方法并没有改变原来Link类实例的状态,应该是没有问题的。但是,编译之后会产生如下编译错误。
- ink.scala:2: error: covariant type T occurs in contravariant position in type T
- of value newHead
- def prepend(newHead: T): Link[T] = new Link(newHead, this)
实际上,泛型变为协变之后就不能把类型参数不加修改的放在成员方法的参数上(这里是newHead)了。但是,通过将成员方法定义为泛型,并按照如下所示描述后就可以避免该问题了(具体原因这里略而不谈)。
- class Link[+T](val head: T, val tail: Link[T]) {
- def prepend[U >: T](newHead: U): Link[U] = new Link(newHead, this)
- }
在Java里也可以定义泛型方法,正如泛型类型定义,通过用类型参数来参数化方法,从而定义了类型安全的泛型方法。例如,List类的map方法就是泛型方法。
- verride final def map[B](f : (A) => B) : List[B]
map方法将以参数形式传入的函数f应用于List的所有元素,并将函数的应用结果组成列表后返回。但是参数函数f的返回结果是什么在定义map方法是不知道的,所以用类型参数B来使map成为泛型方法,从而使它可以通用于各种类型了。
泛型方法是通过在方法名后直接用[..]来括住类型参数方式来定义的。用[]括住的类型参数在方法中可以作为一般类型来使用。而且在类型参数之后加上>:或<
:符号后,可以将类型参数所表示的类型限制为某一类型子类或父类。例如,[U<
:T]的情况下,U必须是T的子类;[U>:T]的情况下,U必须是T的父类。
逆变
另一方面,泛型中的逆变是这样的东西。首先假设有类G(或者接口和特征)和类型T1、T2。在T1是T2的子类的情况下如果G< T2>也是G<
T1>的子类(注意左右与协变是相反的),那么类G就是逆变的。
与协变一样,下面举例说明一下。首先假设有类型为java.util.List<
Object>的变量s1,类型为java.util.List< String>的变量s2。
- java.util.List< Object> s1 = ...;
- java.util.List< String> s2 = ...;
String是Object的子类,由于Java的泛型规则不允许表达式s2=s1,所以将会出现编译错误。这里虽然String是Object的子类,但是java.util.List<
Object>并不是java.util.List<
String>的子类,所以Java的泛型并不是逆变的。如果Java的泛型是逆变的话,那同协变时情况一样,将会产生类型安全上的问题。
假设允许表达式s2=s1。由于s2的元素类型是String,所以从列表中取出元素后返回的类型因该是String。因此,如下代码因该是成立的。
- String str = s2.get(0);
但是,s2所指的列表s1的元素类型是Object,所以s1列表中的取出的元素并不仅限于String,这在类型安全性上就有问题了。
对于Scala的泛型,如果没有特别指示,与Java一样也不是逆变的。假设有如下含有apply方法的LessTan类(apply方法的逻辑是当a小于b时返回true,否则返回false)。
- abstract class LessThan[T] {
- def apply(a: T, b: T): Boolean
- }
如下使用了LessThan类的方法将会出现编译错误。
- val hashCodeLt: LessThan[Any] = new LessThan[Any] {
- def apply(a: Any, b: Any): Boolean = a.hashCode < b.hashCode
- }
- val strLT: LessThan[String] = hashCodeLt
- ...
编译错误的文本如下。显示的错误原因是在因该出现LessThan[String]的地方出现了LessThan[Any],由此看见LessThan类不是逆变的。
- (fragment of Comparator.scala):5: error: type mismatch;
- found : this.LessThan[Any]
- required: this.LessThan[String]
- val strLT: LessThan[String] = hashCodeLt
但是,在类或特征的定义中,在类型参数之前加上一个-符号,就可定义逆变泛型类和特征了。下面尝试一下定义Scala的逆变类。题材是前面的LessThan类,如下所示在LessThan定义的类型参数前加上-符号。
- abstract class LessThan[-T] { def apply(a: T, b: T): Boolean }
将LessThan类如此定义之后,前面错误代码的编译就可以通过了。另外,如果将类型定义为逆变后会发生类型安全性问题,则编译器将报编译错误。
实存(Existantial)类型
前面说过了Java泛型没有协变和逆变特性,但是通过使用Java的通配符功能后可以获得与协变与逆变相近的效果。通配符不是标记在类型定义的地方,而是在类型使用的地方,可以在使用类型处加上G<
? extends T1>或G< ? super T1>。
前者与协变相对应,当T2是T1的子类时,G< T2>是G< ? exnteds
T1>的子类。后者与逆变相对应,T1是T2的子类时,G< T2>是G< ? super
T1>的子类。因此,以下的代码将能正常编译。
- java.util.List< String> s1 = ...;
- java.util.List< ? extends Object> s2 = s1; //对应协变
- java.util.List< Object> s3 = ...;
- java.util.List< ? super String> s4 = s3; //对应逆变
- ...
由于通配符是标记在使用类型的地方,所以每次定义协变或逆变的变量时都要使用它,缺点是比较麻烦。另一方面,即使是没有定义为协变或逆变的泛型类型,也可以将其以协变或逆变的方式处理是它的优点。
Scala中也可以通过使用实存类型方法类实现与Java中通配符相同的功能。例如,下述Scala代码可以实现与上述Java代码相同的功能。
- //java.util.List[_ < : Any] (省略形式)
- var s1: java.util.List[String] = new java.util.ArrayList
- var s2: java.util.List[T] forSome { type T < : Any } = s1
- //java.util.List[_ >: String] (省略形式)
- var s3: java.util.List[Any] = new java.util.ArrayList
- var s4: java.util.List[T] forSome { type T >: String} = s3
- ...
结构(Structural)类型
在类似于Java的语言中只有在类定义好之后才能确定他们的继承关系。假设有如下A、B、C三个Java类定义。
- class A {
- void call() {}
- }
- class B extends A {
- void call() {}
- }
- class C {
- void call() {}
- }
这时假如有方法void foo(A a),那么类型A和B的实例可以作为参数传递给它,但是类型C却不能传递(虽然C中同样定义了方法call)。
这是由于,相比于类B通过exnteds
A语句明确标识了与A的继承关系,而C则没有明确标示出与A的继承关系,这样C就不是A的子类了。这对于Java和C#(C++的情况特殊除外)这类静态语言来说是理所当然的事,所以意识到的人因该不多吧。另一方面,在动态语言社区中,把这称为duck
typing,较普遍的看法是“无论是否有继承关系,只要在对象中存在需要的方法就可以了。”。以刚才的代码为例,A、B和C中都定义了call方法,如果foo方法中只调用call方法的话,那么类C的实例也可以作为参数传给foo。
时常听到这种说法“正是由于动态语言中没有静态类型检查所以才可以使用duck
typing功能。”。但是稍微仔细想一下,虽然是静态语言,但是在编译时还是知道类型持有了哪些方法的。理论上,即使不牺牲静态类型检查,也应该可以描述只要含有某一方法集合就OK的类型的。
Scala中通过结构类型来描述这种类型。结构类型的使用方法比较简单,只要在类型声明的地方,用{}将所需方法的声明括起来就可以了。
- def foo(callable: { def call: Unit }) = ...
另外在列举方法的个数比较多的情况下,可以如下所示来定义别名,这样就不用每次都列出所有方法了,只需使用结构类型的别名即可。
- type Callable = { def call: Unit }
复合(Compound)类型
在使用Java时,是否有过想用“即继承了类型A又实现了接口B”的类型的想法呢?Java中除了类型参数的限制之外,对于这种类型也没有定义方法。然而,Scala中则可以非常简单地描述这种类型。描述方法就是在一般的类名称之后用with来连接附加的类型。
例如对于如下的变量f来说,只要是实现了java.io.Closeable和Readable接口的对象,谁都可以赋值给变量f。
- var f: java.io.Closeable with Readable = ..
相关推荐
操作系统的定义功能与类型.pptx
**系统功能数据类型**列出了与系统功能相关的数据类型。 - **LED_ID**:表示SetLEDBehaviour功能LedId参数代码。 - **LED_BHV**:表示SetLEDBehaviour功能LedBhv参数代码。 - **LED_BHV_ERROR**:表示检测到的...
资金管理系统功能需求收集 资金管理系统是指对资金的管理和运营的自动化系统,旨在提高资金管理效率、降低成本、提高资金利用率和风险控制。下面是资金管理系统的功能需求收集: 账户管理 账户管理功能是资金管理...
对读者的类型和读者档案进行管理,包括添加,修改,删除读者类型和读者用户的相关信息,管理不同类型读者借阅图书的数量。 图书管理功能: 包括对图书类型和具体图书信息的管理,可以增加,修改,删除图书,丰富具体图书的...
在计算机编程中,获取操作系统类型通常是为了实现跨平台兼容性或者进行特定的操作系统功能调用。易语言提供了相关的系统API调用来实现这一目标。例如,可以使用“系统环境”类的“获取操作系统信息”命令来得到当前...
比如,银行可能需要区分不同业务类型的队列(如存款、取款、咨询等),而点菜系统则可能需要结合菜品准备时间来优化呼叫顺序。 总结来说,叫号系统是一个集成了数据管理、用户交互和流程控制的复杂系统。它的设计和...
退化生态系统中不同植被恢复类型对土壤微生物群落功能多样性的影响,毕江涛,贺达汉,本研究的目的是分析退化生态系统中不同植被恢复类型对土壤微生物群落功能多样性的影响,以评价土壤微生物群落对不同恢复植被的响
【中国植被类型图】是一种地理信息系统(GIS)数据文件,主要描述了中国...总的来说,"中国植被类型图"是一个重要的地理信息资源,通过GIS技术可以深入探究中国的生态系统特征,为环境保护和可持续发展提供决策支持。
某送水公司的送水系统 功能要求: ① 实现工作人员、客户信息的管理; ② 实现矿泉水类别和供应商的管理; ③ 实现矿泉水入库管理和出库管理; ④ 实现费用管理; ⑤ 创建触发器,实现入库、出库时相应类型矿泉水的...
四、系统功能模块 该系统的功能模块主要包括四大块: 1. 增加的功能模块:编写基本信息和联系方式。 2. 修改的功能模块:修改联系人信息。 3. 删除的功能模块:删除联系人信息。 4. 查询的功能模块:根据相应的...
总结,Android调用系统分享功能的核心在于创建和启动一个ACTION_SEND的Intent。开发者可以根据不同的数据类型设置Intent,然后通过系统选择器或自定义菜单提供给用户多种分享途径。在实践中,理解和掌握这部分内容...
在系统功能概要设计中,将上述功能模块化,明确每个模块的输入、输出、处理逻辑,绘制系统流程图,规划系统接口(外部接口与用户交互,内部接口连接各功能模块),确保系统的高效运作和良好的用户体验。 综上所述,...
1、系统需求分析及数据库建模 2、主界面WebOS搭建 3、实现图书馆管理系统的登录 4、实现图书馆信息管理 5、更改口令及利用用户自定义控件技术简化代码 6、利用母板页技术及JSOOP思想实现书架信息管理(1) 7、利用母板...
本系统还封装了文件管理功能,在其他模块如若要实现图片/文件上传预览时,前端只需导入现成的 Vue 组件即可实现(使用 viewerjs 依赖实现),后端只需定义 String 类型的实体类变量即可,无需再去研究文件上传预览的...
在系统档案方面,手册提供了档案类型的分类和新建档案的详细步骤,包括个人档案、公司档案、旅行社档案以及销售员档案的创建方法。同时,还阐述了如何查询档案和对档案进行管理操作。对于档案的操作,手册详细说明了...
自定义window服务,window服务开启以后,执行定时任务,完成某些预警功能(类型window操作系统自带的定时任务功能)
该系统的用户是汽车租赁企业的管理人员,其需求功能应包括汽车类型管理、会员类型管理、保险类型管理、销售商管理、保险公司管理、客户信息管理、会员信息管理和汽车租赁、续租、归还管理等主要功能,从而可以形成...
"食堂饭卡管理系统" 食堂饭卡管理系统是西华大学的一种信息管理系统,旨在方便学生...该系统的设计和实现需要考虑多方面的因素,包括需求分析、数据库设计、系统功能设计、界面设计、系统实现、系统测试和系统维护等。