序言
这是一篇个人在系统学习Scala语言时的笔记整理,适合有Java基础的同学看,其中针对与Java语言的不同之处做了对比,方便记忆区分,充分理解Scala语言的编程特性。
有很多一笔带过的地方,如仍有疑惑,相信聪明的你一定能在别处寻得答案。
如有不实之处,欢迎留言指正~~
文章目录
Scala语言基础
变量类型
var 可变
val 不可变,等同于加上 final
数据类型体系
数据类型继承结构图
- 在 scala 中,所有其他类都是 AnyRef 的子类,类似 Java 的 Object;
- AnyVal 和 AnyRef 都扩展自 Any 类。Any 类是根节点;
- Any 中定义了 isInstanceOf、asInstanceOf 方法,以及哈希方法等;
- Null 类型的唯一实例就是 null 对象。可以将 null 赋值给任何引用,但不能赋值给值类型的变量;
- Nothing 类型没有实例。它对于泛型结构是有用处的,举例:空列表 Nil 的类型是 List[Nothing],它是 List[T]的子类型,T 可以是任何类。
基本数据类型的转换
123+“ ” 数字转换为字符串
其他基本类型转换toXxx,“123“.toInt
强制类型转换细节:
- 当进行数据的 从 大——>小,就需要使用到强制转换;
- 强转符号只针对于最近的操作数有效,往往会使用小括号提升优先级;
- Char 类型可以保存 Int 的常量值,但不能保存 Int 的变量值,需要强转;
- Byte 和 Short 类型在进行运算时,当做 Int 类型处理。
数据类型列表
定义数据类型
var num :Int = 123
var age: Int = _ // _ 表示给 age 一个默认的值 ,如果 Int 默认就是 0
数据默认值
类型 | _对应的默认值 |
---|---|
Byte/Short/Int/Long | 0 |
Float/Double | 0.0 |
String/AnyRef(引用类型) | null |
Boolean | false |
Scala 不支持三目运算符
val num = if (5>4) 5 else 4
键盘输入
import scala.io.StdIn
val name = StdIn.readLine()
循环控制
for循环
for(i <- 1 to 3) 前后闭合
for(i <- 1 until 3) 前闭后开
for(i <- 1 to 3; j = 4 - i) 引入变量 j
for(i <- 1 to 3 if i != 2) 循环守卫,if条件为true才会进入循环,类似continue
for (i <- 1 to 10 by 2) 通过by关键字控制步长
for (i <- 1 to 10 if i % 2 == 1) 通过循环守卫控制步长
for(i <- 1 to 3; j <- 1 to 3) 循环嵌套
for(i <- 1 to 10) yield i 将遍历过程中处理的结果返回到一个新 Vector 集合中,使用 yield 关键字
增强for循环
for(i <- arr) println(i)
(1 to 10).foreach(println(_))
while循环
与java一致
while (循环条件) {
循环体(语句)
循环变量迭代
}
do…while
var i = 0
do {
printf(i + "hello\n" )
i += 1
} while (i < 10)
然而没什么用~~
break与continue
Scala 内置控制结构特地去掉了 break 和 continue,是为了更好的适应函数化编程,推荐使用函数式的风格解决 break 和 contine 的功能,而不是一个关键字。
breakable 是一个高阶函数:可以接收函数的函数就是高阶函数;
breakable 对 break()抛出的异常做了处理,代码就继续执行
Scala 内置控制结构特地也去掉了 continue,可以使用 if – else 或是循环守卫实现 continue 的效果。
Scala函数
函数定义
基本语法
def 函数名 ([参数名: 参数类型], ...)[[: 返回值类型] =] {
语句...
return 返回值
}
-
函数声明关键字为 def (definition)
-
[参数名: 参数类型], …:表示函数的输入(就是参数列表), 可以没有。 如果有,多个参数使用逗号间隔
-
函数中的语句:表示为了实现某一功能代码块
-
函数可以有返回值,也可以没有
返回值形式 意义 def func(name: String): Int = { } 完整格式,定义了返回值具体类型 def func(name: String) = { } 表示返回值类型不确定,使用类型推导完成 def func(name: String) { } 表示没有返回值,return 不生效 -
如果没有 return ,默认以执行到最后一行的结果作为返回值
-
Unit等价与Java中的void,Any为任意类型
函数细节
-
函数的形参列表可以是多个, 如果函数没有形参,调用时 可以不带()
-
形参列表和返回值列表的数据类型可以是值类型和引用类型
-
Scala 中的函数可以根据函数体最后一行代码自行推断函数返回值类型。那么在这种情况下,return 关键字可以省略
-
因为 Scala 可以自行推断,所以在省略 return 关键字的场合,返回值类型也可以省略
-
如果函数明确使用 return 关键字,那么函数返回就不能使用自行推断了,这时要明确写成 : 返
回类型 = ,当然如果你什么都不写,即使有 return 返回值为() -
如果函数明确声明无返回值(声明 Unit),那么函数体中即使使用 return 关键字也不会有返回
值 -
如果明确函数无返回值或不确定返回值类型,那么返回值类型可以省略或声明为 Any
-
Scala 语法中任何的语法结构都可以嵌套其他语法结构(灵活),即:函数中可以再声明/定义函数,
类中可以再声明类 ,方法中可以再声明/定义方法 -
Scala 函数的形参,在声明参数时,直接赋初始值(默认值),这时调用函数时,如果没有指定实
参,则会使用默认值。如果指定了实参,则实参会覆盖默认值。 -
如果函数存在多个参数,每一个参数都可以设定默认值,那么这个时候,传递的参数到底是覆
盖默认值,还是赋值给没有默认值的参数,就不确定了(默认按照声明顺序[从左到右])。在这种情况下,
可以采用带名参数
注意区分: 如果函数声明时没有返回值类型,但是有 = 号,可以进行类型推断最后一行代码。
这时这个函数实际是有返回值的,该函数并不是过程。
惰性函数
lazy val res = sum(10, 20)
lazy 不能修饰 var 类型的变量
在调用函数时,加了 lazy ,会导致函数的执行被推迟
我们在声明一个lazy变量时,变量值得分配也会被推迟。
异常
try catch 结构
try {
val r = 10 / 0
} catch {
//1. 在 scala 中只有一个 catch
//2. 在 catch 中有多个 case, 每个 case 可以匹配一种异常 case ex: ArithmeticException
//3. => 关键符号,表示后面是对该异常的处理代码块
//4. finally 最终要执行的
case ex: ArithmeticException=> {
println("捕获了除数为零的算数异常")
}
case ex: Exception => println("捕获了异常")
} finally {
println("scala finally...")
}
声明异常
在 scala 中,可以使用方法声明异常,也可以使用 throws 注释来声明异常,它向调用者函数提供了此方法可能引发此异常的信息。
方法声明
def test(): Nothing = {
throw new ArithmeticException("算术异常")//Exception("异常 NO1 出现~")
}
注释声明
def main(args: Array[String]): Unit = {
func()
}
@throws(classOf[NumberFormatException])//等同于 NumberFormatException.class
def func() = {
"abc".toInt
}
总结:
在 catch子句中,越具体的异常越要靠前,越普遍的异常越靠后,如果把越普遍的异常写在前,把具体的异常写在后,在 scala 中也不会报错,但这样是非常不好的编程风格。
面向对象
Java 是面向对象的编程语言,由于历史原因,Java 中还存在着非面向对象的内容:基本类型 ,null,静态方法等。
Scala 语言来自于 Java,所以天生就是面向对象的语言,而且 Scala 是纯粹的面向对象的语言,即在 Scala 中,一切皆为对象。
类
[修饰符] class 类名 {
类体
}
Scala 语法中,类并不声明为 public,所有这些类都具有公有可见性(即默认就是 public)
一个 Scala 源文件可以包含多个类.,而且默认都是 public
属性
属性是类的一个组成部分,一般是值数据类型,也可是引用类型。
[访问修饰符] var 属性名称 [:类型] = 属性值
class A{
var name: String //抽象属性
}
细节:
Scala 中声明一个属性,必须显示的初始化,然后根据初始化数据的类型自动推断,属性类型可以省略(这点和 Java 不同);
如果赋值为 null,则一定要加类型,因为不加类型, 那么该属性的类型就是 Null 类型;
如果在定义属性时,暂时不赋值,也可以使用符号_(下划线),让系统分配默认值;
对象
val | var 对象名 [:类型] = new 类型()
创建细节
- 如果我们不希望改变对象的引用(即:内存地址), 应该声明为 val 性质的,否则声明为 var, scala设计者推荐使用 val ,因为一般来说,在程序中,我们只是改变对象属性的值,而不是改变对象的引用;
- scala 在声明对象变量时,可以根据创建对象的类型自动推断,所以类型声明可以省略,但当类型和后面 new 对象类型有继承关系,即多态时,就必须写类型声明;
构造器
和 Java 一样,Scala 构造对象也需要调用构造方法,并且可以有任意多个构造方法(即 scala 中构
造器也支持重载),构造器没有返回值。
Scala 类的构造器包括: 主构造器 和 辅助构造器
class 类名(形参列表) { // 主构造器
// 类体
def this(形参列表) { // 辅助构造器
//辅助构造器,必须在第一行显式调用主构造器(可以是直接,也可以是间接)
//再完成赋值
}
def this(形参列表) { //辅助构造器可以有多个...
//调用主构造器
//完成赋值
}
}
如果主构造器无参数,小括号可省略,构建对象时调用的构造方法的小括号也可以省略
class A{}
val a = new A
val b = new A()
Demo
class Person() { //主构造器
var name: String = _
var age: Int = _
//辅助构造器1
def this(name : String) {
this() //直接调用主构造器
this.name = name //完成赋值
}
//辅助构造器2
def this(name : String,age : Int) {
this("匿名") //调用辅助构造器1间接调用了主构造器!
this.name = name
this.age = age
}
}
构造器私有化
class Person private() {}
构造器的修饰符
- Scala 类的主构造器的形参未用任何修饰符修饰,那么这个参数是局部变量;
object Demo {
def main(args: Array[String]): Unit = {
val worker = new Worker("smith")
worker.name //不能访问 inName
}
}
class Worker(inName: String) {
var name = inName
}
- 如果参数使用 val 关键字声明,那么 Scala 会将参数作为类的私有的只读属性使用;
object Demo {
def main(args: Array[String]): Unit = {
val worker2 = new Worker2("smith2")
worker2.inName //可以访问 inName
}
}
class Worker2(val inName: String) {
var name = inName
}
- 如果参数使用 var 关键字声明,那么那么 Scala 会将参数作为类的成员属性使用,并会提供属性对应的 xxx()[类似 getter]/xxx_$eq()[类似 setter]方法,即这时的成员属性是私有的,但是可读写;
object Demo {
def main(args: Array[String]): Unit = {
val worker3 = new Worker3("jack")
worker3.inName = "mary"
println(worker3.inName)
}
}
class Worker3(var inName: String) {
var name = inName
}
Bean属性
将 Scala 字段加@BeanProperty 时,这样会自动生成规范的 setXxx/getXxx 方法。这时可以使用 对象.setXxx() 和 对象.getXxx() 来调用属性。
class Car {
import scala.beans.BeanProperty
@BeanProperty var name: String = null
}
对象创建流程
- 加载类的信息(属性信息,方法信息)
- 在内存中(堆)开辟空间
- 使用父类的构造器(主和辅助)进行初始化
- 使用主构造器对属性进行初始化
- 使用辅助构造器对属性进行初始化
- 将开辟的对象的地址赋给这个对象引用
包与对象
包
命名只能包含数字、字母、下划线、小圆点.,但不能用数字开头, 也不要使用关键字。一般是小写字母+小圆点。
自动引入常用的包java.lang._ scala._ Predef._
package com.wen { //创建包 com.wen
package scala { //创建包 com.wen.scala
class Person { // 表示在 com.wen.scala 下创建类 Person
val name = "Nick"
def play(message: String): Unit = {
println(this.name + " " + message)
}
}
}
}
作用域原则:
Scala 中子包可以直接访问父包中的内容, 大括号体现作用域;
父包要访问子包的内容时,需要 import 对应的类。
包对象
scala 提供了包对象的概念来解决了在包下不能定义变量与函数的问题。
特点
- 每一个包都可以有一个包对象,你需要在父包中定义它;
- 包对象的名字需要和子包一样;
- 在包对象中定义的变量和方法,就可以在对应的包中使用;
- 在底层这个包对象会生成两个类 package.class 和 package$.class;
package com.wen { //创建包 com.wen
package object scala {
var name = "king"
def sayHi(): Unit = {
println("package object scala sayHI~")
}
}
package scala { //创建包 com.wen.scala
class Person { // 表示在 com.wen.scala 下创建类 Person
def play(): Unit = {
println("name=" + name)
sayHi()
}
}
}
}
访问权限
关键字 | 访问权限 |
---|---|
private | 私有权限,只在类的内部和伴生对象中可用 |
protected | 受保护权限,scala 中受保护权限比 Java 中更严格,只能子类访问,同包无法访问 |
[public] | scala 中没有 public 关键字,即不能用 public 显式的修饰属性和方法 |
类型 | 默认情况 | 访问权限 |
---|---|---|
属性 | 从底层看默认属性是 private 修饰的 | 因为提供了 xxx_$eq()[类似setter]/xxx()[类似 getter] 方法,因此从使用效果看是任何地方都可以访问 |
方法 | 默认为 public 访问权限 |
包的访问权限
class Person{
private[scala] val name = "wen"
}
//1. 在该类当中,name属性仍然是private修饰的
//2. 同时在cn.wen.scala包下页可以使用这个变量
//3. private同时修饰了scala包与name属性
导包
- 在 Scala 中,import 语句可以出现在任何地方,并不仅限于文件顶部,import 语句的作用一直延伸到包含该语句的块末尾。这种语法的好处是:在需要时在引入包,缩小 import 包的作用范围,提高效率。
- Java 中如果想要导入包中所有的类,可以通过通配符*,Scala 中采用下 _
- 如果不想要某个包中全部的类,而是其中的几个类,可以采用选取器(大括号)
- 如果引入的多个包中含有相同的类,那么可以将不需要的类进行重命名进行区分,这个就是重命名
- 如果某个冲突的类根本就不会用到,那么这个类可以直接隐藏掉
import cn.wen._ //_表示导入cn.wen包下的所有类
import scala.collection.mutable.{HashMap, HashSet} //{}选择要导入的类
import java.util.{ HashMap=>JavaHashMap, List} //重命名HashMap类为JavaHashMap
import java.util.{ HashMap=>_, _} //导入util下除开HashMap的所有类
面向对象特性
封装
Scala的封装特性与Java一致,这里看一下权限不同的特点:
- 当声明属性 var 时,本身就自动提供了对应 setter/getter 方法;
- 如果属性声明权限为 private 的,那么自动生成的 setter/getter 方法也是 private 的;
- 如果属性省略访问权限修饰符,那么自动生成的 setter/getter 方法是 public 的;
- 因此我们如果只是对一个属性进行简单的 set 和 get ,只要声明一下该属性(属性使用默认访问修饰符) 不用写专门的 getset,默认会创建,访问时,直接对象.变量。这样也是为了保持访问一致性。
继承
Scala 继承的基本语法
class 子类名 extends 父类名{
类体
}
在 scala 中,子类继承了父类的所有属性,但是 private 的属性和方法无法访问。
方法重写
class Emp extends Person {
//这里需要显式的使用 override
override def printName() {
println("Emp printName() " + name)
//在子类中需要去调用父类的方法,使用 super
super.printName()
}
}
类型检查与转换
classOf[String] 获取对象的类名 String.class 。
obj.isInstanceOf[T] 判断某个对象是否属于某个给定的类,判断 obj 是不是 T 类型。
obj.asInstanceOf[T] 就如同 Java 的(T)obj 将 obj 强转成 T 类型。
object TypeConvert {
def main(args: Array[String]): Unit = {
println(classOf[String]) //ClassOf 的使用,可以得到类名
val s = "king"
println(s.getClass.getName) //使用反射机制获取类名
//isInstanceOf asInstanceOf
var a = new A
var b = new b
a = b //将子类引用给父类(向上转型,自动)
var b2 = a.asInstanceOf[B] //将父类的引用重新转成子类引用(多态),即向下转型
}
}
class A{ }
class B extends A{ }
Scala中的超类
Scala 中只有主构造器可以调用父类的构造器。辅助构造器不能直接调用父类的构造器。在 Scala 的构造
器中,你不能调用 super(params)
class Person(name: String){ //父类主构造器
}
class Emp(name: String) extends Person(name) { //将子类参数传递给父类构造器,写法正确
//super(name) //scala中没有这种写法
def this() {
super("wen") //不能再辅助构造器中调用父类的构造器
}
}
重写字段
在 Java 中只有方法的重写,没有属性/字段的重写,准确的讲,是隐藏字段代替了重写。
JVM的动态绑定机制:
- 如果调用的是方法,则 Jvm 机会将该方法和对象的内存地址绑定;
- 如果调用的是属性,则没有动态绑定机制,在哪里调用,就返回对应值;
官方文档这样写:
在一个类中,子类中的成员变量如果和父类中的成员变量同名,那么即使他们类型不一样,只要名字一样。父类中的成员变量都会被隐藏。
覆写字段的注意事项和细节
- def 只能重写另一个 def(即:方法只能重写另一个方法)
- val 只能重写另一个 val 属性 或 重写不带参数的 def
- var 只能重写另一个抽象的 var 属性
abstract class A {
var name: String // 抽象属性,所在类必须为抽象类
val age: Int = 10 // 会生成 public age()
def func(): Int = {
return 200
}
def func1(name: String){ }
}
class B extends A {
override var name: String = "wen" //var重写抽象var,可以不写override
override val age: Int = 20 // val重写val
override val func: Int = 500 // val重写不带参数的def
override def func1(name: String){ } // def重写def
}
var 重写抽象的 var 属性小结
- 一个属性没有初始化,那么这个属性就是抽象属性
- 抽象属性在编译成字节码文件时,属性并不会声明,但是会自动生成抽象方法,所以类必须声明为抽象类
- 如果是覆写一个父类的抽象属性,那么 override 关键字可省略 [ 原因:父类的抽象属性,生成的是抽象方法,因此就不涉及到方法重写的概念,因此 override 可省略 ]
抽象类
抽象类的规范:
- 在 Scala 中,通过 abstract 关键字标记不能被实例化的类;
- 方法不用标记 abstract,只要省掉方法体即可;
- 抽象类可以拥有抽象字段,抽象字段/属性就是没有初始值的字段;
使用细节:
-
默认情况下,抽象类不能被实例化,但是可以在实例化时,动态实现抽象类中所有的抽象属性 / 方法;
val animal = new Animal { override def sayHello(): Unit = { println("say hello~~~~") } }
-
抽象类不一定要包含 abstract 方法。也就是说,抽象类可以没有 abstract 方法;
-
抽象类中可以有实现的方法;
-
一旦类包含了抽象方法或者抽象属性,则这个类必须声明为 abstract;
-
抽象方法不能有主体,不允许使用 abstract 修饰;
-
如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法和抽象属性,除非它自己也声明为 abstract 类;
-
抽象方法和抽象属性不能使用 private、final 来修饰,因为这些关键字都是和重写/实现相违背的;
-
子类重写抽象方法不需要 override,写上也不会错;
匿名类
Java 中的匿名子类
Baby baby = new Baby() {
@Override
public void cry() {
System.out.println("cry...");
}
};
Scala 中的匿名子类
val baby = new Baby {
override def cry(): Unit = {
println("cry...")
}
override var name: String = _ //重写属性,赋默认值
}
伴生对象
Scala 中静态的概念 —— 伴生对象
Scala 语言是完全面向对象(万物皆对象)的语言,所以并没有静态的操作(即在 Scala 中没有静态的概念)。但是为了能够和 Java 语言交互(因为 Java 中有静态概念),就产生了一种特殊的对象来模拟类对象,我们称之为类的伴生对象。这个类的所有静态内容都可以放置在它的伴生对象中声明和调用。
使用说明:
当在同一个文件中,有 class ScalaPerson 和 object ScalaPerson(前提条件)
class ScalaPerson 称为伴生类,将非静态的内容写到该类中
object ScalaPerson 称为伴生对象,将静态的内容写入到该对象(类)
class ScalaPerson 编译后底层生成 ScalaPerson 类 ScalaPerson.class
object ScalaPerson 编译后底层生成 ScalaPerson 类 S c a l a P e r s o n 类 ScalaPerson 类ScalaPerson.class
对于伴生对象的内容,我们可以直接通过 ScalaPerson . 属性或者 . 方法
object AccompanyObject {
def main(args: Array[String]): Unit = {
println(ScalaPerson.sex) //true 在底层等价于 ScalaPerson$.MODULE$.sex()
ScalaPerson.sayHi() //在底层等价于 ScalaPerson$.MODULE$.sayHi()
}
}
//伴生类
class ScalaPerson {
var name : String = _
}
//伴生对象
object ScalaPerson {
var sex : Boolean = true
def sayHi(): Unit = {
println("object ScalaPerson sayHI~~")
}
}
伴生对象的小结
- Scala 中伴生对象采用 object 关键字声明,伴生对象中声明的全是 "静态"内容,可以通过伴生对象名称直接调用;
- 伴生对象对应的类称之为伴生类,伴生对象的名称应该和伴生类名一致;
- 伴生对象中的属性和方法都可以通过伴生对象名(类名)直接调用访问;
- 从语法角度来讲,所谓的伴生对象其实就是类的静态方法和成员的集合;
- 从技术角度来讲,scala 还是没有生成静态的内容,只不过是将伴生对象生成了一个新的类,实现属性和方法的调用;
- 从底层原理看,伴生对象实现静态特性是依赖于 public static final MODULE$ 实现的;
- 伴生对象的声明应该和伴生类的声明在同一个源码文件中,如果不在同一个文件中会运行报错,但是如果没有伴生类,也就没有所谓的伴生对象了,所以放在哪里就无所谓了;
- 如果 class A 独立存在,那么 A 就是一个类, 如果 object A 独立存在,那么 A 就是一个"静态"性质的对象[ 即类对象 ], 在 object A 中声明的属性和方法可以通过 A.属性 和 A.方法 来实现调用;
- 当一个文件中,存在伴生类和伴生对象时,文件的图标会发生变化。
伴生对象的apply方法
class P(name:String) {
var name: String = name
}
object P {
def apply(name: String): P = new P(name)
def apply(): P = new P("匿名")
}
object ApplyDemo {
def main(args: Array[String]): Unit = {
val p = new P("小花")
//使用 apply 方法来创建对象
val p2 = P("小黑") //自动 apply(name: String)
val p3 = P() // 自动触发 apply()
println("p2.name=" + p2.name) //小黑
println("p3.name=" + p3.name) //匿名
}
}
apply方法使用
- 伴生对象的apply函数哪怕没有参数也需要加上一对”()”
- 我们可以在伴生对象里实现apply函数,在函数里做一些事情,如果我们想要得到class对象的实例,而没有通过new的方式,那么它会先去执行该class的伴生对象的apply函数
- 我的理解是,伴生对象的apply方法是作为该对象默认的隐式调用方法去加载,上例中的P()等价于P.apply()
特质 – Trait
Trait介绍
从面向对象来看,接口并不属于面向对象的范畴,Scala 是纯面向对象的语言,在 Scala 中,没有接口;
Scala 语言中,采用特质 trait(特征)来代替接口的概念,也就是说,多个类具有相同的特征时,就可以将这个特质独立出来,采用关键字 trait 声明。理解 trait 等价于(interface + abstractclass);
Trait声明
命名规范,首字母大写
trait 特质名 {
trait 体
}
Trait使用原理
一个类具有某种特质,就意味着这个类满足了这个特质的所有要素,所以在使用时,也采用了 extends 关键字,如果有多个特质或存在父类,那么需要采用 with 关键字连接。特质可以同时拥有抽象方法和具体方法,一个类可以实现/继承多个特质。
//当一个 trait 有抽象方法和非抽象方法时
//1. 一个 trait 在底层对应 public abstract interface TraitDemo 接口,
// 在接口中具有所有的方法,但都是抽象方法,没有具体实现;
//2. 还对应 public abstract class TraitDemo$class 抽象类,
// 在抽象类中也具有所有方法,但是有实现方法
trait TraitDemo { //富接口
def sayHi() //抽象方法
def sayHello(): Unit = { //普通实现方法
println("say Hello~~")
}
}
//1.class Sheep extends TraitDemo 在底层 对应 class Sheep implements TraitDemo
//2.当在 Sheep 类中要使用 TraitDemo 的实现的方法,就通过 Trait03$class
class Sheep extends TraitDemo {
override def sayHi(): Unit = {
println("小羊 say hi~~")
}
}
tips:特质中既有抽象方法,又有非抽象方法,即富接口。
Trait 底层原理参考图
动态混入
特点
动态混入是 Scala 特有的方式(java 没有动态混入),可在不修改类声明/定义的情况下,扩展类的功能,非常的灵活,耦合性低 。
创建
object Demo {
def main(args: Array[String]): Unit = {
//在不修改 类的定义基础,使用 trait 方法
val oracleDB = new OracleDB with Operate //ocp 原则
oracleDB.insert(100)
//如果一个抽象类有抽象方法,如何动态混入特质
val mySql = new MySQL with Operate {
override def query(): Unit = { //实现抽象方法
println("query...")
}
}
mySql.insert(999)
mySql.query()
}
}
trait Operate { //特质
def insert(id: Int): Unit = { //方法(实现)
println("插入数据 = " + id)
}
}
class OracleDB { //普通类
}
abstract class MySQL { //抽象类
def query()
}
tips:在 Scala 中创建对象共有几种方式?
- new 对象
- apply 创建
- 匿名子类方式
- 动态混入
叠加特质
构建对象的同时如果混入多个特质,称之为叠加特质。
特点:
- 那么特质声明顺序从左到右,方法执行顺序从右到左;
- Scala中,如果在特质中调用 super,并不是表示调用父特质的方法,而是向前面(左边)继续查找特质,如果找不到,才会去父特质查找;
- 如果想要调用具体特质的方法,可以指定:super[特质].xxx(…).其中的泛型必须是该特质的直接超类类型。
Demo
object AddTraits {
def main(args: Array[String]): Unit = {
val mysql = new MySQL with DB with File
println(mysql)
mysql.insert(100)
//声明顺序从左往右 方法执行执行顺序从右往左
//1.Operate4... 1.向文件
//2.Data4 2.向数据库
//3.DB4 3.插入数据100
//4.File4
}
}
trait Operate { //特质
println("Operate...")
def insert(id: Int) //抽象方法
}
trait Data extends Operate { //特质,继承了 Operate
println("Data")
override def insert(id: Int): Unit = { //实现/重写 Operate 的 insert
println("插入数据 = " + id)
}
}
trait DB extends Data { //特质,继承 Data
println("DB")
override def insert(id: Int): Unit = { // 重写 Data 的 insert
println("向数据库")
super.insert(id)
}
}
trait File extends Data { //特质,继承 Data
println("File")
override def insert(id: Int): Unit = { // 重写 Data 的 insert
println("向文件")
super.insert(id) //调用了 insert 方法(难点),这里 super 在动态混入时,不一定是父类
}
}
class MySQL {} //普通类
特质中的字段
- 特质中可以定义具体字段,如果初始化了就是具体字段,如果不初始化就是抽象字段;
- 混入该特质的类就具有了该字段,字段不是继承,而是直接加入类,成为自己的字段;
- 特质中未被初始化的字段在具体的子类中必须被重写。
特质的构造顺序
在声明类的时候混入特质:
- 调用当前类的超类构造器
- 第一个特质的父特质构造器
- 第一个特质构造器
- 第二个特质构造器的父特质构造器, 如果已经执行过,就不再执行
- 第二个特质构造器
- 重复…
- 当前类构造器
在创建对象时动态混入特质:
- 调用当前类的超类构造器
- 当前类构造器
- 第一个特质的父特质构造器
- 第一个特质构造器
- 第二个特质构造器的父特质构造器, 如果已经执行过,就不再执行
- 第二个特质构造器
- 重复…
扩展类的特质
特质可以继承类,以用来拓展该特质的一些功能,所有混入该特质的类,会自动成为那个特质所继承的超类的子类。如果混入该特质的类,已经继承了另一个类(A 类),则要求 A 类是特质超类的子类,否则就会出现了多继承现象,发生错误。
// LoggedException 特质继承了 Exception,就可以使用 Exception 功能
trait LoggedException extends Exception {
def log(): Unit = {
println(getMessage()) // getMessage方法来自于 Exception 类
}
}
自身类型
自身类型:主要是为了解决特质的循环依赖问题,同时可以确保特质在不扩展某个类的情况下,依然可以做到限制混入该特质的类的类型。
//Logger 就是自身类型特质,当这里做了自身类型后,那么
// trait Logger extends Exception,要求混入该特质的类也是 Exception 子类
trait Logger {
// 明确告诉编译器,我就是 Exception,如果没有这句话,下面的 getMessage 不能调用
this: Exception =>
def log(): Unit ={
// 既然我就是 Exception, 那么就可以调用其中的方法
println(getMessage)
}
}
class Console extends Logger {} // 错误
class Console extends Exception with Logger {}//对
类的嵌套
嵌套类的使用
内部类访问外部类的属性
- 通过外部类对象访问:外部类名.this.属性名
- 通过外部类别名访问:外部类别名.属性名
object ScalaInnerClassDemo {
def main(args: Array[String]): Unit = {
//创建外部类的实例
val outer : ScalaOuterClass = new ScalaOuterClass();
//在 scala 中,创建成员内部类的语法是
//对象.内部类 的方式创建, 这里语法可以看出在 scala 中,默认情况下内部类实例和外部对象关联
val inner = new outer.ScalaInnerClass
inner.info()
//创建静态内部类实例
val staticInner= new ScalaOuterClass.ScalaStaticInnerClass()
}
}
class ScalaOuterClass {
var name = "wen" //定义属性
myouter => //这里我们可以这里理解 外部类的别名 看做是外部类的一个实例
class ScalaInnerClass { //成员内部类
def info() = {
// 访问方式1:外部类名.this.属性名
// 理解 ScalaOuterClass.this 就相当于是 ScalaOuterClass 这个外部类的一个实例
// 通过 ScalaOuterClass.this 实例对象去访问 name 属性
println("name = " + ScalaOuterClass.this.name)
// 访问方式2:外部类别名.属性名
println("name = " + myouter.name)
}
}
}
object ScalaOuterClass { //伴生对象
class ScalaStaticInnerClass { //静态内部类
}
}
类型投影
类型投影是指:在方法声明上,如果使用 外部类#内部类 的方式,表示忽略内部类的对象关系,等同于 Java 中内部类的语法操作,我们将这种方式称之为 类型投影(即:忽略对象的创建方式,只考虑类型)
语法:ScalaOuterClass#ScalaInnerClass。
object ScalaInnerClassDemo {
def main(args: Array[String]): Unit = {
//创建外部类的实例
val outer1 : ScalaOuterClass = new ScalaOuterClass();
val outer2 : ScalaOuterClass = new ScalaOuterClass();
//创建内部类的实例
val inner1 = new outer1.ScalaInnerClass
val inner2 = new outer2.ScalaInnerClass
inner1.test(inner2) //如果在方法中不加入类型投影会报错,提示不匹配
}
}
class ScalaOuterClass {
outer =>
var name = "wen" //定义属性
class ScalaInnerClass { //成员内部类
inner =>
var age = 18
// ScalaOuterClass#ScalaInnerClass 类型投影的作用就是屏蔽 外部对象对内部类对象的影响
def test(ic: ScalaOuterClass#ScalaInnerClass): Unit = {
System.out.println("使用了类型投影" + ic)
}
}
}
隐式转换
隐式函数
隐式转换函数是以 implicit 关键字声明的带有单个参数的函数。这种函数将会自动应用,将值从一种类型转换为另一种类型
def main(args: Array[String]): Unit = {
implicit val name: String = "wen" //隐式值
def sayHi(implicit name: String):Unit={ //方法内引用隐式值
println("你好,"+ name)
}
implicit def func(dou:Double): Int = { //隐式函数,底层 生成 func$1
dou.toInt
}
sayHi // 底层 sayHi$1(name)
val num: Int = 3.5 //底层编译 func$1(3.5)
}
隐式转换的使用细节
- 隐式转换函数的函数名可以是任意的,隐式转换与函数名称无关,只与函数签名(函数参数类型和返回值类型)有关;
- 隐式函数可以有多个(即:隐式函数列表),但是需要保证在当前环境下,只有一个隐式函数能被唯一匹配
隐式转换丰富类库功能
object ImplicitDemo {
def main(args: Array[String]): Unit = {
implicit def addDelete(msql:MySQL): DB = { //编写一个隐式函数,丰富 mySQL 功能
new DB
}
val mySQL = new MySQL //创建 mysql 对象
mySQL.insert()
mySQL.delete() // 编译器工作 分析 addDelete$1(mySQL).delete()
}
}
class MySQL {
def insert(): Unit = {
println("insert")
}
}
class DB {
def delete(): Unit = {
println("delete")
}
}
隐式值
隐式值的优先级
编译器的优先级为 传值 > 隐式值 > 默认值,隐式值匹配不能有歧义,都没有就会报错。
隐式类
隐式类的特点
- 隐式类所带的构造参数有且只能有一个;
- 隐式类必须被定义在“类”或“伴生对象”或“包对象”里,即隐式类不能是 顶级的(top-level objects);
- 隐式类不能是 case class;
- 作用域内不能有与之相同名称的标识符;
object ImplicitClassDemo {
def main(args: Array[String]): Unit = {
implicit class DB(val m: MySQL) { //底层 ImplicitClassDemo$DB$2
def addSuffix(): String = {
m + " scala"
}
}
val mySQL = new MySQL
mySQL.query() //本身
mySQL.addSuffix() //关联到 DB1$1(mySQL).addSuffix();
}
}
class DB {}
class MySQL {
def query(): Unit = {
println("query Ok")
}
}
隐式的转换时机
- 当方法中的参数的类型与目标类型不一致时, 或者是赋值时;
- 当对象调用所在类中不存在的方法或成员时,编译器会自动将对象进行隐式转换(根据类型);
隐式解析机制
即编译器是如何查找到缺失信息的,解析具有以下两种规则:
- 首先会在当前代码作用域下查找隐式实体(隐式方法、隐式类、隐式对象)。(一般是这种情况)
- 如果第一条规则查找隐式实体失败,会继续在隐式参数的类型的作用域里查找。类型的作用域是指与该类型相关联的全部伴生模块,一个隐式实体的类型 T 它的查找范围如下(第二种情况范围广且复杂在使用时,应当尽量避免出现):
- 如果 T 被定义为 T with A with B with C,那么 A,B,C 都是 T 的部分,在 T 的隐式解析过程中,它们的伴生对象都会被搜索;
- 如果 T 是参数化类型,那么类型参数和与类型参数相关联的部分都算作 T 的部分,比如List[String]的隐式搜索会搜索 List 的伴生对象和 String 的伴生对象;
- 如果 T 是一个单例类型 p.T,即 T 是属于某个 p 对象内,那么这个 p 对象也会被搜索;
- 如果 T 是个类型注入 S#T,那么 S 和 T 都会被搜索
隐式转换的两个条件
- 不能存在二义性
- 隐式操作不能嵌套使用
implicit def f1(d: Double): Int = {
d.toInt
//val num2:Int = 2.3 //底层 f1$1(2.3) //f1$1 对应的就是 f1,就会形成递归
}
val num1: Int = 1.1
数据结构
集合类型
Scala 同时支持不可变集合和可变集合,默认采用不可变集合,不可变集合可以安全的并发访问
不可变集合:scala.collection.immutable
可变集合: scala.collection.mutable
集合有三大类:
序列 Seq(有序的,Linear Seq)、集 Set、映射 Map【key->value】,所有的集合都扩展自 Iterable 特质
不可变集合继承结构图
说明:
-
Seq 是 Java 没有的, List 被归属到 Seq 了,Scala 的 List 和 java 不是同一个概念;
-
for 循环中的 1 to 3 ,就是 IndexedSeq 下的 Vector集合;
-
String 是属于 IndexeSeq;
-
Queue 和 Stack 被归属到 LinearSeq;
-
Map 体系中有一个 SortedMap,说明 Scala 的 Map 可以支持排序
-
IndexSeq 和 LinearSeq 的区别
[ IndexSeq ] 是通过索引来查找和定位,因此速度快,比如 String 就是一个索引集合,通过索引即可定位;
[ LineaSeq ] 是线型的,即有头尾的概念,这种数据结构一般是通过遍历来查找,它的价值在于应用到一些具体的应用场景 (电商网站, 大数据推荐系统 :最近浏览的 10个商品);
可变集合继承结构图
说明:
可变集合中比不可变集合更加丰富
- 在 Seq 集合中, 增加了 Buffer 集合,将来开发中,常用的有 ArrayBuffer 和 ListBuffer
- 如果涉及到线程安全可以选择使用 syn… 开头的集合
数组
定长数组
def main(args: Array[String]): Unit = {
val arr = new Array[Int](4) //指定长度
arr(0) = 1
arr(1) = 2
arr(2) = 3
arr.update(1,3) //修改索引为1的值为3
//arr(4) = 5 //报错,索引越界异常
arrPrint(arr)
val arr1 = Array(1,2,3,4,5) //利用伴生对象object Array 的 apply方法,直接赋值
arrPrint(arr1)
}
def arrPrint(arr: Array[Int]):Unit = {
for(i <- arr)
println(i)
}
变长数组
val arrBuf = new ArrayBuffer[Int](2) //指定长度方式创建
arrBuf(0) = 1 //数组索引越界异常,不能使用索引方式来赋值,因为0号索引没有数据
val arrBuf = ArrayBuffer[Int](2,3,4,5) //apply方式创建
arrBuf(0) = 1 //在该索引有数据的情况下可以使用,表示修改数据
arrBuf.append(6,7,8) //扩容操作,支持可变参数,可接受一个或多个参数
arrBuf.size //数组容量
arrBuf.length //数组长度
arrBuf.remove(1) //移除该索引的数据
println(arrBuf) //直接输出ArrayBuffer(2, 3, 4, 5)
总结
- Array是不可变数组类似 Java 中的 Xxx[ ],如Int[ ],直接输出对象是内存地址;
- ArrayBuffer是不可变数组类似 Java 中的ArrayList,重写了toString直接输出对象为内容格式;
- Array[ ] / ArrayBuffer[ ]中的泛型可自动推导;
- append 方法会导致数组在底层会重新分配空间,进行扩容,内存地址会发生变化,也就成为新的ArrayBuffer;
数组的转换
arr.toArray 转换为定长数组
arr.toBuffer 转换为变长数组
多维数组
val arr = Array.ofDim[Double](3,4) //说明:二维数组中有三个一维数组,每个一维数组中有四个元素
arr(1)(1) = 11.11 //赋值
Array.ofDim[]() //接受可变参数,每个参数代表一个维度的容量
Scala数组与Java中List的转换
object ArrayBufferJavaList {
def main(args: Array[String]): Unit = {
// Scala 集合和 Java 集合互相转换
val arr = ArrayBuffer("1", "2", "3")
import scala.collection.JavaConversions.bufferAsJavaList
//对象 ProcessBuilder使用到上面的 bufferAsJavaList
val javaArr = new ProcessBuilder(arr)
// 这里 arrList 就是 java 中的 List
val arrList = javaArr.command()
println(arrList) //输出 [1, 2, 3]
}
}
元组 - Tuple
元组也是可以理解为一个容器,可以存放各种相同或不同类型的数据。
注意:元组中最大只能有 22 个元素
val tuple = (1,2,"abc",'d')
tuple._1 //按顺序号取值
tuple.productElement(0) //按元素索引号取值
println(tuple) //直接输出 (1,2,abc,d)
for(item <- tuple.productIterator) println(item) //获取迭代器遍历
列表 - List
Scala 中的 List 和 Java List 不一样,在 Java 中 List 是一个接口,真正存放数据是 ArrayList,而 Scala的 List 可以直接存放数据,就是一个 object,默认情况下 Scala 的 List 是不可变的,List 属于序列 Seq。
object List extends SeqFactory[List] List类的声明
不可变列表
val List = scala.collection.immutable.List 创建完整格式
val list = List(5,6) //创建直接赋值
list(0) //按索引取出元素
val list5 = 1+:list:+'a':+"abc" //List(1,5, 6, a, abc)
val list1 = Nil //创建一个空集合List( ),并不是null
val list2 = 1::2::3::4::list::Nil //List(1, 2, 3, 4, List(5, 6))
val list3 = 1::2::3::4::list:::Nil //List(1, 2, 3, 4, 5, 6)
val list4 = 1::2::3::4::list //List(1, 2, 3, 4, 5, 6)
-
默认是不可变List,是因为 List 在 scala 包对象声明了val List = scala.collection.immutable.List
-
List 中可以放任何数据类型,类型等价为 List[Any]
-
Nil也在 scala 包对象声明的,因此不需要引入其它包也可以使用
-
:+表示在列表右端添加元素,+:表示在列表左端添加元素,+为值得一侧,:为对象一侧
-
符号::与:::其实是运算方式,从右往左执行,::代表直接添加,:::代表添加里面的二级元素
eg:1::2::3::4::list::Nil
执行逻辑为:
List() 先添加一个空集合
List(List(x,x)) 再将list对象直接添加
List(1,2,3,4,List(x,x)) 然后在左侧依次添加元素
语法规范:
对象必须位于最右侧
如果已经有list对象可以不必加Nil,没有必须加
:::必须写在最后面,有且仅有一个
可变列表
ListBuffer
import scala.collection.mutable.ListBuffer //导包
val listBuf = ListBuffer(1,2,"abc") //创建,直接赋值
println(listBuf(0)) //获取元素,索引号
listBuf+="bcd" //添加元素
listBuf.append(123) //添加元素,等价与+=
listBuf.remove(1) //移除元素下标为1的元素
队列 - queue
队列是一个有序列表,底层用数组或是链表来实现,遵循先入先出的原则。
Scala中有 scala.collection.mutable.Queue 和 scala.collection.immutable.Queue 两种,通常使用可变队列。
val que = new mutable.Queue[Int] //创建
que += 1 //添加元素,在右侧
que ++= list //将对象中的元素加入
que.head //获取第一个元素 不影响原队列
que.dequeue //取出第一个元素 影响原队列
que.last //获取最后一个元素 不影响原队列
que.tail //获取除第一个元素以外的所有元素集合 不影响原队列
映射 - Map
Scala中Map 是一个散列表(数组+链表),它存储的内容是键值对(key-value)映射,Java的Map是无序的,Scala的不可变Map是有序的,可变Map是无序的。
可变 Map (scala.collection.mutable.Map)
不可变Map(scala.collection.immutable.Map)
val map1 = Map("Alice" -> 10, "Kotlin" -> "北京") //不可变Map
val map2 = mutable.Map("Alice" -> 10, "Kotlin" -> "北京") //可变Map
val map3 = new scala.collection.mutable.HashMap[String, Int] //空Map
val map4 = Map() //空Map
val map5 = mutable.Map(("Alice" , 10), ("Kotlin" , "北京")) //Map底层为二元元组,与之等价
map1.contains("Alice") //是否存在Key值
map1("Alice") //按Key取值,找不到Key报错java.util.NoSuchElementException: key not found
map1.get("Bob") //按Key取值,找到返回Some,找不到返回None
val value = map1.getOrElse("Bos","key1" -> "value1")//找不到Bos就返回默认键值对
val value1 = map1.getOrElse("Bos","value1") //也可以只返回默认值
map1 + ("Bob" -> 20) //不可变map添加值
map1("Alice") = 18 //Key存在则修改,不存在则添加
map2 += ("abc" -> 123, "bcd" -> 12) //Key存在则修改,不存在则添加
map2 -= ("Alice","Bos") //key 存在,就删除,如果key不存在,也不会报错
-
不可变Map的声明顺序与输出顺序一致,可变Map不一致
-
map.get(key).get 取值,会将数据进行包装,返回一个 Option 对象,要么是 Some,要么是 None
Some(x),x代表值,可以通过.get方法取值x
None调用.get方法会抛出异常java.util.NoSuchElementException: None.get
-
不可变Map在参数类型为var情况下,可以使用+=方式添加元素,即使使用var,但是在方法签名中的不可变Map依然不能使用+=,只能使用+,而可变Map在任何时候都能使用+或+=
遍历Map
for((k,v) <- map) println(k +" -> "+ v) //分别操作key和value
for(key <- map.keys) println(key) //操作所有的key
for(value <- map.values) println(value) //操作所有的value
for(tuple <- map) println(tuple) //返回一个key和value的元组
for(tuple <- map.Iterable) println(tuple) //与上等价
//证实具有Iterable特质的对象在for循环中默认传入的是一个迭代器
集 - Set
Set 集不可重复元素,不保留顺序,默认是以哈希集实现
不可变Set scala.collection.immutable.Set
可变Set scala.collection.mutable.Set
val set = Set[String]("wen","hello") //泛型指定元素类型
val set1 = Set(1, 2, 3) //默认不可变Set
val set2 = mutable.Set(1,2) //可变Set
//添加元素
set2.add(3) //Set(1,2,3)
set2.add(3,4) //add方式添加多个参数会形成Set(1,2,(3,4)),相当于加入一个元组类型
set2 += (5,6) //+=方式添加会扁平化加入Set
set2 += 8 //1个参数括号省略
//移除元素
set2 -= (1,3) //如果元素不存在,不生效,也不报错
set2.remove(3,4) //删除元组,使用add方式添加的元组
//Set遍历
for(x <- set02) { //实际传入的是迭代器
println(x)
}
高阶函数
可以接受一个函数的函数就是高阶函数
def map[B](f: (A) => B): HashSet[B] //map函数签名,f传入函数,HashSet[B]返回集类型
//定义高阶函数
def cast(tag: Double): Int = {
return tag.toInt
}
//方法名:参数类型=>返回值类型,普通参数
def test(f: Double => Int, tag: Double): Int = {
f(tag)
}
def main(args: Array[String]): Unit = {
val result: Int = test(cast,3.25)
}
map高阶函数
map是一个高阶函数
def large(n:Int): Int = {
2 * n
}
def main(args: Array[String]): Unit = {
val result = List[Int](1,2,3).map(large) //map中传入函数
}
flatmap
flat 即压扁,压平,扁平化,效果就是将集合中的每个元素的子元素映射到某个函数并返回新的集合。
def upper( s : String ) : String = s. toUpperCase
val names = List("Alice", "Bob", "Nick")
val namesup = names.flatMap(upper) //List(A, L, I, C, E, B, O, B, N, I, C, K)
filter - 过滤
filter:将符合要求的数据(筛选)放置到新的集合中
val map = mutable.HashMap("Alice" -> 10, "Bob" -> 20, "Kotlin" -> "北京")
val resultMap = map.filter(_._1.startsWith("A")) //判断Key,方法必须返回boolean
println(resultMap) //Map(Alice -> 10)
ruduceLeft
reduceLeft 的运行机制
def reduceLeft[B >: A](@deprecatedName('f) op: (B, A) => B): B
方法签名
reduceLeft(f) 接收的函数需要的形式为 op: (B, A) => B): B
reduceleft(f) 的运行规则是 从左边开始执行将得到的结果返回给第一个参数
然后继续和下一个元素运行,将得到的结果继续返回给第一个参数
reduceRight 执行逻辑与reduceLeft相反
// 计算List中的最小值
val list = List(3,4,2,5,7)
val minValue = list12.reduceLeft(min)
def min(n1: Int,n2: Int): Int = {
if(n1 < n2) n1 else n2
}
val list1 = List(1,2,3,4,5)
list1.reduceLeft(subtract) //执行逻辑((((1-2)-3)-4)-5) 输出 -13
list1.reduceRight(subtract) //执行逻辑(1-(2-(3-(4-5)))) 输出 3
list1.reduce(subtract) //默认执行reduceLeft 输出 -13
def subtract(n1: Int,n2: Int): Int = {
n1 - n2
}
注意:reduceLeft是把之前的结果放在第一个参数,reduceRight是把结果放在第二个参数
fold - 折叠
val list = List(1,2,3,4)
list.foldLeft(5)(subtract) //函数柯里化,等价于List(5,1,2,3,4).reduceLeft(subtract)
list.foldRight(5)(subtract) //等价于List(1,2,3,4,5).reduceRight(subtract)
list.fold(5)(subtract) //默认执行foldLeft
//方法缩写
val num = (5/:list)(subtract) //等价于list.foldLeft(5)(subtract)
val num1 = (5:\list)(subtract) //等价于list.foldRight(5)(subtract)
scanLeft扫描
与fold计算逻辑一致,但是会把计算结果存放到一个对应集合里
val num = List(1,2,3,4).scanLeft(5)(subtract)
//List(1,2,3,4).fold(5)(subtract)计算结果存到List中
println(num) //List(5, 4, 2, -1, -5)
val num1 = (1 to 5).scanLeft(5)(subtract)
println(num1) //Vector(5, 4, 2, -1, -5, -10)
val num2 = (1 to 5).scanRight(5)(subtract)
println(num2) //Vector(-2, 3, -1, 4, 0, 5),从右往左放
subtract(n1: Int,n2: Int) 此类计算以Left计算为例,在第一轮参数不够时,直接将参数放入二号位参数n2,2号位参数的每一次重新赋值,都表示一次临时结果,直至遍历完。
误区:一开始以为是和默认值进行一次运算,在算n1*n2时发现不是。
wordcount案例
import scala.collection.mutable
object wordcount {
def main(args: Array[String]): Unit = {
val str = "qwueouqwohewqoueoqwuoqweuowuwoe"
var map = Map[Char,Int]()
val count = str.foldLeft(map)(count1)
println(count)
val map1 = mutable.Map[Char,Int]()
val countnum = str.foldLeft(map1)(count2)
println(countnum)
}
//不可变Map实现
def count1(map: Map[Char,Int],char:Char): Map[Char,Int] ={
map+(char ->(map.getOrElse(char,0)+1))
}
//可变Map实现
def count2(map: mutable.Map[Char,Int],char:Char): mutable.Map[Char,Int] ={
map+=(char ->(map.getOrElse(char,0)+1))
}
}
扩展
zip - 拉链
两个集合,按照顺序依次进行元素合并,合并为一个二元元组,当某一个集合出现元素不足,则舍弃并结束
val list1 = List(1, 2, 3, 4)
val list2 = List(4, 5, 6)
val list3 = list1.zip(list2) // (1,4),(2,5),(3,6)
迭代器
通过对象的iterator特质进行遍历
//while方式
while (iterator.hasNext) {
println(iterator.next())
}
//for方式
for(item <- iterator) {
println(item)
}
Stream - 流
stream 是一个集合。这个集合,可以用于存放无穷多个元素,但是这无穷个元素并不会一次性生产出来,而是需要用到多大的区间,就会动态的生产,末尾元素遵循 lazy 规则(即:要使用结果才进行计算的) 。
def streamOb(num: BigInt):Stream[BigInt] = num #::streamOb(num + 1)
//指定生成规则n+1,streamOb时自定义的
val strea1 = streamOb(1)//创建流对象
println(strea1.head) //测试获取的时num
println(strea1.tail) //当对流执行 tail 操作时,就会生成一个新的数据,同一对象仅一次执行有效
View - 视图
特点
- view 方法产出一个总是被懒执行的集合
- view 不会缓存数据,每次都要重新计算,比如遍历 View 时
//计算一个数反转后仍等于自己
val res = (1 to 100).filter(eq) //方法被立即执行
val view = (1 to 100).view.filter(eq) //只有当view被调用时,函数才会被执行
def eq(i: Int):Boolean={
print("方法调用")
i.toString.equals(i.toString.reverse)
}
并行集合
Scala 为了充分使用多核 CPU,提供了并行集合(有别于前面的串行集合),用于多核环境的并行计算。
主要算法:
Divide and conquer : 分治算法,Scala 通过 splitters(分解器),combiners(组合器)等抽象层来实现,主要原理是将计算工作分解很多任务,分发给一些处理器去完成,并将它们处理结果合并返回;
Work stealin 算法【学数学】,主要用于任务调度负载均衡(load-balancing),通俗点完成自己的所有任务之后,发现其他人还有活没干完,主动(或被安排)帮他人一起干,这样达到尽早干完的目的;
(1 to 10).foreach(x => println(x+"=>"+Thread.currentThread().getName))
(1 to 10).par.foreach(x => println(x+"=>"+Thread.currentThread().getName))
//ForkJoinPool-1-worker-15
模式匹配
match case 类似 Java中的switch case,区别是Java的匹配中如果不想每一个都匹配,必须要写break,Scala中不用写,匹配到以后会自动中断。
def charMatch(c: Char) = c match {
case '+' => 1
case '-' => -1
//case中的条件守卫
case ch if ch.isDigit => ch.toInt //ch为一个变量名,会将要匹配的字符赋给这个变量
case _ if c.toString.equals("3") => 3 //隐藏变量名,使用形参c
case _ => 0 //_为通配符,如果都不匹配,又没有写通配会报错
}
类型匹配
val result = obj match {
case a: Int => a //将 a = obj,再判断类型
case _: BigInt => "超大数字" //隐藏变量
case b: Map[String, Int] => "对象是一个字符串-数字的 Map 集合"
case c: Map[Int, String] => "对象是一个数字-字符串的 Map 集合"
case d: Array[String] => d //"对象是一个字符串数组"
case e: Array[Int] => "对象是一个数字数组"
case f: BigInt => Int.MaxValue
case _ => "啥也不是"
}
总结:
-
在进行类型匹配时,编译器会预先检测是否有可能的匹配,如果没有则报错
-
如果 case 后有 条件守卫即 if ,那么这时的 _ 不是表示默认匹配
-
对象匹配时,如果不想接受某个值,可以使用 _ 来忽略,case Bundle(_, _, Book(desc, _), _*) => desc
即 _ 作为参数时,表示忽略某一个参数
-
_* 表示任意多个参数
匹配数组
Array(x,y) 匹配数组有两个元素,并将两个元素赋值为 x 和 y。当然可以依次类推 Array(x,y,z)
val arrs = Array(Array(0), Array(1, 0), Array(0, 1, 0),
Array(1, 1, 0), Array(1, 1, 0, 1))
for (arr <- arrs ) {
val result = arr match {
case Array(0) => "0" //匹配有且只有0的数组
case Array(x, y) => x + "=" + y //匹配两个元素的数组
case Array(0, _*) => "以 0 开头和数组" //匹配以0为首位的数组
case _ => "什么集合都不是"
}
}
匹配列表
for (list <- Array(List(0), List(1, 0), List(88), List(0, 0, 0), List(1, 0, 0))) {
val result = list match {
case 0 :: Nil => "0" //匹配有且仅有0的列表
case x :: y :: Nil => x + " " + y //匹配两个元素的列表
case 0 :: tail => "0 ..." //tail变量,将匹配值赋给变量
case x :: Nil => x //匹配一位元素的列表
case _ => "something else" //通配
}
println(result)
}
注: ::运算方式,从右往左运算,在左侧添加元素
匹配元组
for (pair <- Array((0, 1), (1, 0), (10, 30), (1, 1), ( 0, 2, 3))) {
val result = pair match {
case (0, _) => "0 ..." //匹配0开头的二元元组
case (y, 0) => y //匹配0结尾的二元元组
case (x, y) => (y, x) //匹配到任意二元元组
case _ => "other" //通配
}
println(result)
}
匹配对象
object objMatch{
def main(args: Array[String]): Unit{
val namesString = "Alice,Bob,Thomas" //字符串
namesString match {
case Names(first, second, third) => {
println("the string contains three people's names")
println(s"$first $second $third")
}
case _ => println("nothing matched")
}
}
}
object Names {
//当构造器是多个参数时,就会触发这个对象提取器
def unapplySeq(str: String): Option[Seq[String]] = {
if (str.contains(",")) Some(str.split(","))
else None
}
}
总结
case Names(x,x,x)运行机制,当执行case 对象(x,x,x),系统认为发生匹配行为,会默认调用对象身上的对象提取器unapply(一个参数时)或者unapplySeq(多个参数时),如果返回Some对象则将Some中的值取出,判断元素个数是否满足参数个数(多了少了都不行),满足条件则匹配成功,再分别赋值给各个参数,如果返回None对象则匹配失败。
变量声明的模式
val (x, y, z) = (1, 2, "hello") //一一对应
val arr = Array(1, 7, 2, 9)
val Array(first, second, _*) = arr // 匹配 arr 的前两个元素,必须使用_*
val (q, r) = BigInt(10) /% 3 //说明 q = BigInt(10) / 3 r = BigInt(10) % 3
注:/%这个很奇葩,试了其他组合好像都不行,就/%好使
for表达式中的模式
for ((k, 0) <- map) { //只匹配value为0的键值对
println(k + " --> " + 0)
}
for ((k, v) <- map if v >= 1) { //for循环守卫写法,更加灵活强大
println(k + " ---> " + v)
}
样例类
语法格式:
即使没有够惨参数也需要加()
特点:
- 样例类用case关键字声明,本质上仍然是类,是为模式匹配而优化的类
- 构造器中的参数都成为val,除非被显式声明为var(不建议)
- 样例类提供了apply方法,不用new关键字也能创建对象
- 样例类提供了模式匹配unapply的方法
- 样例类自动生成了 toString、equals、hashCode 和 copy 方法
样例类的对象匹配
object caseClass {
def main(args: Array[String]): Unit = {
//样例类对象匹配
for(item <- Array(RMB(50.00),Dollar(5.0,"USA"),Unknown)){
item match {
case RMB(x) =>println("¥"+x)
case Dollar(x,y) => println("$"+x+" from "+y)
case Unknown => println("Unknown")
case _ => println("No Matched!!!")
}
}
//对象copy -> 克隆
val dollar = new Dollar(100.0,"USA")
println(dollar) //样例类重写了toString
println(dollar.copy()) //即使没有参数也要写()
println(dollar.copy(99)) //修改属性,默认顺序第一个参数
println(dollar.copy(country = "wen")) //修改指定属性,指定方法声明的形参名
}
}
abstract class money{}
case class RMB(value: Double) extends money {}
case class Dollar(value: Double,country: String) extends money {}
case object Unknown extends money{}
匹配细节
//知识点 1 忽略参数
val res = sale match {
//如果我们进行对象匹配时,不想接受某些值,则使用_ 忽略即可,_* 表示所有
case Bundle(_, _, Book(desc, _), _*) => desc
}
//知识点 2-通过@表示法将嵌套的值绑定到变量。_*绑定剩余 Item 到 rest
val res2 = sale match {
//如果我们进行对象匹配时,不想接受某些值,则使用_ 忽略即可,_* 表示所有
case Bundle(_, _, art @ Book(_, _), rest @ _*) => (art, rest)
}
密封类
如果想让case样例类必须在同一个源文件中声明,可以将case的超类声明为密封类sealed
abstract sealed class money{} //所有它的样例类,都必须在和它同一个文件声明
case class RMB(value: Double) extends money {}
case class Dollar(value: Double,country: String) extends money {}
case object Unknown extends money{}
函数式编程
偏函数
//将集合 list 中的所有数字+1,并返回一个新的集合 , 忽略非数字
val list = List(1, 2, 3, 4, "hello")
//方案 1 filter -> map
val listToInt = list.filter(x => x.isInstanceOf[Int]).map(x=>x.asInstanceOf[Int]+1)
//方案 2 模式匹配
val result = list.map{ x=>
x match{
case x:Int => x+1
case _ =>
}
}
//方案 3 偏函数
val partialFunc = new PartialFunction[Any,Int] { //[接受参数类型,返回结果类型]
// isDefinedAt(x: Any) 如果返回true,就会去调用apply构建对象实例,如果是false则过滤
override def isDefinedAt(x: Any): Boolean = x.isInstanceOf[Int]
//对象获取器
override def apply(v1: Any): Int = v1.asInstanceOf[Int]+1
}
val result1 = list.collect(partialFunc)
//简化形式1
def partialFunc1: PartialFunction[Any,Int]={
case x:Int=>x+1
}
val result2 = list.collect(partialFunc1)
//简化形式2
val list2 = list.collect{
case x:Int=>x+1
}
总结
- 使用构建特质的实现类(使用的方式是 PartialFunction 的匿名子类),PartialFunction 是个特质
- 构建偏函数时,参数形式 [Any, Int]是泛型,第一个表示参数类型,第二个表示返回参数
- 当使用偏函数时,会遍历集合的所有元素,编译器执行流程时先执行 isDefinedAt()如果为 true ,
就会执行 apply, 构建一个新的 Int 对象返回,isDefinedAt() 为 false 就过滤掉这个元素,即不构建新的 Int 对象 - map 函数不支持偏函数,因为 map 底层的机制是循环遍历,无法过滤处理原来集合的元素
- collect 函数支持偏函数
函数变量
Scala中函数作为一个变量传入到了另一个函数中,那么该作为参数的函数的类型是:function1,即:(参数类型) => 返回类型。
def plus(x:Int)=x*2
val res= List(1,2,3).map(plus(_))
匿名函数
没有函数名的方法体就是匿名函数
val func = (x:Int,y:Int)=>x+y
func(2,3)
匿名函数的说明
- 不需要写 def 函数名
- 不需要写返回类型,使用类型推导
- = 变成 =>
- 如果有多行,则使用{} 包括
高阶函数
能够接受函数作为参数的函数,叫做高阶函数 (higher-order function)。
//高阶函数特性 1 :可以接受函数作为参数
def test(f: Double => Double, f2: Double =>Int , n1: Double) = { }
// 方法形参名:参数类型 =>返回值类型 普通参数
//高阶函数特性2 : 返回值也可以是函数类型
def minusxy(x: Int) = {
(y: Int) => x - y //匿名函数
}
val f1 = minusxy(3) //此时方法体中的x被置为常量
println(f1(1)) // 2
println(f1(9)) // -6
println(minusxy(4)(9)) // -5
参数类型推断
写法说明
-
参数类型是可以推断时,可以省略参数类型
-
当传入的函数,只有单个参数时,可以省去括号
-
如果变量只在=>右边只出现一次,可以用_来代替
但是<
list.redece(_+_)
>这个写法可以,我想是因为这个折叠算法,一次只传入一个参数,另一个就变成常量了,所以才可以用吧
val list = List(1, 2, 3, 4)
list.map((x:Int)=>x + 1)
list.map((x)=>x + 1)
list.map(x=>x + 1)
list.map(_ + 1)
list map (_ + 1)
闭包
闭包就是一个函数和与其相关的引用环境组合的一个整体(实体)。
//指定一个后缀名,创建一个闭包,判断传入的name是否有后缀名,没有就加入指定后缀名
val func1=(suffix: String)=>(name:String) =>if(name.contains(".")) name else name+s
val func2 = func1(".txt") //此时的func2就是一个闭包
println(func2("wen"))
println(func2("wen.wuoe"))
注:在使用闭包时,需要清楚返回函数引用了函数外的哪些变量,因为他们会组合成一个整体(实体),形成一个闭包。
函数柯里化
函数编程中,接受多个参数的函数都可以转化为接受单个参数的函数,这个转化过程就叫柯里化。
柯里化就是证明了函数只需要一个参数而已,柯里化是面向函数思想的必然产生结果。
//实现两个整数相加的函数
def sum1(x: Int, y: Int) = x + y //普通函数
sum1(10,20)
def sum2(x: Int) = (y: Int) => x + y //返回匿名函数
sum2(10)(20)
def sum3(x: Int)(y:Int) = x + y //函数柯里化语法
sum3(10)(20) //调用
控制抽象
控制抽象函数,满足如下条件
- 参数是函数
- 函数参数没有输入值也没有返回值,func: () => Unit
//利用控制抽象实现while格式
//实现将一段代码(从形式上看),作为参数传递给高阶函数,在高阶函数内部执行这段代码
def whileDemo(func: () => Unit) = {
new Thread(){
override def run(): Unit = func() //此时不写括号,代码将不会执行
}.start()
}
whileDemo{
()=>
println("while start")
Thread.sleep(1000)
println("while stop")
}
//去括号写法
def whileDemo(func: => Unit) = { //1
new Thread(){
override def run(): Unit = func //2
}.start()
}
whileDemo{ //3
println("while start")
Thread.sleep(1000)
println("while stop")
}
//进阶版 - 实现while的条件控制
def whileDemo(condition: => Boolean)(func: => Unit): Unit = {
if(condition){
func
whileDemo(condition)(func)
}
}
var x = 10
println("while start")
whileDemo(x>0){
println(x)
x-=1
}
println("while stop")
日期
val date = new Date()
println(s"date = $date") //date = Wed Jul 24 04:10:24 CST 2019
val simpleDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") //指定日期输出格式
val dateFormat = simpleDate.format(date) //将当前日期格式化
println(s"dateFromat = $dateFormat") //datefromat = 2019-10-205 04:10:24
Scala语言特点
Scala 是运行在 Java 虚拟机(Java Virtual Machine)之上,因此具有如下特点:
- 轻松实现和丰富的 Java 类库互联互通;
- 它既支持面向对象的编程方式,又支持函数式编程;
- 它写出的程序像动态语言一样简洁,但事实上它确是严格意义上的静态语言;
- Scala 就像一位武林中的集大成者,将过去几十年计算机语言发展历史中的精萃集于一身,化繁为简,为程序员们提供了一种新的选择。设计者马丁·奥得斯基 希望程序员们将编程作为简洁,高效,令人愉快的工作。同时也让程序员们进行关于编程思想的新的思考。
递归编程思想
编程范式
- 在所有的编程范式中,面向对象编程(Object-Oriented Programming)无疑是最大的赢家;
- 但其实面向对象编程并不是一种严格意义上的编程范式,严格意义上的编程范式分为:命令式编程(Imperative Programming)、函数式编程(Functional Programming)和逻辑式编程(LogicProgramming)。面向对象编程只是上述几种范式的一个交叉产物,更多的还是继承了命令式编程的基因;
- 在传统的语言设计中,只有命令式编程得到了强调,那就是程序员要告诉计算机应该怎么做。而递归则通过灵巧的函数定义,告诉计算机做什么。因此在使用命令式编程思维的程序中,是现在多数程序采用的编程方式,递归出镜的几率很少,而在函数式编程中,大家可以随处见到递归的方式。
scala 中循环不建议使用 while 和 do…while,而建议使用递归
案例1:计算1~999999999的和
//使用while循环实现
val startTime = System.currentTimeMillis()
var num11 :Int= 0
var sum11:BigInt = BigInt(0)
while(num11 < 999999999) {
sum11+=num11
num11+=1
}
println(sum11)
val endTime = System.currentTimeMillis()
println("while循环用时: " + (endTime-startTime))
println("+++++++++++++++++++++++++++++++++++++++")
//使用递归实现
val startTime1 = System.currentTimeMillis()
def DG(num: Int,sum: BigInt):BigInt={
if(num < 999999999) digui(num+1,sum+num) else sum
}
var sum22: BigInt= 0
val result22 = DG(0,sum22)
println(result22)
val endTime1 = System.currentTimeMillis()
println("递归循环用时: " + (endTime1-startTime1))
println("+++++++++++++++++++++++++++++++++++++++")
//使用scala并行计算
val startTime3 = System.currentTimeMillis()
val parSum = (BigInt(0) to BigInt(1000000)).par.reduceLeft(_+_)
println(parSum)
val endTime3 = System.currentTimeMillis()
println("并行计算用时: " + (endTime3-startTime3))
案例2:计算list中的最大值
def listMax(list: List[Int]): Int = {
if (list.isEmpty) throw new NoSuchElementException
if (list.size == 1) list.head
else if (list.head > listMax(list.tail)) {
list.head
} else listMax(list.tail)
}
案例3:字符串反转
def reverse(str:String):String={
if(str.size == 1) str else reverse(str.tail)+str.head
}
案例4:阶乘
def factorial(n: Int): Int = {
if (n == 1) 1 else n * factorial(n - 1)
}
案例5:递归文件名
//这是一段Java伪代码
//其中findFile(path)找到该路径下的所有文件
//其中fildDir(path)找到该路径下的所有文件夹
public void getAllFilename(String path){
File[] files = findFile(path);
foreach(File file:files){
if(file.isfile){
System.out.println(file.getName)
}
}
FileDir[] dirs = fildDir(path);
foreach(FileDir dir: dirs){
if(dir.isdir){
getAllFileName(dir.toString)
}
}
}
完