欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Scala基础学习入门

程序员文章站 2022-06-19 19:37:44
...
 
从技术上来讲,scala程序并不是一个解释器,你在命令行中输入的内容被快速地编译成字节码,然后这段字节码交给Java虚拟机执行。
 
变量声明:
 
val val不能再继续赋值, 鼓励使用该命令方式
var 生命周期中可以被多次赋值 大多数程序并不需要那么多var变量
 
scala中变量或函数的类型总是写在变量或函数名称后面:
 
val greeting: String = "Hello"
 
 
仅当同一行代码中存在多条语句时才需要用分号隔开。
 
scala中的数组
 
如果数组的长度固定,使用Array,如果长度可能有变化则使用ArrayBuffer,通过ArrayBuffer.toArray方法返回Array;
如果需要遍历数组,数组和数组列表有一些语法上的不同,for循环的语法:
 
for (i <- 0 until a.length)
     println(i+”:”+a(i))
 
 
until是RichInt类的方法,返回所有小于(但不包括)上限的数字,可以使用 until(end, step)来限定跳跃的步数,进行定制化的遍历操作;还可以使用reverse方法来反转整个Range。
 
对于scala来说,以某种方式对它进行转换是非常简单的,这些转换动作不会修改原始数组,而是产生一个全新的数值,可以使用 for (elem <- a) yield 2 * elem 来返回一个类型与原始集合相同的新集合(并不会修改原始数组),当然也可以按照下面的方式,仅针对某几个过滤好的元素来操作:
 
for (elem <- a if elem % 2 == 0) yield 2 * elem
 
 
数组中也提供了一些比较常用的算法,
 
  • sum: 可以调用sum操作来获取Array其值的总和;
  • min:Array的最小值;
  • max:Array中的最大值;
  • sorted:将Array或ArrayBuffer排序并返回经过排序的Array/ArrayBuffer,不会修改原始版本;
  • sortWith:提供函数用于排序;
  • scala.util.Sorting.quickSort(数组)
  • mkString: 连接数组中的字符串,可以定义连接符,起始和结束符号;
 
scala数组是用Java数组实现的,可以在Java与Scala之间来回传递,但是可以使用更加方便的方式,引入scala.collection.JavaConversions中的隐式转换方法,这样就可以在代码中直接使用Java的集合。
 
映射和元组
 
scala中可以使用下面的方式来创建映射:
 
val scores = Map("Alice"->10, "Bob"->3, "Cindy"->4)
 
 
->操作符可以用于创建对偶,对偶 “Alice”->10, 产出的值为(“Alice”, 10)。
 
在scala中,函数与映射之间的相似性尤为明显,将使用()表示法来查找某个键对应的值,scores(“Alice”);getOrElse(key, default)来获取某个值,当key不存在时使用default值代替。
 
如果想要更新映射中的值,可以对()的值进行赋值操作:scores(“Alice”)=20,或者通过+=操作来添加多个关系,-=来移除某个键和对应的值。
 
对于映射的迭代,可以用循环来进行遍历:
 
for ((k,v) <- 映射) 处理k和v
 
 
如果需要反转一个映射,交换键和值的位置,可以使用for yield操作来简单实现:
 
for ((k,v) <- 映射) yield (v,k)
 
 
如果需要实现与Java Map的互操作,可以通过增加import scala.collection.JavaConversions.mapAsScalaMap/mapAsJavaMap来实现。
 
映射是键值对偶的集合,对偶是元组tuple的最简单形态,它只有两个元素,而元组是可以包含多个元素的,Tuple[Int, Double, java.lang.String],访问元组的方式比较简单,只需要根据 variable._1 ._2这种方式来实现。
 
使用元组的原因之一就是把多个值绑在一起,使它们能够被一起处理,这通常用zip方法来完成:
 
val symbols = Array("<", "-", ">")
val counts = Array(2, 10, 2)
val pairs = symbols.zip(counts)
 
 
//生成的pairs就会存在一个 Tuple的集合 (“>”, 2), (“-“, 10), (“>”, 2)
 
类-class
 
scala中,类并不声明为public,方法默认是public的。scala源文件中可以包含多个类,所有这些类型都具有公有可见性。在调用无参方法时,可以写上圆括号,也可以不写。关于这点的最佳实践是,对于setter类型的方法使用括号,而getter类型的方法不用使用括号。
 
