第一章 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; (执行时完成)

image.png
image.png

第三章 运算符

  • 位运算符面试题:
    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
  1. HashMap扩容时,为什么是容量的2倍?
    答:通过原理,HashMap底层的数据容量必须为2的N次方,为了能保证这一点,所以扩容后的容量也应该是2的N次方


  • 在Scala中其实是没有运算符的,所有运算符都是方法
    scala是完全面向对象的语言,所以数字其实也是一个数值类型的对象
    当调用对象的方法时,点.可以省略
    如果函数参数只有一个,或者没有参数,()可以省略
    Scala对一些基础的运算进行了简化,如果方法采用了特殊符号,那么点“.”可以省略

    1. object ScalaOper {
    2. def main(args: Array[String]): Unit = {
    3. val i : Int = 10
    4. val j : Int = i.+(10)
    5. val k : Int = j +(20)
    6. val m : Int = k + 30
    7. println(m)
    8. println(i.toString()) // 10
    9. println(i toString()) // 10
    10. println(i toString) // 10
    11. }
    12. }
      原则上来讲,方法名中含有字母的不省略;方法名中使用了特殊字符的省略
    
  • 以下代码执行结果如何?

    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扩容image.png
  • 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线程在等待;也因为与对象有关,可以释放锁
  • image.png

  • image.png

  • 父类实现接口,和子类没有任何关系
    子类实现接口,和父类也没有任何关系
    接口的实现,和类的上下级没有任何关系,只和当前类有关系,所以接口和当前实现的类层级相同
    image.png

  • image.png

单例对象

Scala中构造方法私有化:

class User private {
     // Scala中构造方法私有化
  // class User private() 或 class User private
}

private class User {
  // 这个不是构造方法私有化,而是表示这是一个私有类 
}

Scala是一个完全面向对象的语言,没有静态语法,那Scala怎么声明单例对象呢?
使用object关键字就可以声明单例对象

object关键字的作用:
顶层的类如果使用object关键字声明,那么编译时会产生两个类:

  1. 本身的类(ClassName)
  2. 单例对象的类(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")
}

第十章 隐式转换

简介

image.png
Scala中ByteInt是对象类型,不是基本数据类型,所以不存在精度的概念
如果两个类型之间能够进行转换而不发生错误,那么必须两个类型之间有关系(继承关系、实现关系、混入[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...")
          }
      }
    }
    

    隐式机制

    所谓的隐式机制,就是一旦出现编译错误时,编译器会从哪些地方查找对应的隐式转换规则

隐式转换作用域:

  1. 在当前代码的作用域中
  2. 在上一级的作用域中(父类、父类的伴生对象)
  3. 在特质的作用域中(特质、特质的伴生对象)
  4. 当前类所在的包对象的作用域中

其实如果需要使用隐式转换,最直接的方式只需要导入即可

第十一章 泛型

泛型和类型的关系以及区别

  • 泛型和类型无关,不能当成整体来使用
  • 类型用于声明外层对象的类型,泛型主要用于声明内部数据的类型 List<User> // Java ,所以有时也将泛型称为“类型参数”
  • 类声明了泛型,仅仅表用可以设定约束,但是不一定必须有约束
  • 如果没有泛型,那么泛型的类型自动设定为Object
  • 当泛型相同时,考虑类型的父子关系,如果泛型不相同,不会考虑类型的父子关系(Java中)
  • 泛型是不可变的,不会考虑泛型的父子关系(Java中)

    泛型的作用时机

    泛型的作用是在声明之后起作用,泛型是对内部数据的类型起到约束的作用,而不是方法的约束
    泛型在编译后是不存在的,泛型只在编译时起作用,在运行时不起作用,字节码中没有泛型(**泛型的擦除**),泛型主要用于在编译器中进行操作

    泛型的协变和逆变

    Scala中泛型也是不可变的,但是马丁想让泛型和类型当成整体来使用,同时对语法进行了变化:

  • 协变(在泛型的前面增加符号:+):如果ChildUser的子类型,那么将Test[Child]也当成Test[User]的子类型来使用

  • 逆变(在泛型的前面增加符号:-):如果ParentUser的父类型,那么将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(","))
      }
    }
    

    附件

    01-尚硅谷大数据技术之Scala.docx