Scala类与对象

类简介

简介

类是对象的蓝图。一旦你定义了类,就可以用关键字new根据类的蓝图创建对象。在类的定义里,可以放置字段和方法,这些被笼统地称为成员。对于字段,不管是val还是var定义的,都是指向对象的变量。对于方法,用def定义,包含了可执行代码。字段保留了对象的状态或数据,而方法使用这些数据执行对象的运算工作。当类被实例化的时候,运行时环境会预留一些内存来保留对象的状态映像——即变量的内容。

示例

创建类示例:

class SumAccumulator {
    var sum = 0
}

然后实例化两次:

val acc: SumAccumulator = new SumAccumulator
val csa: SumAccumulator = new SumAccumulator 

这时内存里对象的状态映像如下:

 

由于字段sum是var类型而非val,所以sum可以被赋予新的Int类型的值:例如

acc.sum = 3

此时映像会变成:

 

图中同时存在两个sum变量,一个在acc指向的对象里,另一个在csa指向的对象里。字段的另一种说法是实例变量(instance variable),因为每一个实例都有自己的变量集。

尽管acc和csa都是val类型,但是还是可以修改acc指向的对象的值(上面的sum)。val类型对象对acc(或csa)的限制仅限于不可以把它们再次赋值给其他对象。例如,下面的尝试将会失败:

//编译不过,因为acc是val
acc = new ChecksumAccumuulator

因此,我们可以得出结论,acc将始终指向初始化的Checksuumaccumulator对象,但是对象包含的字段可以随时改动。

Scala类的重要特性

访问修饰符

  • Public是Scala默认的访问级别

为了保证对象的健壮性,可以把类中字段变为私有的(private)以阻止外界直接对它访问。因为私有字段只能被定义成在同一类里的方法访问,所有跟新字段的代码将锁定在类里。要声明字段是私有的,可以把访问修饰符private放在字段的前面。

代码示例:

class SumAccumulator {
    private var sum = 0
} 

使用private修饰后,任何从类外部对sum的访问都将失败

val acc: SumAccumulator = new SumAccumulator
acc.sum = 3 //编译不通过,因为sum是私有的

方法

1. Scala方法参数类型

Scala方法参数中的类型都是val而不是var类型

def add(a: Int, b: Int): Int = {
    a = a + b //编译错误:Reassignment to val (就是对val类型赋值的错误)
    a
}    

2. Scala方法中return

Scala方法中的return可以去掉,从而简化代码

代码示例:

def numSum(num:Int):Int ={
    val sum = num + 10
    sum //省略return关键字,简化代码
 
}

此时返回值就是sum

3. Scala方法中的“=”号

Scala方法中的“=”号非常重要:因为Scala编译器可以把任何类型转换为Unit如果定义的函数中”=”号忘记了添加,那么Scala会默认它返回值为Unit类型。若你本来想返回一个String类型的值,由于没有添加”=”,String类型返回值会被转换为Unit类型而丢弃掉。

代码示例:

def printStr() {
    "这是String类型的返回值"
}

返回结果为:   ()

正确代码示例:

def printStr(): String = {
    "这是String类型的返回值"
}

 返回结果为:    这是String类型的返回值

4. Scala方法表达式

假如某个方法仅计算单个结果的表达式,这可以去掉花括号,如果结果表达式很短,甚至可以把它放在def的同一行里。

代码示例:

def numSum(num:Int):Int = num + 10

5. Scala中分号推断

scala程序中,语句末尾的分号通常是可选的,若一行仅有一个语句也可以不加分号。不过,如果一行包含多条语句时,分号则是必须的。

不加分号:

if(x < 2)
println("too small")
else
println("ok")

必须加分号:

val x = 10; println(x)  //两个语句,必须加分号

Scala通常的风格是把操作符放在行尾而不是行头:

错误示例:

val x = 10;
val y = 3
val z = x //它会被编译器识别为z = x ; +y 两个语句
+y

打印结果:10

正确示例:

val x = 10;
val y = 3
val z = x +
y
println(z)

打印结果:13

