第一章 Scala入门
- Scala是基于Java的语言,所以Java的语法规则在Scala中可以直接使用
- Scala语言的运行环境其实也是JVM,所以Scala语言真正运行的程序其实也是字节码文件(.class文件)
- Scala是一个函数式编程语言,也是一门强类型语言,在程序运行之前,一定要明确类型
- Spark(离线数据计算) —— 95%Scala 5%Java
Flink(流式(实时)数据计算) —— 30%Scala 70%Java
Kafka —— 100%Scala Scala语言式基于Java语言开发的,所以Java的语法可以在Scala代码中直接使用
第二章 变量和数据类型
Scala中本地变量的声明规则:
Scala中的变量需要使用关键字声明:var | val
var | val 变量的名称: 变量的类型 = 变量的值
使用var声明的变量的值可以修改
使用val声明的变量的值不可以修改
更推荐使用val- 如果变量的类型可以通过变量值推断出来,那么变量的类型可以省略
Java是强类型的语言,就意味着程序执行之前,变量的类型一定要确定
既然一个变量的类型确定了,那么取值也基本上确定了
如果一个变量的取值能够确定,那么它的类型其实基本上也就确定了
既然通过取值可以确定变量的类型,那么类型就可以不用声明,省略即可var name = "zhangsan"
Scala代码灵活、简单,但是隐藏了很多的细节,所以容易出错
这种类型推断不能应用在多态的场景下 - Java中声明一个变量,可以不用显式地初始化,在使用这个变量之前初始化即可
String name;
name = "zhangsan";
- Java中如果变量只有声明,而没有初始化,Java不会分配内存空间给这个变量,也根本不会存这个变量,这个变量在编译时会删除
- 静态代码块中其实是对静态属性进行初始化的地方,使用final修饰的静态变量,初始化不是由静态代码块完成的,而是加载时,自动完成了初始化
public static int age = 30;
相当于
public static int age;
static {
age = 30;
}
final的直接自动赋值,不走静态代码块public final static int age = 30;
- 插值字符串不能使用在JSON格式中,会出现错误(官方承认)
- Scala进行输出的时候,使用的是Java的IO流
- 遇到new、getstatic 和 putstatic 或 invokestatic 这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。对应场景是:使用 new 实例化对象、读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法。
- 栈内存:方法执行时,方法的栈帧(小内存),栈帧中存储的是代码指令和局部变量,以及基本数据类型
堆内存:程序在执行时,动态创建的对象
1. new 2. 反射 3. 反序列化 4. 克隆
方法区内存: - 编译器进行常量编译时,会直接计算,变量计算是在执行时完成
代码1:
int a = 10 + 10 + 10; (编译时完成)
代码2:
int a = 10;
int b = a + a + a; (执行时完成)
第三章 运算符
- 位运算符面试题:
1. HashMap从JDK1.8之后会有红黑二叉树结构,极限情况下,HashMap存放多少条数据会变结构?
答:红黑二叉树查询效率比链表快
极限情况下,HashMap中防止11条数据,就会形成红黑二叉树结构
8(链表的长度) + 64(底层的数据结构的容量)=> 红黑二叉树
HashMap的底层计算数据所在的位置采用的算法是:
hash(key.hashCode) & (length - 1)
当链表长度为8时还没问题,当长度为9(>8时),会认为容量不够,会扩容为2倍
8(16 )=> 9(162)=>10(322)=> 11(64,感觉一直扩容不对劲了,就变成红黑树了)=> Tree
- HashMap扩容时,为什么是容量的2倍?
答:通过原理,HashMap底层的数据容量必须为2的N次方,为了能保证这一点,所以扩容后的容量也应该是2的N次方
在Scala中其实是没有运算符的,所有运算符都是方法
scala是完全面向对象的语言,所以数字其实也是一个数值类型的对象
当调用对象的方法时,点.可以省略
如果函数参数只有一个,或者没有参数,()可以省略
Scala对一些基础的运算进行了简化,如果方法采用了特殊符号,那么点“.”可以省略object ScalaOper {
def main(args: Array[String]): Unit = {
val i : Int = 10
val j : Int = i.+(10)
val k : Int = j +(20)
val m : Int = k + 30
println(m)
println(i.toString()) // 10
println(i toString()) // 10
println(i toString) // 10
}
}
原则上来讲,方法名中含有字母的不省略;方法名中使用了特殊字符的省略
以下代码执行结果如何?
val a = new String("abc") val b = new String("abc") // Scala中双等号其实就是equals,字符串的双等号其实就是比较内容,并且进行非空判断 // 如果就想比较内存地址,需要采用特殊的方法:eq println(a == b) // true println(a.equals(b)) // true println(a.eq(b)) // false
Scala中双等号其实就是equals,字符串的双等号其实就是比较内容,并且进行非空判断<br /> 如果就想比较内存地址,需要采用特殊的方法:eq
第四章流程控制
Scala中所有的表达式都有返回值,表达式的结果值取决于表达式满足条件的最后一行代码的执行结果,如果表达式的条件都不成立,那么结果值为Unit
Scala是强类型语言,在程序执行之前,就应该确定类型,所以当确定不了返回值类型是AnyVal还是AnyRef时就返回Any类型的返回值- HashMap扩容
- Scala中没有三元运算符,使用if分支判断来代替三元运算符
Scala中可以将字符串当成字符数组进行处理
// Scala中可以将字符串当成字符数组进行处理 val s = "Hello" for (c: Char <- s) { println(c) } // 如果数据类型可以推断出来,那么数据类型可以省略,在编译期间进行补全 for (c <- s) { println(c) }
默认情况下,for循环的返回值是Unit,Unit本身也是一个值,表示的含义是没有返回值,null其实是一个对象,但是表示对象为空;如果想要保留每一次循环的结果,需要采用特殊的关键字 yield;Scala中yield是一个关键字,Java中yield是Thread类的一个方法,如果想要在Scala中使用Thread类的方法,需要采用特殊方式:增加反引号 Thread.
yield
()// TODO 循环返回值 // 默认情况下,for循环的返回值是Unit // Unit本身也是一个值,表示的含义是没有返回值 // null其实是一个对象,但是表示对象为空 val result2 = for (i <- 1 to 5) { i } println(result2) // () // 如果想要保留每一次循环的结果,需要采用特殊的关键字 yield val result3 = for (i <- 1 to 5) yield { i * 2 } println(result3) // Vector(2, 4, 6, 8, 10) // Scala中yield是一个关键字,Java中yield是Thread类的一个方法 // 如果想要在Scala中使用Thread类的方法,需要采用特殊方式:增加反引号 // Thread.`yield`()
scala是完全面向对象的语言,所以无法使用break,continue关键字这样的方式来中断,或继续循环逻辑,而是采用了函数式编程的方式代替了循环语法中的break和continue;Scala中break方法会抛出异常,终止线程的后续执行,从逻辑上不对,为了解决这个问题,需要额外再添加代码
Breaks.breakable { // 捕捉异常 for (i <- 1 to 5) { if (i == 3) { // break; Breaks.break() // 抛出异常 } println("i = " + i) } }
第五章 函数式编程
面向对象编程
分解对象,行为,属性,然后通过对象的关系以及行为的调用来解决问题函数式编程
将问题分解成一个一个的步骤,将每个步骤进行封装(函数),通过调用这些封装好的功能按照指定的步骤,解决问题。第六章 面向对象编程
Java中sleep()和wait()的区别?
- sleep是一个静态方法,wait是一个成员方法
- 因为sleep是一个静态方法,和对象无关,当执行t1.sleep(1000);时,t1线程没有休眠,哪一个线程调用sleep方法,哪一个线程休眠;也因为与对象无关,无法释放锁
- wait是一个成员方法,和对象有关,当执行t2.wait();时,t2线程在等待;也因为与对象有关,可以释放锁
- sleep是一个静态方法,wait是一个成员方法
父类实现接口,和子类没有任何关系
子类实现接口,和父类也没有任何关系
接口的实现,和类的上下级没有任何关系,只和当前类有关系,所以接口和当前实现的类层级相同
单例对象
Scala中构造方法私有化:
class User private {
// Scala中构造方法私有化
// class User private() 或 class User private
}
private class User {
// 这个不是构造方法私有化,而是表示这是一个私有类
}
Scala是一个完全面向对象的语言,没有静态语法,那Scala怎么声明单例对象呢?
使用object关键字就可以声明单例对象
object关键字的作用:
顶层的类如果使用object关键字声明,那么编译时会产生两个类:
- 本身的类(ClassName)
- 单例对象的类(ClassName$):目的就是为了创建单例
单例对象的类的目的就是为了创建单例
object语法是用于模仿Java中的静态语法:类名.属性(方法)
这个单例对象是伴随着类所产生的一个对象,所以称之为伴生对象
如果class关键字声明的类和object声明的类名称一样,那么class声明的类就称之为伴生类
一般情况下,将成员方法都声明在伴生类中,需要构建类的对象进行访问
将静态方法(属性)声明在伴生对象中,通过类名进行访问,模仿静态语法
class User { // 伴生类
}
object User { // 伴生对象
def apply() = new User() // 构造伴生类对象
}
...
val user1 = new User() // 通过构造方法创建对象
val user2 = User.apply() // 通过伴生对象的apply方法构造伴生类对象
val user3 = User() // scala编译器省略apply方法,自动完成调用
第八章 模式匹配
所谓的模式匹配,其实为了模仿switch语法
Scala中没有default语法,使用特殊分支判断(case => {}),如果case 分支放置在最前面,那么后续的所有分支都执行不到,但是不会发生错误;如果分支中没有case _ 分支,那么如果其他分支也匹配不成功,那么会发生异常(MatchError)
Scala中没有break关键字,那么就没有所谓的穿透现象,如果匹配成功,那么执行箭头后面的代码,这个执行完毕后,会自动跳出,无需break
如果分支逻辑代码只有一行,那么大括号可以省略
【Scala的泛型中, (下划线)表示任意类型,如:List[] 】
模式匹配不支持泛型,泛型不起作用
Array[String]不是泛型**,编译器会编译为 String[] ,Array的类型匹配比较特殊,需要单独记**
object Test_3_18 {
def main(args: Array[String]): Unit = {
val arr = Array("xm", 123, 3.26, true)
val name = arr(Random.nextInt(arr.length))
println("name: " + name)
name match {
case str: String => println(s"match String $str")
case int: Int => println(s"match Int $int")
case double: Double => println(s"match Double $double")
case boolean: Boolean => println(s"match Boolean $boolean")
case matchTest: MatchTest => println(s"match MatchTest $matchTest")
case _: Any => println("nothing")
}
}
class MatchTest{
}
}
对函数的参数进行模式匹配时,只能对一个参数进行匹配;
对函数的参数进行模式匹配时,如果直接匹配,会导致有歧义,无法确定小括号的含义,为了明确含义,使用模式匹配需要增加case关键字;
如果使用case关键字进行模式匹配,那么小括号需要变为大括号
模式匹配中匹配对象需要有 unapply 方法,不然会报错
如果声明一个类的时候前面使用了case关键字声明,那么这个类称之为“样例类”,它是专门为模式匹配设计的;
样例类不仅仅生成半生对象,也生成了很多常用的方法(例如:apply,unapply);
样例类自动实现可序列化接口;
样例类的构造参数会自动变为类的属性,可以直接访问,但是不能更改(默认用val声明),如果想要修改,那么需要显式使用var声明参数(例如:case class User(var name: String, age: Int))
偏函数:这个偏表示的就是部分数据
这里的全指的就是全部数据
所谓的偏函数其实就是一个只对数据集部分数据进行处理的函数
使用偏函数进行处理,要求函数支持偏函数处理
第九章 异常
Java中异常有两大分类:
- 编译时异常:在程序编译时出现的异常,就称之为编译时异常(×)
- 编译时异常:在程序编译时出现(Java编译器会对其进行检查),提示程序开发且必须处理的异常就称之为编译时异常(√)
- 运行时异常:在程序运行时出现的异常,特点是Java编译器不会对其进行检查,就称之为运行时异常
Java中所有的异常都是类,怎么可能会在编译时创建呢
**
Scala的异常处理类似于模式匹配(类型匹配)
Scala的异常处理中,范围大的异常一般都是在后面捕捉,不然会先匹配到范围大的异常
Scala中的异常不区分所谓的编译时异常和运行时异常,也无需显示抛出方法异常,所以Scala中没有throws
关键字。
如果Java程序调用scala代码,明确异常需要增加注解 @throws(Exception)
try {
var n= 10 / 0
} catch {
case e: ArithmeticException => {
// 发生算术异常
println("发生了算术异常")
}
case _ =>{
// 对异常处理
println("发生了其他异常")
}
} finally {
println("finally")
}
第十章 隐式转换
简介
Scala中Byte
、Int
是对象类型,不是基本数据类型,所以不存在精度的概念
如果两个类型之间能够进行转换而不发生错误,那么必须两个类型之间有关系(继承关系、实现关系、混入[trait
]关系)
object ImplicitConversion {
def main(args: Array[String]): Unit = {
// 从语法逻辑上理解,下面的代码会发生错误,但是可以执行
// val声明的是final的类,两个之间不可能有关系
// 下面的代码其实是由编译器进行了隐式转换
val b: Byte = 10
val i: Int = b
println(i)
// 从语法逻辑上理解,下面的代码会发生错误,但是可以执行
// 字符串是不能够直接当成数组使用的,所以应该是不能使用索引访问的
// Scala中没有字符串的类,字符串来自Java,但是Java的字符串没有apply方法
// 编译器在编译过程中,将字符串转换成了Scala的StringOps类,所以就有了apply方法
// 这个转换的过程看不见,由编译器自动完成,所以称为隐式转换
val s = "abc"
println(s.apply(0))
println(s(0))
// 伴生对象中可以声明apply方法,可以通过伴生对象直接调用
// 伴生类中可以声明apply方法,可以通过伴生对象直接调用
// apply方法一般用于构建当前类的对象,但不是固定的,只是常规用法
val user = new User()
user.apply("zhagnsan")
user("zhagnsan")
}
class User {
def apply(name: String) = {
println("xxxxx")
}
}
object User {
def apply(): User = new User()
}
}
隐式函数
当程序出现编译错误的时候,编译器会尝试在整个作用域范围内,查找能够让当前错误程序编译通过的转换逻辑,这种操作称之为二次编译,这个转换逻辑必须要让编译器识别。这个由编译器自动完成,所以称之为隐式转换
隐式转换是二次编译,隐式转换是在编译出现错误时执行的
在转换逻辑前增加关键字 implicit
,让编译器识别:
object ImplicitConversion {
def main(args: Array[String]): Unit = {
// 同一个作用域中,有相同的转换规则,那么编译器不知道调用哪一个,所以会报错,这是不允许的
// 隐式转换其实就是类型的转换,所以转换时,隐式函数中参数只有一个,就是需要被转换的类型
// 在转换逻辑前增加关键字 implicit ,让编译器识别
implicit def transform(d: Double): Int = {
d.toInt
}
// 在转换逻辑前增加关键字 implicit ,让编译器识别
implicit def transform_user(user: User): UserExt = {
new UserExt
}
// 当程序出现编译错误的时候,编译器会尝试在整个作用域范围内,查找能够让当前错误程序编译通过的转换逻辑
val age: Int = thirdPart_Age()
println("第三方程序提供的年龄数据为:" + age)
// 隐式转换是二次编译,隐式转换是在编译出现错误时执行的
val user = thirdPart_User
user.insertUser()
user.updateUser()
}
def thirdPart_Age(): Double = {
20.5
}
class UserExt {
def updateUser() = {
println("update user...")
}
}
class User {
def insertUser() = {
println("insert user...")
}
}
def thirdPart_User = {
new User
}
}
同一个作用域中,有相同的转换规则,那么编译器不知道调用哪一个,所以会报错,这是不允许的
隐式转换其实就是类型的转换,所以转换时,隐式函数中参数只有一个,就是需要被转换的类型
隐式参数&隐式变量
object ImplicitConversion {
def main(args: Array[String]): Unit = {
// 如果声明函数参数时,预先判断出来,参数值在后期可能会发生变化,那么可以将这个参数设定为隐式参数
// 所谓的隐式参数,就是让编译器识别的
// 编译器发现参数为隐式参数时,会在作用域中查找参数值,这个参数值称之为隐式变量
// 隐式变量的类型和隐式参数的类型保持一致
def reg(implicit password: String = "000000"): Unit = {
println("注册用户,初始密码为:" + password)
}
implicit val psw: String = "123123"
// 隐式参数调用时,可以不使用参数列表
reg // 123123
// 当增加参数列表小括号时,等同于告诉编译器,不让编译器帮我找参数值,我自己来找,此时隐式转换不起作用
reg() // 000000
reg("111111") // 111111
// 函数柯里化 函数作为参数使用
def test(x: Int, y: Int)(implicit f: (Int, Int) => Int) = {
f(x, y)
}
implicit def sum = (x: Int, y: Int) => { x + y }
println(test(10, 20)) // 30
println(test(10, 20)(_ + _)) // 30
println(test(10, 20)(_ - _)) // -10
// 源码:def sortBy[B](f: A => B)(implicit ord: Ordering[B]): Repr = sorted(ord on f)
val list = List(1, 2, 3, 4)
val newList = list.sortBy(num=>num)(Ordering.Int.reverse)
println(newList) // List(4, 3, 2, 1)
}
}
【排序功能补充】【元组排序】tuple
能进行排序,先比较元组中的第一个元素,如果相同,比较第二个,依次类推
// tuple能进行排序,先比较元组中的第一个元素,如果相同,比较第二个,依次类推
val newList = userList.sortBy(
user => {
(user.age, user.salary)
}
)(Ordering.Tuple2[Int, Int](Ordering.Int, Ordering.Int.reverse)) // 年龄升序,薪水降序
// 如果有多个数据参与排序,可以自定义排序规则
userList.sortWith(
(u1, u2) => {
// 返回结果就是你预期的结果的状态
// 如果是我们需要的结果就返回 true ,如果不是就返回 false
if (u1.age < u2.age) { // 年龄升序
true
} else if (u1.age == u2.age) {
u1.salary > u2.salary // 薪水降序
} else {
false
}
}
)
隐式类
将类声明为 implicit
,那么编译器会自动将当前编译错误的类和隐式类进行转换,如果能够转换,那么直接转换,此时就不需要隐式函数了
隐式类需要声明构造参数,这个参数就是用于转换的类型
- 其所带的构造参数有且只能有一个
- 隐式类必须被定义在“类”或“伴生对象”或“包对象”里,即隐式类不能是顶级的
object ScalaImplicit { def main(args: Array[String]): Unit = { val emp = new Emp() emp.insertUser() } class Emp { } // 将Emp转为User implicit class User(emp: Emp) { def insertUser(): Unit = { println("insert user...") } } }
隐式机制
所谓的隐式机制,就是一旦出现编译错误时,编译器会从哪些地方查找对应的隐式转换规则
隐式转换作用域:
- 在当前代码的作用域中
- 在上一级的作用域中(父类、父类的伴生对象)
- 在特质的作用域中(特质、特质的伴生对象)
- 当前类所在的包对象的作用域中
第十一章 泛型
泛型和类型的关系以及区别
- 泛型和类型无关,不能当成整体来使用
- 类型用于声明外层对象的类型,泛型主要用于声明内部数据的类型
List<User> // Java
,所以有时也将泛型称为“类型参数” - 类声明了泛型,仅仅表用可以设定约束,但是不一定必须有约束
- 如果没有泛型,那么泛型的类型自动设定为Object
- 当泛型相同时,考虑类型的父子关系,如果泛型不相同,不会考虑类型的父子关系(Java中)
-
泛型的作用时机
泛型的作用是在声明之后起作用,泛型是对内部数据的类型起到约束的作用,而不是方法的约束
泛型在编译后是不存在的,泛型只在编译时起作用,在运行时不起作用,字节码中没有泛型(**泛型的擦除**),泛型主要用于在编译器中进行操作泛型的协变和逆变
Scala中泛型也是不可变的,但是马丁想让泛型和类型当成整体来使用,同时对语法进行了变化:
协变(在泛型的前面增加符号:+):如果
Child
是User
的子类型,那么将Test[Child]
也当成Test[User]
的子类型来使用逆变(在泛型的前面增加符号:-):如果
Parent
是User
的父类型,那么将Test[Parent]
也当成Test[User]
的子类型来使用泛型的上限和下限
一般用于生产数据和消费数据,消费数据时泛型的上限(从上往下找)用于保证获取的数据的功能不丢失,所以需要限定类型;泛型的下限(从下往上找)用于生产数据要保证数据持有最基本的功能(转换)
Scala中泛型不用关键字,用颜文字表示:// 泛型上限 def test[A <: User](a: A): Unit = { println(a) } // 泛型下限 def test[A >: User](a: A): Unit = { println(a) }
上下文限定
上下文限定是将泛型和隐式转换的结合产物
object ScalaGeneric { def main(args: Array[String]): Unit = { def f[A: Test](a: A) = println(a) // 如果代码提示(...),一般情况下是隐式转换有问题 implicit val test: Test[User] = new Test[User] f( new User() ) } class Test[T] { } class Parent { } class User extends Parent{ } class SubUser extends User { } }
第十二章 正则表达式
之前讲的模式匹配其实体现的是数据的规则,当前的正则表达式体现的是字符串的规则
object ScalaRegex { def main(args: Array[String]): Unit = { // 构建正则表达式 val pattern = "Scala".r val str = "Scala is Scalable Language" // 匹配字符串 - 第一个 println(pattern findFirstIn str) // 匹配字符串 - 所有 val iterator: Regex.MatchIterator = pattern findAllIn str while ( iterator.hasNext ) { println(iterator.next()) } // 匹配规则:大写,小写都可 val pattern1 = new Regex("(S|s)cala") val str1 = "Scala is scalable Language" println((pattern1 findAllIn str1).mkString(",")) } }
附件