Scala基础
Scala 是 Scalable Language 的简写,是一门多范式的编程语言
联邦理工学院洛桑(EPFL)的Martin Odersky于2001年基于Funnel的工作开始设计Scala。
Funnel是把函数式编程思想和Petri网相结合的一种编程语言。
Odersky先前的工作是Generic Java和javac(Sun Java编译器)。Java平台的Scala于2003年底/2004年初发布。.NET平台的Scala发布于2004年6月。该语言第二个版本,v2.0,发布于2006年3月。
截至2009年9月,最新版本是版本2.7.6 。Scala 2.8预计的特性包括重写的Scala类库(Scala collections library)、方法的命名参数和默认参数、包对象(package object),以及Continuation。
2009年4月,Twitter宣布他们已经把大部分后端程序从Ruby迁移到Scala,其余部分也打算要迁移。此外, Wattzon已经公开宣称,其整个平台都已经是基于Scala基础设施编写的。
本文主要引用了 菜鸟教程 对于基础部分的详解,部分可能有省略,也有部分加入了自己的见解,主要是为了见证自己学习的道路~
在加入本人见解的部分,会使用 *
标明主题
在本文中,只着重讨论scala与java的不同之处~
Scala 特性
面向对象特性
Scala是一种纯面向对象的语言,每个值都是对象。对象的数据类型以及行为由类和特质描述。
类抽象机制的扩展有两种途径:一种途径是子类继承,另一种途径是灵活的混入机制。这两种途径能避免多重继承的种种问题。
函数式编程
Scala也是一种函数式语言,其函数也能当成值来使用。Scala提供了轻量级的语法用以定义匿名函数,支持高阶函数,允许嵌套多层函数,并支持柯里化。Scala的case class及其内置的模式匹配相当于函数式编程语言中常用的代数类型。
更进一步,程序员可以利用Scala的模式匹配,编写类似正则表达式的代码处理XML数据。
静态类型
Scala具备类型系统,通过编译时检查,保证代码的安全性和一致性。类型系统具体支持以下特性:
- 泛型类
- 协变和逆变
- 标注
- 类型参数的上下限约束
- 把类别和抽象类型作为对象成员
- 复合类型
- 引用自己时显式指定类型
- 视图
- 多态方法
扩展性
Scala的设计秉承一项事实,即在实践中,某个领域特定的应用程序开发往往需要特定于该领域的语言扩展。Scala提供了许多独特的语言机制,可以以库的形式轻易无缝添加新的语言结构:
- 任何方法可用作前缀或后缀操作符
- 可以根据预期类型自动构造闭包。
并发性
Scala使用Actor作为其并发模型,Actor是类似线程的实体,通过邮箱发收消息。Actor可以复用线程,因此可以在程序中可以使用数百万个Actor,而线程只能创建数千个。在2.10之后的版本中,使用Akka作为其默认Actor实现。
Scala 数据类型
数据类型*
Scala 与 Java 有着相同的数据类型,下表列出了 Scala 支持的数据类型:
数据类型 | 描述 |
---|---|
Byte | 8位有符号补码整数。数值区间为 -128 到 127 |
Short | 16位有符号补码整数。数值区间为 -32768 到 32767 |
Int | 32位有符号补码整数。数值区间为 -2147483648 到 2147483647 |
Long | 64位有符号补码整数。数值区间为 -9223372036854775808 到 9223372036854775807 |
Float | 32 位, IEEE 754 标准的单精度浮点数 |
Double | 64 位 IEEE 754 标准的双精度浮点数 |
Char | 16位无符号Unicode字符, 区间值为 U+0000 到 U+FFFF |
String | 字符序列 |
Boolean | true或false |
Unit | 表示无值,和其他语言中void等同。用作不返回任何结果的方法的结果类型。Unit只有一个实例值,写成()。 |
Null | null 或空引用 |
Nothing | Nothing类型在Scala的类层级的最底端;它是任何其他类型的子类型。 |
Any | Any是所有其他类的超类 |
AnyRef | AnyRef类是Scala里所有引用类(reference class)的基类 |
上表中列出的数据类型都是对象,也就是说scala没有java中的原生类型。在scala是可以对数字等基础类型调用方法的。
例如
// Int对象的.+方法
val data1 = 1.+(1)
println(data1)
// 相当于运算符重载 两个Int对象相加
val data2 = 1 + 1
println(data2)
结果
2
2
进程已结束,退出代码0
多行字符串的表示方法
多行字符串用三个双引号来表示分隔符,格式为:**””” … “””**。
实例如下:
val foo = """
www.kanaikee.com
"""
Scala 基础语法
在学之前,需要明白scala中几个重要特性
变量定义*
使用关键词 var
声明变量,使用关键词 val
声明常量。
// val 定义不可变对象 常量
val immutable = 0
// var 定义可变对象 变量
var variable = 0
scala编译器会自动推断变量类型,但是,如果没有赋初值,那么编译器就无法判断数据类型,因此会报错
所以,如果在没有指明数据类型的情况下声明变量或常量必须要给出其初始值,否则将会报错。
函数定义*
// 主函数定义
def main(args: Array[String]): Unit = {}
// 普通函数定义
def fun(num: Int): Int = {
num + 1
}
如上所示,scala函数并无return
就可自动返回最后一行代码所对应的对象
上面普通函数定义可以简化为如下所示
def fun(num: Int) = num + 1
即函数返回值类型及 {}
都可省略,此处也说明了scala一切皆对象
val function0: Int = fun(1)
def fun(x: Int): Int = x + 1
包定义
Scala 使用 package
关键字定义包,在Scala将代码定义到某个包中有两种方式:
第一种方法和 Java 一样,在文件的头定义包名,这种方法就后续所有代码都放在该包中。 比如:
package com.kanaikee
class HelloWorld
第二种方法有些类似 C#,如:
package com.kenaikee {
class HelloWorld
}
第二种方法,可以在一个文件中定义多个包。
Null 值
空值是 scala.Null
类型。
scala.Null
和scala.Nothing
是用统一的方式处理Scala面向对象类型系统的某些**”边界情况”**的特殊类型。
Null
类是null引用对象的类型,它是每个引用类(继承自AnyRef
的类)的子类。Null不兼容值类型。
Scala 访问修饰符
Scala 访问修饰符基本和Java的一样,分别有:private
,protected
,public
。
如果没有指定访问修饰符,默认情况下,Scala 对象的访问级别都是 public
。
Scala 中的 private
限定符,比 Java 更严格,在嵌套类情况下,外层类甚至不能访问被嵌套类的私有成员。
私有(Private)成员
用 private 关键字修饰,带有此标记的成员仅在包含了成员定义的类或对象内部可见,同样的规则还适用内部类。
class Outer{
class Inner{
private def printf(){
println("f")
}
class InnerMost{
printf() // 正确
}
}
(new Inner).printf() //错误
}
保护(Protected)成员
在 scala 中,对保护(Protected)成员的访问比 java 更严格一些。因为它只允许保护成员在定义了该成员的的类的子类中被访问。而在java中,用 protected关键字修饰的成员,除了定义了该成员的类的子类可以访问,同一个包里的其他类也可以进行访问。
package com.kanaikee
class Super {
protected def printf() {
println("f")
}
}
class Sub extends Super {
printf()
}
class Other {
(new Super).printf() //错误
}
公共(Public)成员
Scala 中,如果没有指定任何的修饰符,则默认为 public。这样的成员在任何地方都可以被访问。
class Outer {
class Inner {
def printf() { println("f") }
class InnerMost {
printf() // 正确
}
}
(new Inner).printf() // 正确因为 f() 是 public
}
作用域保护
Scala中,访问修饰符可以通过使用限定词强调。格式为:
private[x]
// 或
protected[x]
这里的x指代某个所属的包、类或单例对象。如果写成private[x],读作”这个成员除了对[…]中的类或[…]中的包中的类及它们的伴生对像可见外,对其它所有类都是private。
这种技巧在横跨了若干包的大型项目中非常有用,它允许你定义一些在你项目的若干子包中可见但对于项目外部的客户却始终不可见的东西。
package com{
package scala{
private[com] class Navigator{
protected[scala] def useStarChart(){}
class LegOfJourney{
private[Navigator] val distance = 100
}
private[this] var speed = 200
}
}
package kanaikee{
import scala._
object Vehicle{
private[kanaikee] val guide = new Navigator
}
}
}
上述例子中,类 Navigator 被标记为 private[bobsrockets] 就是说这个类对包含在 bobsrockets 包里的所有的类和对象可见。
比如说,从 Vehicle 对象里对 Navigator 的访问是被允许的,因为对象 Vehicle 包含在包 launch 中,而 launch 包在 bobsrockets 中,相反,所有在包 bobsrockets 之外的代码都不能访问类 Navigator。
Scala 运算符*
运算符在scala中其实也是一种对象
val data1 = 1.+(1)
val data2 = 1 + 1
// data1 == data2 == 2
val data3 = 1.>(2)
val data4 = 1 > 2
// data3 == data4 == false
scala的运算符操作其实类似于运算符重载,scala底层是在每个类型的底层封装了这些方法,使得可以像函数调用那样调用运算符
/** Returns the sum of this value and `x`. */
def +(x: Byte): Int
/** Returns the sum of this value and `x`. */
def +(x: Short): Int
/** Returns the sum of this value and `x`. */
def +(x: Char): Int
/** Returns the sum of this value and `x`. */
def +(x: Int): Int
/** Returns the sum of this value and `x`. */
def +(x: Long): Long
/** Returns the sum of this value and `x`. */
def +(x: Float): Float
上面为
Int
类型里面所封装的一部分+
函数而 Scala 一切皆对象,那么函数自然为对象,那么运算符函数自然也是一种对象
Scala 方法和函数
概念介绍*
在上文可知, Scala 中,函数的命名是以 def
开始,但是 val
同样能定义函数,只是前者应该叫方法,后者应该叫函数,但是在日常开发中,不必拘泥与起叫法,知道其区别及用法就够了
val fun1 = (a: Int, b: Int) => a + b
// 打印出的是此lambda表达式在编译过后的类型名称
println(fun1)
println(fun1(1, 1))
def fun2(a: Int, b: Int) = a + b
println(fun2(1, 1))
执行以上代码,输出结果为:
com.scala.kanaikee.Test$$$Lambda$1/12209492@65e2dbf3
2
2
进程已结束,退出代码0
由此可看出,函数和方法二者在语义上的区别很小。
- 方法是类的一部分,而函数是一个对象可以赋值给一个变量。换句话来说在类中定义的函数即是方法。
- 方法跟 Java 的类似,方法是组成类的一部分。
- 函数是一个完整的对象,Scala 中的函数其实就是继承了 Trait 的类的对象。
- 使用 val 语句可以定义函数,def 语句定义方法。
scala函数还有其他一些特性
传值调用和传名调用*
- 传名参数 call-by-name
- 传值参数 call-by-value
object Test {
var count = 0
def main(args: Array[String]): Unit = {
delayed1(time())
println()
count = 0
delayed2(time())
}
def time(): Long = {
count = count + 1
println("time() 方法 第 " + count + " 调用")
System.nanoTime
}
/**
* 传名调用
*
* @param t
* @return
*/
def delayed1( t: => Long ): Long = {
println("在 delayed1() 方法内")
println("参数: " + t)
t
}
/**
* 传值调用
*
* @param t
* @return
*/
def delayed2( t: Long ): Long = {
println("在 delayed2() 方法内")
println("参数: " + t)
t
}
}
执行以上代码,输出结果为:
在 delayed1() 方法内
time() 方法 第 1 调用
参数: 547506285523100
time() 方法 第 2 调用
time() 方法 第 1 调用
在 delayed2() 方法内
参数: 547506285589200
进程已结束,退出代码0
由此可以看出,传值调用是在用到函数的地方只有一次计算,但是传名调用是在用到传入值的时候,才会计算相应的函数,并且不会保存计算结果,下次用到时还会重新计算,所以传入参数就变得不是那么固定,因为每次都会重新计算一遍
指定函数参数名
一般情况下函数调用参数,就按照函数定义时的参数顺序一个个传递。但是我们也可以通过指定函数参数名,并且不需要按照顺序向函数传递参数,实例如下
object Test {
def main(args: Array[String]) {
printInt(b=5, a=7);
}
def printInt( a:Int, b:Int ) = {
println("Value of a : " + a );
println("Value of b : " + b );
}
}
执行以上代码,输出结果为:
Value of a : 7
Value of b : 5
可变参数
Scala 允许你指明函数的最后一个参数可以是重复的,即我们不需要指定函数参数的个数,可以向函数传入可变长度参数列表。
Scala 通过在参数的类型之后放一个星号来设置可变参数(可重复的参数)。
这点 Scala 和 Java 类似
例如:
object Test {
def main(args: Array[String]) {
printStrings("Runoob", "Scala", "Python");
}
def printStrings( args:String* ) = {
var i : Int = 0;
for( arg <- args ){
println("Arg value[" + i + "] = " + arg );
i = i + 1;
}
}
}
执行以上代码,输出结果为:
Arg value[0] = Runoob
Arg value[1] = Scala
Arg value[2] = Python
默认参数值
Scala 可以为函数参数指定默认参数值,使用了默认参数,你在调用函数的过程中可以不需要传递参数,这时函数就会调用它的默认参数值,如果传递了参数,则传递值会取代默认值。实例如下:
object Test {
def main(args: Array[String]) {
println( "返回值 : " + addInt() );
}
def addInt( a:Int=5, b:Int=7 ) : Int = {
var sum:Int = 0
sum = a + b
return sum
}
}
执行以上代码,输出结果为:
返回值 : 12
高阶函数
高阶函数是指使用其他函数作为参数、或者返回一个函数作为结果的函数。在Scala中函数是“一等公民”,所以允许定义高阶函数。这里的术语可能有点让人困惑,我们约定,使用函数值作为参数,或者返回值为函数值的“函数”和“方法”,均称之为“高阶函数”。
高阶函数(Higher-Order Function)就是操作其他函数的函数。
Scala 中允许使用高阶函数, 高阶函数可以使用其他函数作为参数,或者使用函数作为输出结果。
以下实例中,apply() 函数使用了另外一个函数 f 和 值 v 作为参数,而函数 f 又调用了参数 v:
object Test {
def main(args: Array[String]): Unit = {
println(apply(layout, 10))
}
// 函数 f 和 值 v 作为参数,而函数 f 又调用了参数 v
def apply(f: Int => String, v: Int): String = f(v)
def layout[A](x: A): String = "[" + x + "]"
}
执行以上代码,输出结果为:
[10]
高阶函数只在此提及一部分概念,后面会专门讲到scala高阶函数的使用技巧
匿名函数
Scala 中定义匿名函数的语法很简单,箭头左边是参数列表,右边是函数体。
使用匿名函数后,我们的代码变得更简洁了。
下面的表达式就定义了一个接受一个Int类型输入参数的匿名函数:
var inc = (x:Int) => x+1
上述定义的匿名函数,其实是下面这种写法的简写:
def add2 = new Function1[Int,Int]{
def apply(x:Int):Int = x+1;
}
以上实例的 inc 现在可作为一个函数,使用方式如下:
var x = inc(7)-1
同样我们可以在匿名函数中定义多个参数:
var mul = (x: Int, y: Int) => x*y
mul 现在可作为一个函数,使用方式如下:
println(mul(3, 4))
我们也可以不给匿名函数设置参数,如下所示:
var userDir = () => { System.getProperty("user.dir") }
userDir 现在可作为一个函数,使用方式如下:
println( userDir() )
实例
object Demo {
def main(args: Array[String]) {
println( "multiplier(1) value = " + multiplier(1) )
println( "multiplier(2) value = " + multiplier(2) )
}
var factor = 3
val multiplier = (i:Int) => i * factor
}
输出结果为:
multiplier(1) value = 3
multiplier(2) value = 6
函数嵌套
我们可以在 Scala 函数内定义函数,定义在函数内的函数称之为局部函数。
以下实例我们实现阶乘运算,并使用内嵌函数:
object Test {
def main(args: Array[String]) {
println( factorial(0) )
println( factorial(1) )
println( factorial(2) )
println( factorial(3) )
}
def factorial(i: Int): Int = {
def fact(i: Int, accumulator: Int): Int = {
if (i <= 1)
accumulator
else
fact(i - 1, i * accumulator)
}
fact(i, 1)
}
}
执行以上代码,输出结果为:
1
1
2
6
偏应用函数
Scala 偏应用函数是一种表达式,你不需要提供函数需要的所有参数,只需要提供部分,或不提供所需参数。
如下实例,我们打印日志信息:
import java.util.Date
object Test {
def main(args: Array[String]) {
val date = new Date
log(date, "message1" )
Thread.sleep(1000)
log(date, "message2" )
Thread.sleep(1000)
log(date, "message3" )
}
def log(date: Date, message: String) = {
println(date + "----" + message)
}
}
执行以上代码,输出结果为:
Thu Aug 25 18:59:44 CST 2022----message1
Thu Aug 25 18:59:44 CST 2022----message2
Thu Aug 25 18:59:44 CST 2022----message3
实例中,log() 方法接收两个参数:date 和 message。我们在程序执行时调用了三次,参数 date 值都相同,message 不同。
我们可以使用偏应用函数优化以上方法,绑定第一个 date 参数,第二个参数使用下划线(_)替换缺失的参数列表,并把这个新的函数值的索引的赋给变量。以上实例修改如下:
import java.util.Date
object Test {
def main(args: Array[String]) {
val date = new Date
val logWithDateBound = log(date, _ : String)
logWithDateBound("message1" )
Thread.sleep(1000)
logWithDateBound("message2" )
Thread.sleep(1000)
logWithDateBound("message3" )
}
def log(date: Date, message: String) = {
println(date + "----" + message)
}
}
执行以上代码,输出结果为:
Thu Aug 25 18:59:45 CST 2022----message1
Thu Aug 25 18:59:45 CST 2022----message2
Thu Aug 25 18:59:45 CST 2022----message3
函数柯里化(Currying)
柯里化(Currying)指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有第二个参数为参数的函数。
实例
首先我们定义一个函数:
def add(x:Int,y:Int)=x+y
那么我们应用的时候,应该是这样用:add(1,2)
现在我们把这个函数变一下形:
def add(x:Int)(y:Int) = x + y
那么我们应用的时候,应该是这样用:add(1)(2),最后结果都一样是3,这种方式(过程)就叫柯里化。
实现过程
add(1)(2) 实际上是依次调用两个普通函数(非柯里化函数),第一次调用使用一个参数 x,返回一个函数类型的值,第二次使用参数y调用这个函数类型的值。
实质上最先演变成这样一个方法:
def add(x:Int)=(y:Int)=>x+y
那么这个函数是什么意思呢? 接收一个x为参数,返回一个匿名函数,该匿名函数的定义是:接收一个Int型参数y,函数体为x+y。现在我们来对这个方法进行调用。
val result = add(1)
返回一个result,那result的值应该是一个匿名函数:(y:Int)=>1+y
所以为了得到结果,我们继续调用result。
val sum = result(2)
最后打印出来的结果就是3。
完整实例
下面是一个完整实例:
object Test {
def main(args: Array[String]) {
val str1:String = "Hello, "
val str2:String = "Scala!"
println( "str1 + str2 = " + strcat(str1)(str2) )
}
def strCat(s1: String)(s2: String): String = {
s1 + s2
}
}
执行以上代码,输出结果为:
str1 + str2 = Hello, Scala!
上面strCat()()
函数也可合成一个完整函数
def strCat1(s1: String, s2: String): String = strCat(s1)(s2)