Scala分号推断规则:

  1. 疑问行由一个不能合法作为语句结尾的字结束,如句点或中缀操作符。
  2. 下一行开始于不能作为语句开始的词。
  3. 行结束于括号(...)或方框[...]内部,因为这些符号不能容纳多个语句。

6. Scala无参方法

调用Scala类中无参方法时,可以写上括号,也可以不写。对于类中的取值器来说,去掉()是不错的风格

代码示例:

object exam1 {
def main(args: Array[String]): Unit = {
val per: Person = new Person
per.talk() //ok
per.talk //同样ok
}
 
}
 
class Person {
def talk(): Unit = println("Talking")
}

如果你想强制使用这种风格,可以在声明方法时不带()

代码示例:

per.talk() //此时这种写法是错误的
per.talk //OK的
class Person {
def talk = println("Talking")
}

Singleton对象

Scala比Java更为面向对象的特点之一是Scala不能定义静态成员,而是代之以定义单例对象(singleton Object)。除了用object关键字替换了class关键字以外,单例对象的定义看上去与类定义一致。

例子:ChecksumAccumulator.scala源码:

class ChecksumAccumulator {
    private var sum = 0
    def add(b:Byte){sum += b}
    def checksum():Int = ~(sum & 0xFF) + 1
}
import scala.collection.mutable.Map
object ChecksumAccumulator {
    private val cache = Map[String, Int]()
    def calculate(s:String):Int=
        if(cache.contains(s))
            cache(s)
        else {
            val acc = new ChecksumAccumulator
            for(c <- s)
                acc.add(c.toByte)
            val cs = acc.checksum()
            cache += (s -> cs)
            cs
        }
}

上面源码中的单例对象叫做ChecksumAccumulator,与前一个例子里的类同名。当单例对象与某个类共享同一个名称时,它就被称为是这个类的伴生对象(companion object)。类和它的伴生对象必须定义在一个源文件里。类被称为是这个单例对象的伴生类(companion class)。类和它的伴生对象可以相互访问其私有成员。

这段缓存代码的说明如下:

类和单例对象间的差别是,单例对象不带参数,而类可以。因为单例对象不是用new关键字实例化的,所以没有机会传递给它实例化参数。每个单例对象都被实现为虚拟类(synthetic class)的实例,并指向静态的变量,因为它们与Java静态类有着相同的初始化语义。特别要指出的是,单例对象在第一次被访问的时候才会被初始化。

不与伴生类共享名称的单例对象被称为独立对象(standalone object)。它可以用在很多地方,例如作为相关功能方法的工具类,或者定义Scala应用的入口点。

Scala程序

  想要编写能够独立运行的Scala程序,就必须创建有main方法(仅带一个参数Array[String],且结果类型为Unit)的单例对象。任何拥有合适签名的main方法的单例对象都可以用来作为程序的入口点。

Summer.scala文件源码:

import ChecksumAccumulator.calculate

object Summer {
    def main(args:Array[String]) {
        for(arg <- args)
            println(arg + ": " + calculate(arg))
    }
}

要执行Summer应用程序,需要把以上的代码写入文件Summer.scala中,因为Summer使用了ChecksumAccumulator,所以还要把ChecksumAccumulator的代码,上面的源码(类及它的伴生对象),放在文件ChecksumAccumulator.scala中。

Scala和Java之间有一点不同,Java需要类名称与源码文件名同名,而在Scala对于源文件的命名没有硬性规定。然而通常情况下如果不是脚本,推荐的风格是像在Java里那样按照所包含的类名来命名文件,这样程序员就可以比较容易地根据文件名找到类。

Scala的脚步必须以结果表达式介绍。因此如果你尝试以脚本方式执行Summer.scala,Scala解释器将会报错说Summer.scala不是以结果表达式结束的。正确做法是:需要用Scala编译器真正的编译这些文件,然后执行输出的类文件,方式之一使用Scala的基本编译器,scalac。

D:workworkspacescala>scalac ChecksumAccumulator.scala Summer.scala

D:workworkspacescala>