在编写Java类时,我们往往不喜欢使用公有字段,scala中对每个字段都提供getter/setter方法,假如字段名为age,则getter/setter方法名称分别叫做age和age_=。scala中对每个字段生成getter/setter方法听上去比较恐怖,但我们可以控制该过程:
 
  • 如果字段是私有的,则getter/setter方法也是私有的;
  • 如果字段是val,则只有getter方法被生成(该字段对应java的final属性);
  • 如果不需要任何getter/setter,可以将字段声明为private[this];
 
在scala/Java中,方法可以访问该类的所有对象私有字段,但在scala中允许我们定义更加严格的访问限制,通过private[this]这个修饰符来实现,且该字段不会生成getter/setter方法。
 
scala提供生成的getter/setter方法针对Java的Bean规范时,可能并不是预期的,很多Java规范依赖getFoo/setFoo这种类型的命名规范。当在scala字段中标注@BeanProperty时,这样的方法就会自动生成了。
 
与Java一样,scala中也可以有任意多的构造器,不过scala中有着主构造器(primary constructor)的概念,除了主构造器之外,类还可以有任意多个辅助构造器。
 
辅助构造器的名称为this,每一个辅助构造器都必须以一个对先前已定义的其他辅助构造器或主构造器的调用开始。一个类如果没有显示地定义主构造器则自动拥有一个无参的主构造器。
 
在scala中每个类都会有主构造器,主构造器并不以this方法定义,而是与类定义交织在一起。
 
主构造器的参数直接放置在类名之后,主构造器的参数被编译成字段,其值被初始化成构造时传入的参数,主构造器会执行类定义中的所有语句:
 
class Person(var name:String, private var age:Int){
     print(“….")
}
 
 
在构造该Person对象时,会同时打印出其中的语句。
 
如果类名之后没有参数,则具备一个无参主构造器,这样一个构造器仅仅是简单地执行类体中的所有语句而已。主构造器中的参数也可以不带val/var,这取决于后续其他方法是否使用该字段,如果没有使用仅当做是局部变量,如果使用了该字段就升级为类型中字段。
 
对象
 
scala中没有静态方法或静态字段,可以使用object这个语法结构来达到同样的目的,对象定义了某个类的单个实例,对象使用object来定义而非class,可以将下面的类定义想象成静态工具类方法,用于产生JVM内部唯一的unique数字:
 
object Accounts {
  private var lastNumber = 0
 
  def newUniqueNumber() = {
    lastNumber += 1
    lastNumber
  }
}
 
 
对象在本职上可以拥有类的所有特性——甚至可以扩展其他类或者特质,但不能提供构造器函数。任何在Java中使用单例对象的地方,在scala中都可以用对象来实现:
 
