Scala学习笔记(3)-面向对象编程上篇
本文是对Scala语言面向对象编程的学习总结(上篇),共包括如下章节:
- 概述
- 类的定义
- 包(package)
- 成员访问控制
- 对象
- 构造函数
- 小结
参考资料:
1、如果要了解scala开发环境的搭建,可参考《Scala学习笔记(1)-快速起步》 。
2、如果要了解scala语言的基本语法,可参考《Scala学习笔记(2)-基础语法》。
3、如果要了解scala语言的面向函数的编程知识,可参考《Scala学习笔记(5)-函数式编程》。
一、概述
Scala是一种纯面向对象的编程语言,其所有的值都是对象。Scala面向对象的机制和java,c++类似,一些通用的面向对象的特点都支持,也有些自己特有的特性。下面章节会详细介绍。
二、类的定义
(一)基本概念
类是面向对象编程中的基本单元,类是创建对象的蓝图(或模板)。同java一样,scala中的类通过关键字class定义,对象使用new关键字创建。
定义一个最简单的类,只需要关键字class和类名。如下面例子:
class User
虽然scala不要求类名的首字母大写,但建议大写。定义了类之后,就可以创建对象,同java一样,创建对象使用new关键字,如下面代码:
var user = new User
println(user)
可以看出,User类没有显示的定义构造函数,有一个默认的空的构造函数。创建对象时,可以省略()。也就是说,var user = new User() 也是合法的。
面向对象编程的最基础特征就是封装,也就是将数据和操作放到一个类中,我们在面向对象的设计中,最重要的工作就是根据业务模型来设计类,将数据和匹配的操作封装到一个类中。其中的数据和操作都称为类的成员,而数据一般称为类的成员变量,操作称为类的成员方法。
下面我们来看看scala中如何定义成员变量和方法。
(二)成员变量
我们来看一个简单例子:
class Person{
val name:String=""
var age:Int=20
}
上面定义了一个Person类,类中有2个成员变量,其中name变量用关键字val来定义,age变量用关键字var来定义的。对于scala类中的成员变量声明,有如下特点:
1、var和val区别是,val定义的成员变量的值后续不能再改变。
2、成员变量声明需要要初始化(否则就要定义为抽象类,关于抽象类的概念后面章节会介绍),如上面例子。如果变量有初始值,就可以省去类型定义,因为scala可以自动推断。如下面定义类。
class Person{
val name=""
var age=20
}
3、对于var定义的成员变量,可以用占位符_来代替具体的值,如下面定义。
class Person{
val name=""
var age:Int = _
}
用占位符_来替换实际的值进行初始化,则该变量的值就是该类型的默认值,比如对于Int型的,就是0;对于引用型的(AnyRef子类的),如String,则默认值为null。
除了在类中定义成员变量外,还可以通过构造函数来定义成员变量,这个在后面章节会介绍。
(三)成员方法
在scala中,使用def关键字定义方法。 def后跟方法名,参数列表,返回值类型和方法体。语法格式如:
def 函数名(参数列表):返回值类型 = {
方法体代码
}
下面看一个方法定义的例子:
def add(first:Int, second:Int):Int = {
val sum = first+second
return sum
}
上面定义了一个标准方法。
相比java中的方法,scala的方法定义比较灵活,有很多需要注意的细节,下面一一介绍:
1、scala的参数是不可变的,即不能给它重新赋值。
2、return关键字可省略,在scala中,所有语句都是bu表达式,表达式总是会返回一个值。省略了result, 方法体内最后一个被执行的语句作为表达式所返回的值就是方法的返回值。如:
def add(first:Int, second:Int) :Int= {
val sum = first+second
sum*2
}
3、如果方法体中只有一个语句,则可省略{},如:
def add(first:Int, second:Int) :Int = first+second
4、如果可以通过方法体最后一个表达式推断出返回值类型,则方法的返回值类型声明可以省略。如:
def add(first:Int, second:Int) = first+second
需要注意的是:如果方法中出现了return语句,则方法声明必须显示的定义返回值类型,如下面的代码会编译报错:
def add(first:Int, second:Int) = first+second
如果省略了返回值类型,则也可以 = ,但不能省略{}。如下面语法也是正确的:
def add(first:Int, second:Int) = {
if(first>second)
return first+second
else
return first-second
}
5、Unit类型
在c/c++及java中,如果方法没有返回值,则方法的返回值声明为void。我们看看同样代码在scala中是什么情况。如下面方法定义:
def hello() = {
println("hello,world")
}
如果按照java语言来看,这个方法是没有返回值的。我们来测试下,在scala交互式程序中定义和调用该方法,执行过程如下:
scala> def hello() = {
| println("hello,world")
| }
hello: ()Unit
scala> val re = hello()
hello,world
re: Unit = ()
scala> println(re)
()
对比前面例子的执行输出。根据上面的执行输出,可以看出hello方法是有返回值的,返回值类型为 Unit ,具体的值为()。Unit是scala中的一种数据类型,表示没有值,其有唯一的实例()。在scala中,方法如果没有返回具体的值,那返回的类型就设置为Unit类型。
(四)不带()的方法定义
如果一个方法没有参数,则方法名后面的()可以省略,如下面是正确的代码:
class Demo{
def name = "tom"
}
上面代码中的Demo类中定义了一个方法name,注意是方法,不是变量。这时name方法没有带()。对于不带()的方法,在调用时也不能带(),如name()这样调用是错误的,只能name这样调用。
如果 def name()= "tom"这样定义时带(),则调用时既可以带(),也可以不带()。看到这里,会觉得scala的语法棉花糖太多了。
不带()的方法,还有一个特点时,子类可以重写该方法,不仅可以用方法去重写,也可以用变量去重写。如下面代码:
class Child extends Demo{
override val name="jack"
}
上面Demo类的子类Child重写了name方法,但重写的时候定义的是变量,而不是方法(当然重写成方法也是没问题的)。
Scala作者建议,如果一个方法在逻辑上表达一种属性的返回值,那么在定义方法时尽量使用不带括号的写法,因为这样看上去更像一个类的属性,而不像一个方法。需要注意的是,由于不带括号的方法比带括号的方法在使用上更严格,因此将来要把一个带括号的方法定义改为不带括号的方法定义就比较麻烦,因为需要先将所有带括号的方法调用,比如name(), 统统改为不带括号的。
(五)方法参数的默认值
scala的方法参数支持设置默认值,对于有默认值的参数,调用方法时如果不传入参数值,则使用默认值。如下面例子:
object Hello {
def main(args: Array[String]){
test()
test("jack")
test("jack",20)
}
def test(name:String="tom",age:Int=3)={
println(name,age)
}
}
上面代码中定义的test方法,有两个缺省值的参数。main方法中的三种调用方式都是正确的。
如果只有部分参数有默认值,则没有有默认值的参数建议放在参数列表的前面,否则在不设置有默认值参数时需要以带参数名的方式来调用方法。如下面例子:
object Hello {
def main(args: Array[String]){
test("jack",20)
test(age=20)
}
def test(name:String="tom",age:Int):Unit={
println(name,age)
}
}
scala的方法支持在调用时带参数名调用,这样传入参数的顺序就不必要按照方法定义参数的顺序传入。
(六)方法返回多个值
在Java/c++中我们知道,方法的返回值只能有一个值,如果我们需要返回多个值,只能返回一个对象(对象中有多个成员变量),或者返回一个数组,或集合对象。但在scala中,利用元组类型,yuanzu可以直接返回多个值,让代码编写起来更加简单,我们先看一个例子:
object Hello {
def main(args: Array[String]){
var (name,age) = test
println(name)
println(age)
}
def test={
("tom",12)
}
}
上面例子代码中,test方法返回一个元组类型的值,在main方法中通过 var (name,age) = test 来提取test方法返回的元组中的所有元素。
(七)this关键字
方法是类的一部分,在方法的实现代码中,可以直接访问本类中的其它成员。this关键字代表对象本身,可以通过this来引用成员,这与java中的this关键字作用是一样的。
当成员名和局部变量不冲突时,可以省去this关键字。如下面例子:
class Person{
var name:String="tom"
def show={
println(name)
}
def setName(name:String)={
this.name = name;
}
}
上面代码中show方法直接使用了成员变量name,省去了this关键字。而setName方法中因为存在同名的局部变量,所以需要通过this关键字来引用成员变量。
(八)方法重载
同Java一样,scala也支持方法重载,即一个类中存在多个同名的方法,但参数不同。调用时,scala会根据传入的参数不同调用匹配的方法。如下面例子:
class Person{
var name:String="tom"
var age:Int=10
def setData(name:String)={
this.name = name;
}
def setData(name:String,age:Int)={
this.name = name;
this.age = age;
}
}
上面代码中有两个同名的方法setData,但它们的参数列表不同。
三、对象
(一)单例对象
在java中,在类中可以通过static关键字定义静态的成员变量和方法,静态成员是归属类的,与对象无关。可以直接通过类名来访问。
在scala中,并不支持静态成员。但是scala可以通过定义单例对象来实现同样的功能。这里的单例对象不是如java中是一种设计模式,在scala中是一种具体的语法。
定义单例对象不使用class关键字,而是使用object关键字。我们先看一个简单例子:
object User{
var name="tom"
}
val name = User.name
User.name = "jack"
上面代码使用object关键字定义了一个单例对象,然后直接通过对象名即可访问对象中的成员,类似Java中的静态变量。在scala中,单例对象不是一个类,它就是一个对象(类似在java中自己实现的一个单例),只不过不需要由自己创建,而是由scala运行环境帮创建,并且在整个运行过程中只有一份,我们也不能通过new关键字来针对单例对象再创建一个对象。
在scala中,一个单独可执行的scala程序,至少需要一个单例对象,且该单例对象中有一个 def main(args: Array[String]) 这样的main方法,main方法就是程序的入口,类似Java中的静态main方法作为java程序的入口。
(二)伴生类与伴生对象
在Java中,一个类中即可以定义静态成员,也可以有实例成员。但在scala中,把这两个分开了。静态成员的功能由单例对象来实现。
在scala中,可以定义同名的单例对象和类。如果存在同名的单例对象和类,则该对象和类之间就会产生关系。在scala中,会把单例对象称为同名类的伴生对象,会把类称为同名单例对象的伴生类。如果一个单例对象没有同名的伴生类,我们一般称这个对象为孤立对象,反之称为伴生对象。
举个例子,假设我们定义了object(单例对象)Person和class(类)Person,则 object Person称为class Person的伴生对象,而class Person则称为object Person的伴生类。
伴生类和伴生对象有如下几个注意的地方:
1、每个类都可以有伴生对象,伴生类与伴生对象写在同一个文件中;
2、伴生类中和伴生对象可互相访问其private字段。关于成员访问控制,在下一章节会详细介绍。
四、包(package)
(一)基本概念
scala中的package功能,同Java中的包、c++中的命名空间一样,用于大型工程代码的组织,同时解决命名冲突的问题。
scala中的package与java中的package很类似,如下面例子:
package com.demo
class Test{
def show ={
println("test")
}
}
上面定义的Test类,位于com.demo包下,包路径加类名才是类的唯一标识。其它包中的类使用Test类时,可以使用com.demo.Test来引用,如:
object Hello {
def main(args: Array[String]){
var test = new com.demo.Test
test.show
}
}
显然,如果在一个文件中多次使用到Test类,com.demo.Test这样的写法比较臃肿。同Java一样,scala支持import语句来简化代码的编写。如下面例子:
import com.demo.Test
object Hello {
def main(args: Array[String]){
var test = new Test
test.show
}
}
上面代码通过import com.demo.Test导入了指定的类,这样我们就可以使用Test类时不用带包路径。
如果我们希望将一个package中的所有类都导入,而不需要显示的导入每个类,在java中用来代替具体的类名,如import com.demo. ,在scala中,是用_代替*。如下面例子:
import com.demo._
object Hello {
def main(args: Array[String]){
var test = new Test
test.show
}
}
对于单例对象,我们还可以将对象中的成员导入,这样使用时可以直接使用成员名,不用写单例对象名。类似java中的静态导入。如下面例子:
package com.demo
object Test{
def show ={
println("test")
}
}
上面代码定义了Test单例对象,我们希望直接使用其中的show方法,而不需要Test.show这样使用,方法如下:
import com.demo.Test.show
object Hello {
def main(args: Array[String]){
show
}
}
同样,我们可以使用import com.demo.Test._ 来导入Test对象中的所有成员,而不需要一个个的导入具体的成员。
(二)import的高级特性
import除了最常见的使用外(如上面例子),还有一些特殊的使用场景。
1、缺省导入
对于scala包中的所有类,不需要显示的import,scala会自动导入。比如我们用到的基础数据类型 Boolean , Int类型都位于scala包中,但我们使用时并没有显示导入。
对于scala.Predef(是一个单例对象)中的所有成员,scala会自动导入,不需要显示导入。比如String类型,就是scala.Predef中的一个成员,我们可以直接使用。
2、重命名
如果同时需要使用两个不同包中的同名的类,普通的import方式就会存在冲突。比如:
import java.util.HashMap
import scala.collection.mutable.HashMap
上面两个语句都导入了相同名字的HashMap,如果我们代码中直接使用HashMap,就会报错。这时,我们当然可以使用全路径来区分。但scala提供了一种重命名的方式,举例如下:
import java.util.{ HashMap => JavaHashMap }
import scala.collection.mutable.HashMap
object Hello {
def main(args: Array[String]){
var map1:HashMap[String,String]=null
var map2:JavaHashMap[String,String]=null
}
}
上面代码中第一个import语句将 HashMap 重命名为JavaHashMap ,这样使用时就不会冲突了。
五、成员访问控制
(一)回顾下Java中的方式
成员访问控制是指访问类成员时的权限控制。我们先回顾下Java中的成员访问控制,在java中,对类成员的访问有4种级别的控制,分别是:
-
private:私有成员,表示被private关键字修饰的成员只能被同一个类中的方法访问。
-
默认方式:包内成员,表示没有加任何修饰符的成员可以被同一个包(package)中的任何类的任何方法访问。
-
protected:保护成员,表示被protected关键字修饰的成员可以被包内访问,也可以被子类访问。
-
public:公开成员,表示被public关键字修饰的成员可以在任何地方被访问。
可以看出,java的上述4种级别的控制权限严厉程度从高到低,private最严格,只能被本类中方法访问;public最宽松,可以在任何地方被访问。private和public也是最常用的两种权限控制级别。
我们再来看scala的访问权限控制,与java类似,但也有些不同的地方。
(二)private方式
由private关键字修饰的类成员只能被该类的成员以及该类的伴生对象(后面会介绍)访问,这点同java的prviate级别一样。如下面例子:
class Demo{
private val value="hello"
def show{
println(value)
}
}
object Hello {
def main(args: Array[String]){
var demo = new Demo
demo.value=10 //会报编译错误
}
}
上面例子中,Demo类中定义了一个private的成员变量value,value可以被类中的show方法访问。但不能被外部类及非伴生单例对象访问,如在Hello单例对象的main方法中访问,会报编译错误。
(三)protected方式
由protected关键字修饰的成员只能在该类及其子类中访问,外部不能访问,类似java中的protected权限控制。
(四)默认方式
没有加任何关键字修饰的成员,可以在任何地方被使用,类似java中的public关键字,这点与java的默认方式完全不同。我们前面的很多例子都是没加任何修饰符。
(五)private[this]方式
在scala中还存在使用private[this]修饰的成员,如下面代码:
class Demo{
private[this] val value="hello"
}
被private[this]修饰的成员只能在类的内部使用。看到这里可能觉得奇怪,这与单独的prviate关键字修饰有啥区别呢?
prvivate[this]修饰和prviate修饰非常类似,它们之间的最主要区别在于伴生对象的访问权限控制。
class Demo{
private var name="tom"
private[this] var value="hello"
}
object Demo{
def show{
val demo = new Demo
demo.name = "jack"
demo.value = "world" //会报编译错误
}
}
上面代码中,Demo单例对象是Demo类的伴生对象,在单例对象中,可以访问Demo类(即其伴生类)的private成员,但不能访问private[this]成员,可以看出,prviate[this]比private的权限控制更严格。
同样,我们看下伴生类中使用伴生对象的例子:
object Demo{
private var name="tom"
private[this] var value="hello"
}
class Demo{
def show{
Demo.name = "jack"
Demo.value = "world" //编译报错
}
}
在伴生类中,访问伴生对象,可以直接访问private成员,但不能访问private[this]成员,会报编译错误。
说明:scala还有包作用域的访问控制方式,但需要改变packge和类的编写方式,不太常用,这里就不作介绍。
六、构造函数
(一)基本概念
同java一样,scala类也有构造函数,创建对象时会调用构造函数。我们先回顾下java的构造函数特点,一个java类可以显示的定义0个或多个构造函数,有如下特点:
1)构造函数名必须与类名一样,无返回值
2)如果没有显示定义构造函数,则java会自动帮创建一个默认的构造函数(没有参数)
3)如果定义了构造函数,则java不会帮创建默认的构造函数
4)如果定义了多个构造函数,这些构造函数属于重载,需要满足方法重载的要求(即参数列表不能完全一致),这些构造函数无主次之分
5)创建对象时,java根据所调用的构造函数传入的参数来决定调用哪个构造函数
scala的构造函数与java类似,但也有些差别,最主要的差别是:scala的构造函数分为主构造函数和辅助构造函数,有且只有一个主构造函数,可以有0个或多个辅构造函数。
(二)主构造函数介绍
下面我们来看下主构造函数的定义。在Scala中,每个类都有主构造函数。但在前面的例子中,我们定义的Person类中没有定义主构造函数。这如java一样,这时由scala自动帮生成了一个默认的不带参数的主构造函数。
在scala中显示定义带参数的主构造函数,不是去单独定义一个构造函数方法,而是和类的定义交织在一起。如下面例子:
class Person(var name:String,var age:Int){
}
下面我们来创建和使用Person对象,如下面例子:
scala> val per = new Person("tom",20)
per: Person = Person@57b33c29
scala> val name = per.name
name: String = tom
scala> per.name="jack"
per.name: String = jack
通过上面的例子代码可以看出,我们要定义带参数的主构造函数,只需在class后面的类名后面加上相应的参数,而且这些参数自动会变成类的成员变量(我们不能再在类中定义同名的成员变量了)。这比在java中完成同样的功能所需代码简洁很多。
这里还有一个问题,我们知道,在Java中我们可以在构造函数中编写代码。但scala的主构造函数没有对应的方法,如果我们希望在调用主构造函数创建对象时也能执行一些初始化操作,那代码放在哪里呢? 在scala中,我们可以在类的主体(即{})中添加任意的代码,这样在创建对象时,从开始到结尾所有代码都会被执行。如下面例子:
class Person{
val name=""
var age:Int = _
println("i am created")
age=12
println("name="+name+",age="+age)
}
下面我们创建一个对象,在scala命令行中执行会看到如下结果。
scala> var obj = new Person
i am created
name=,age=12
obj: Person = Person@2f2dc407
scala> val age = obj.age
age: Int = 12
可以看出,类中的这些代码都被执行了。
我们在声明主构造函数时,同普通的方法一样,可以给参数设置默认值,这样在创建对象时可以不传入参数值。如下面例子:
scala> class Person(var name:String="tom",var age:Int=20){ }
defined class Person
scala> val per1 = new Person
per1: Person = Person@503fa290
scala> println(per1.name+"="+per1.age)
tom=20
scala> val per2 = new Person("jack",10)
per2: Person = Person@7beba1a8
scala> println(per2.name+"="+per2.age)
jack=10
上面代码给主构造函数的参数设置了默认值,这时我们在创建对象时可以不传入参数值,成员变量的初始值就是设置的默认值。当然我们也可以在创建对象时传入值,这样成员变量的值就是创建对象时传入的值。
为主构造函数设置参数默认值,如果有多个参数,也可以只给部分参数设定默认值。如:
class Person(var name:String,var age:Int=12){ }
这样创建对象可以不传入有默认值的参数,如
new Person("jack")
当然,如果是前面参数有默认值,则创建对象时需要指定参数名,如:
class Person(var name:String="tom",var age:Int){ }
new Person(age=12)
(三)辅助构造函数
上面我们介绍了scala类的主构造函数,主构造函数有且只有一个。对于scala类,还可以显示的定义0个或多个辅助构造函数。
对于辅助构造函数有如下要求:
1)以this作为方法名定义(不同于java中的以类名定义)
2)每一个辅助构造函数的第一行代码必须是对主构造函数或其它的辅助构造函数的调用,通过this方法调用。这意味着主构造函数一定会被辅构造函数调用(直接或间接)。
3)辅助构造函数的参数不能加var或val标记。
4)辅助构造函数的参数不会成为成员变量,其作用是用来给成员变量传值的。
class Person{
var name=""
var age:Int =0
def this(name:String)={
this()
this.name = name
}
def this(name:String,age:Int)={
this(name)
this.age = age
}
}
创建对象时,scala会自动根据传入的参数来调用相匹配度的主构造函数或辅助构造函数。所以scala的各个构造函数之间要符合重载的要求。下面的创建对象都是正确的:
scala> var p = new Person
p: Person = Person@54972f9a
scala> var p = new Person("A")
p: Person = Person@525aadf2
scala> var p = new Person("A",12)
p: Person = Person@4a577b99
(四)主构造函数中的成员访问控制
前面介绍主构造函数我们已经知道,可以通过主构造函数方便的为类设置成员变量,如下面例子:
class Person( var name:String)
上面代码定义了Person,通过主构造函数声明了一个成员变量name。这种方式下,name的权限都是默认方式,即公开方式。
如果我们希望设置name的访问权限为private或protected,则只需在var关键字前面加上相应的关键字即可,如:
class Person( private var name:String)
如果我们希望name的访问权限为private[this]方式,即完全私有的,则一种方式是加上private[this]修饰符,还有一种更简单的方式是不使用var或val关键字定义,这样声明的成员变量就是private[this]的方式,如:
class Person(name:String){
def show{
println(name)
}
}
(五)构造函数的私有化
默认情况下,构造函数都是公有化的,在创建对象时可以直接调用。如果我们希望某个构造函数私有化,只在类的内部使用而不对外开放。这时只需加上private关键字修饰符。
1、对于主构造函数,private关键字加在类名和()之间。
2、对于辅助构造函数,private关键字加在def之前即可。
七、小结
本文对scala面向对象的编程的基本概念和特点进行了总结,涉及类的定义、单例对象、成员访问控制、构造函数等内容。面向对象的编程还有很多重要特性,如继承、多态等,这些会在下篇总结文章中介绍。