D:workworkspacescala>fsc ChecksumAccumulator.scala Summer.scala

D:workworkspacescala>

 

D:workworkspacescala>scala Summer of love
of: -213
love: -182

D:workworkspacescala>

Scala中的Application

为了使代码更简洁,Scala还提供了另外一种运行Scala程序的方式,那就是直接继承scala.Application接口(Trait)。
直接继承自Application的运行方式:
object RunAppWithoutMain extends Application {  
    println("runing scala app without main")  
}
之所以这里无须定义main方法,那是因为在Application这个接口中定义了一个main方法,main方法在执行时会初始化RunAppWithoutMain这个对象,并执行它的主构造方法,而所有直接写在对象中的代码都会被scala编译器收集到主构造方法中,于是就被运行了。
extends Application虽然比编写main方法要方便,但是也有一些副作用。直接继承自Application导致的副作用:
1. 无法接受命令行参数。因为args参数不会被传入
2. 在Scala中,如果一个程序是多线程的,那么这个程序必须具有一个main方法。所以第二种写法只能适用于单线程的程序
3. Application这个接口在执行一个程序的代码之前,需要进行一些初始化。而某些JVM不会对这些初始化代码进行优化。

所以第二种方法只适用于一些非常简单的场合,大部分情况不推荐使用。
 

Scala类中getter和setter

getter和setter

Scala类中使用公有字段的话,任何人都可以修改这个字段,使得安全性大大降低。所以我们更倾向于使用getter和setter方法:

Scala对类中每个字段都提供了getter和setter方法,分别叫做age和age_=,

代码示例:

创建一个exam1.scala文件,文件内容如下

class Person {
var age = 0
}

如果想查看这些方法,可以先编译Person类,用scalac命令编译,然后用javap查看字节码:

D:workworkspacescala>scalac exam1.scala

D:workworkspacescala>javap Person
Compiled from "exam1.scala"
public class Person {
  public int age();
  public void age_$eq(int);
  public Person();
}

D:workworkspacescala>

 

你可以自己重新定义getter和setter方法。

代码示例:

object exam1 {
 
    def main(args: Array[String]): Unit = {
        val per: Person = new Person
        per.age = 18
        per.age = 16 //由于在setter里面控制了age不能变小,所以执行结果age不会变
        println(per.age)
        per.age = 19 //使用setter,赋予大于原来的age
        println(per.age)
        }
    }
 
    class Person {
        private var privateAge = 0 //变成私有变量并改名
        def age = privateAge
        def age_=(newAge: Int) { // age_= 不能有空格
        if (newAge > privateAge) privateAge = newAge //使得年龄不能变小
    }
}

打印结果为:

18
19

Scala中每个字段生成getter和setter的限制:

  1. 如果字段是私有的,则getter和setter方法也是私有的。
  2. 如果字段是val,则只有getter方法被生成。
  3. 如果你不需要任何getter或setter,可以将字段声明为private[this]

Scala在实现类中属性时的四个选择:

  1. 自动生成一个getter和setter。
  2. 自动生成一个getter。
  3. 自定义foo和foo_=方法。
  4. 自定义foo方法。

Bean属性

JavaBean规范把Java属性定义为一堆getFoo和setFoo方法。类似于Java,当你将Scala字段标注为 @BeanProperty时,getter和setter方法会自动生成。

代码示例:

import scala.beans.BeanProperty
 
object exam1 {
    def main(args: Array[String]): Unit = {
        val per: Person = new Person
        per.name = "zhagnsan"
        per.setName("lisi") //BeanProperty生成的setName方法
        println(per.getName) //BeanProperty生成的getName方法
    }
}
 
 
class Person {
    @BeanProperty var name:String = _
}

打印结果为:

Lisi

上述类Person中由@BeanProperty生成了四个方法:

name: String
name_= (newValue: String): Unit
getName(): String
setName (newValue: String): Unit

图示为针对字段生成的方法:

 

Scala类中构造器

和java或C++一样,Scala也可以有任意多的构造器。不过,Scala类有一个构造器比其他所有构造器都更为重要,它就是主构造器。除了主构造器外,Scala类还可以有任意多的辅助构造器。