  • 作为存放工具函数或常量的地方;
  • 高效地共享单个不可变实例;
  • 需要用单个实例来协调某个服务时;
 
在Java中,经常需要用到既有实例方法又有静态方法的类,在scala中,可以通过类和类同名的伴生对象来达到同样的目的,就好比Java中既存在静态方法又存在实例方法一样。类和它的伴生对象可以互相访问其私有特性,它们必须存在于同一个源文件中。
 
一个object可以扩展类以及一个或多个特质(trait),其最终结果是一个扩展了指定类以及特质的类对象,同时拥有在对象定义中给出的所有特性,此时该实例在JVM中就为同一个实例存在。
 
我们通常会定义和使用对象的apply方法,当遇到下面类型的表达式,apply方法就会被调用,通常这样的一个apply方法返回的是伴生类对象:
 
Object(参数1, 参数2,...)
 
 
对于嵌套表达式而言,这种方式省去了new关键字,会方便很多。注意,Array(100)和new Array(100)很容易混淆,前者调用的是apply方法,后者调用的是构造器。
 
每个scala程序都必须从一个对象的main方法开始,这个方法的签名类似:
 
def main(args: Array[String]) {
}
 
 
除了每次都提供main方法之外,还可以扩展app特质,将程序代码放入构造器方法体内,如果需要命令行参数,可以通过args来得到:
 
 
object Hello extends App{
     println(“Hello!")
}
 
 
如果在调用该应用程序时设置了-Dscala.time选项的话,程序在退出时会显示逝去时间。
 
和Java不同,scala并没有枚举类型,不过标准类库中提供了一个Enumeration助手类,用于产出枚举。
 
object TrafficLightColor extends Enumeration {
  val Red, Yellow, Green = Value
}
 
 
记住枚举的类型为TrafficLightColor.Value,可以使用import TrafficLightColor._来静态导入所有枚举值。
 
 
特质
 
scala和java一样不允许类从多个超类继承,因为对于多重继承来说的代价非常之高。Java采取了非常强的限制策略,类只能扩展自一个超类,可以实现任意数量的接口,但接口只能包含抽象方法,不能包含字段,所以在Java中我们经常看到同时提供接口和抽象基类的做法。
 
scala中提供特质而非接口,特质可以同时拥有抽象方法和具体方法,而类可以实现多个特质,这个设计干净利落地解决了Java接口的问题。
 
trait的定义很简单,类似Java接口:
 
trait Logger {
  def log(msg: String)
}
 
 
子类实现时使用extends而非implements:
 
class ConsoleLogger extends Logger{
  override def log(msg: String): Unit = {println(msg)}
}
 
 
在重写特质的抽象方法时不需要给出override关键字,如果需要的特质不止一个,需要用with关键字来添加额外的特质(所有Java接口都可以当做scala的特质使用)。
 
特质的方法并不一定需要是抽象的,注意如果实现具有带实现的特质时,可以说得到了一个具体的实现方法,这在Java接口中是无法做到的,也可以说被混入了。注意让特质拥有具体行为存在一个弊端,当特质改变时,所有混入了该特质的类都必须要重新编译。
 
trait可以在对象定义时才被“混入”到对象中,这无疑进一步省略了一个java class定义(相比于内部类或匿名内部类),注意被混入的trait需要有着具体方法实现,这样在SavingAccount类中就可以在运行时才决定使用的trait:
 
class SavingAccount extends Account with Logger{
  override def withdraw(amount: Double): Unit = {
    if(amount > 1000) log("Insufficient funds")
  }
}
val acct = new SavingAccount with ConsoleLogger
 
 
此外,特质还可以叠加,可以为类或对象添加多个互相调用的特质,从最后一个开始,这对于需要分阶段加工处理某个值的场景非常有用,从设计模式角度,这个非常类似于Java中的装饰者模式(Decorator,继承同样的接口,叠加不同的实现)。
 
额外增加两个trait实现:
 
trait ShortLogger extends Logger{
  val maxLength = 15
  override def log(msg: String): Unit ={
    super.log(if(msg.length <= maxLength) msg else msg.substring(0, maxLength - 3))
  }
}
trait TimestampLogger extends Logger{
  override def log(msg: String): Unit = {
    super.log(new java.util.Date() + " " + msg)
  }
}
 
 
在定义时,使用两个特质进行叠加到ConsoleLogger中,注意,我们在定义ShortLogger/TimestampLogger时,使用的都是super.log,这样就会将修改过的msg传入到ConsoleLogger中,执行的顺序取决于定义的顺序,限制性ShortLogger,再执行TimestampLogger,此种定义使用时ShortLogger执行的super.log会调用TimestampLogger,同样TimestampLogger的super.log也会调用到ConsoleLogger。
 
val acct = new SavingAccount with ConsoleLogger with TimestampLogger with ShortLogger
acct.withdraw(1001)
 
 
可以非常方便地在对象声明处修改顺序,这好比在语言层面上增加了AOP功能。
 
对特质而言,无法从源码判断super.someMethod会执行到哪里,确切的方法依赖于使用这些特质的对象或类给出的顺序,要灵活得多。如果需要控制具体哪一个特质的方法被调用,可以在括号中给出名称: super[ConsoleLogger].log(…),这里给出的类型必须是超类型,无法使用继承层次中更远的特质或类。
 
特质中可以包含大量工具方法,而这些工具方法可以依赖一些抽象方法来实现,with一个特质,在实现中大量使用特质中的方法,到对象创建时才去指定特质的具体实现。从Java的实际模式角度,这可以看成是策略模式(Strategy,可替换的算法实现)的直接语言层面实现。
 
当然,特质也会有构造器,特质的构造器以如下方式运行:
 
  1. 调用超类的构造器;
  2. 特质构造器在超类构造器之后,类构造器之前执行;
  3. 特质由左到右被构造;
  4. 每个特质当中,父特质先被构造;
  5. 如果多个特质共有一个父特质,而那个父特质已经被构造,则不会再次构造;
  6. 所有特质构造完毕,子类被构造。
 
下面的类被声明:
 
class SavingAccount extends Account with FileLogger with ShortLogger
 
 
构造器的执行顺序:1.Account(超类);2.Logger(第一个特质的父类);3.FileLogger(父类);4.ShortLogger(第二个特质,由于其超类已经被构造过,跳过);5.SavingAccount。
 
特质不能有构造器参数,每个特质都有一个无参数的构造器,缺少构造器参数是特质与类之间唯一的技术差别,除此之外特质可以具备类的所有特性。 
 
相关标签: scala