辅助构造器

Scala中辅助构造器和Java或C++十分相似,只有两处不同:

  1. 辅助构造器的名称为this。
  2. 每一个辅助构造器都必须以一个对先前已定义的其他辅助构造器或主构造器的调用开始

这里有一个带有两个辅助构造器的类。

代码示例:

object exam1 {
    def main(args: Array[String]): Unit = {
        val per1: Person = new Person //主构造器
        val per2: Person = new Person("Bob") //第一个辅助构造器
        val per3: Person = new Person("Bob",18) //第二个辅助构造器
    }
}
 
    class Person {
        private var name = ""
        private var age = 0
        //一个辅助构造器
        def this(name: String) {
            this() //调用主构造器 
            this.name = name
        }
 
        //另一个辅助构造器
        def this(name: String, age: Int) {
            this(name) //调用前一个辅助构造器
            this.age = age
        }
    }    

主构造器

在Scala中,每个类都有主构造器。主构造器并不以this方法定义,而是与类定义交织在一起

主构造器的参数直接放在类名之后

代码示例:

object exam1 {
def main(args: Array[String]): Unit = {
val per: Person = new Person("Bob", 18) //使用主构造器实例化对象
println(per.name + " : " + per.age)
}
}
 
class Person(val name: String, val age: Int) {
//产生私有的name和age,没有setter
//.....
}

打印结果:

Bob : 18

上述简短的Person类定义极大简化了相同功能的Java代码:

与上例相同功能的Java代码示例:

class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
 
public String name() {
return this.name;
}
 
public int age() {
return this.age;
}
}

Scala主构造器定义的所有语句都会执行

代码示例:

class Person(val name: String, val age: Int) {
//产生私有的name和age,没有setter
println("主构造器定义的所有语句都会执行") //每次使用主构造器时都会执行
def description = name + "is" + age + "years old"
}

不同类型的主构造器参数对应会生成的字段和方法:

 

如果想让主构造器变成私有的,可以像这样放置private关键字:

class Person private (val name: String, val age: Int) {
//......
}

这样一来,类用户就必须通过辅助构造器来构造Person对象了.

Scala嵌套类

在Scala中,你几乎可以在任何语法结构中内嵌任何语法构造。你可以在函数中定义函数,在类中定义类。

代码示例:

class Network {
 
//在Network类中定义类Member
class Member(val name: String) {
    val contacts = new ArrayBuffer[Member]
}
 
private val members = new ArrayBuffer[Member]
 
def join(name: String) = {
    val m = new Member(name)
    members += m
    m
}
}

考虑有如下两个网络:

al chatter = new Network
val myFace = new Network
 

在Scala中,每个实例都有它自己的Member类(内部类),就和它们有自己的members(内部类)字段一样。也就是说chatter.Member和myFace.Member是不同的两个类

作用:拿网络示例来说,你可以在各自的网络中添加成员,但是不能跨网添加成员。

代码示例:

val chatter = new Network
val myFace = new Network
val fred = chatter.join("Fred")
val wilma = chatter.join("wilma")
fred.contacts += wilma //OK
val barney = myFace.join("Barney")
 
//错误:不能将一个myFace.Member添加到chatter.Member元素缓冲中
fred.contacts += barney

在嵌套类中,你可以通过 外部类.this 的方式来访问外部类的this引用,就像Java那样。

class Network(val name: String) {
//在Network类中定义类Member
class Member(val name: String) {
//....
def description = name + "inside" + Network.this.name
}
}

如果你觉得需要,也可以使用如下语法建立一个指向该引用的别名:

class Network(val name: String) { outer =>
//在Network类中定义类Member
class Member(val name: String) {
//....
def description = name + "inside" + outer.name
}
}

上述语法使得outer变量指向Network.this。对这个变量,你可以使用任何合法的名称。

转自:https://blog.csdn.net/u011204847/article/details/51105362

原文地址:https://www.cnblogs.com/duanxz/p/9459394.html