1.Scala代码的反编译
/**
* @author huleikai
* @create 2019-05-17 18:41
*/
object TestScala {
def main(args: Array[String]): Unit = {
println("Hello Scala")
}
}
上面的Scala中的伴生对象的代码,我们通过反编译来看看它底层的源码的神秘面纱,如下所示:
/**
* @author huleikai
* @create 2019-05-17 18:43
*/
public class TestScala {
public static void main(String[] paramArrayOfString) {
//TestScala..MODULE$.main(paramArrayOfString);
TestScala$.MODULE$.main(paramArrayOfString);
}
}
//我们可以理解scala在运行时,做了一个包装,我们平时在object伴生对象中的代码其实最终在这儿
final class TestScala$ {
public static final TestScala$ MODULE$;
static {
MODULE$ = new TestScala$();
}
public void main(String[] args) {
System.out.println("Hello Scala");
}
private TestScala$() { MODULE$ = this; }
}
从上面反编译的结果,我们可以看出,其实我们在Scala中的伴生对象,其实底层就是一个被final修饰的类,并且这个类是单例的!
2.Scala 面向对象
Scala语言是面向对象的。Scala语言来自于Java,所以天生就是面向对象的语言,由于历史原因,Java中还存在着非面向对象的内容例如:基本类型(比如 int、float) ,null,静态方法等,但是Scala是纯粹的面向对象语言,即在Scala中,真正的实现一切皆为对象!
2.1 Scala中定义类
在 Scala 中,类并不用声明为 public。在 Scala 文件中,文件名可以不用和类名一致,Scala 源文件中可以包含多个类,所有这些类都具有公有可见性,即这些类默认都是被public修饰的。
类定义语法: class 类名(构造器参数列表) {}
class Student(sid:Int, var sname:String){}
2.2 Scala类中的属性
/**
* @author huleikai
* @create 2019-05-20 12:20
*/
object Student {
def main(args: Array[String]): Unit = {
val student = new Student()
student.name="Tom"
student.age=18
student.gender="男"
printf("学生:\n 姓名: %s 性别: %s 年龄: %d",student.name,student.gender,student.age)
}
}
class Student{
var name:String = _
var age:Int = _
var gender:String = _
}
我们通过反编译工具反编译出Student.class类,其底层内容如下所示:
public class Student {
private String name;
private int age;
private String gender;
public static void main (String[] paramArrayOfString) {
Student..MODULE$.main(paramArrayOfString);
}
public String name() {
return this.name;
}
public void name_$eq(String x$1) {
this.name = x$1;
}
public int age() {
return this.age;
}
public void age_$eq(int x$1) {
this.age = x$1;
}
public String gender() {
return this.gender;
}
public void gender_$eq(String x$1) {
this.gender = x$1;
}
}
通过反编译字节码文件,我们可以看出,当我们在Scala中声明一个class类,底层该类默认的修饰符为public,在Scala类中声明的属性,默认的修饰符则为private私有的,那为什么我们可以在Scala的伴生对象中直接使用对象.属性名的方式进行该属性值得赋值及获取呢?通过反编译文件我们可以清晰的知道,当我们在Scala类中声明一个属性时,Scala的底层会同时帮我们生成该属性类似Java中的getter和setter方法,唯一的区别就是生成的getter和setter方法名与Java中的有些不同,例如:Student中的name属性,Scala底层会帮我们生成一个名为name_$eq(String x$1)和name()的两个方法,这两个方法对应Java中的setName(String name)和getName(),而当我们在object伴生对象中调用该属性时,其实底层调用的是对应的这两个方法。
Scala类属性的声明方式:
class Student {
//用val修饰的变量是只读属性,有getter但没有setter,相当与Java中用final修饰的变量
val id = "100"
//用var修饰的变量既有getter又有setter
var age: Int = 18
//类私有字段,只能在当前类的内部或伴生对象object中使用
private var name: String = "黄渤"
//被private[this]修饰的变量或方法只能在本类中访问使用,它的同名伴生对象都不能访问
private[this] val nickname = "影帝"
}
① 在Scala类中,被val修饰的属性是只读属性,即只有getter方法但没有setter方法,相当于Java中使用final修饰的变量
② 在Scala类中,被var修饰的属性既有getter方法也有setter方法
③ 在Scala类中,被private修饰的类私有属性,只能在当前类的内部或者伴生对象object中使用,其他地方无法访问
④ 在Scala类中,被private[this]修饰的类属性或者方法只能在本类中访问使用,即使它的同名伴生对象都不能访问
⑤ 在Scala的属性上加上@BeanProperty这个注解,会自动生成Java规范的 setXxx/getXxx 方法,并且对原来底层自动生成类似xxx(),xxx_$eq()方法,没有冲突,二者可以共存。
注意:在Scala类中被private修饰的属性,跟普通属性底层都是一样的会生成两个类似getter和setter方法,那么Scala是怎么控制该私有属性的访问权限呢?答案是其底层会将Scala中使用private修饰的变量名采用包名的方法进行声明,例如:我声明的Student.class的包名是com.hlk.scala.Student,那么在Student类中声明的被private修饰的属性名则为:private String com$hlk$scala$Student$$name!
2.3 Scala类构造器
在Scala中,构造器分为主构造器和辅助构造器,主构造器只能有一个,但是辅助构造器可以有一个或者根据参数列表重载多个,下面小亦就带领大家来看看Scala语言中的主构造器和辅助构造器:
① 主构造器
每个类都有主构造器,主构造器的参数直接放置类名后面,与类交织在一起,主构造器会执行类定义中的所有语句,当在创建对象时,需要进行相关初始化操作时,可以将初始化语句放在主构造器{}中,同样也可以在类中添加或重写相关方法。
主构造器中直接被val或者var修饰的变量,就是这个类的成员变量。如果没有被var或者val修饰,那么,就相当于是这个主构造器的一个普通参数,但是这个普通参数如果被 {} 中的某个方法使用了的话,那么就会提升成一个被 private[this] val 修饰的本类私有成员变量。主构造器中的代码无论如何在构造一个对象的时候都被会执行。
class Student(sid:Int, var sname:String, val myid:Int,
private var mynickname:String,private[this] var gender:String){}
注意:现在的这个sid就是一个主构造器的一个普通参数,如果这个参数被{}内的任意一个方法使用,那么此时这个参数就提升为一个只能被本类访问的私有成员变量private[this] val sid,声明为private[this] var gender:String这个参数被var修饰,那么它是一个引用可变的成员变量,然后又被private[this]修饰,所以它只能在本类中被访问使用,就连同名的伴生对象也不可访问。
② 辅助构造器
如果禁用掉了主构造器(即主构造器被 private 修饰),则必须使用辅助构造函数来创建对象。 类的主构造器声明为:
class Queen private(val name: String, prop: Array[String], private var age: Int = 18){}
私有的主构造器只能在同名的伴生对象中调用创建对象,在其他地方都不可以使用主构造器new创建该类的对象,所以此时就需要定义一个辅助构造器,在别的地方进行使用。在辅助构造器中,定义的变量不能指定权限修饰符。在辅助构造器中定义的构造器参数不能被var或val修饰,辅助构造器中的参数列表必须包含主构造器的中声明的构造参数,且不能和主构造器的参数完全一样。
辅助构造函数具有两个特点:
a.辅助构建器的名称为 this,Java 中的辅助构造函数与类名相同,这常常会导致修改类名时出现不少问题,Scala 语言避免了这样的问题
b.调用辅助构造函数时,必须先调用主构造函数或其它已经定义好的构造函数
class Student private(name: String, age: Int, private[this] var gender: String) {
private[this] var salary:Double = _
//用this关键字定义辅助构造器
def this(name: String, age: Int, gender: String, salary: Double) {
//每个辅助构造器必须以主构造器或其他的辅助构造器的调用开始
this(name, age, gender)
this.salary = salary
println("执行辅助构造器")
}
}
3.Scala中的包package
3.1 Scala中包的可见性和访问修饰符的使用
① 当属性访问权限为默认时,从底层看属性是private的,但是因为提供了xxx_$eq()[类似setter]/xxx()[类似getter] 方法,因此从使用效果看是任何地方都可以访问
② 当方法访问权限为默认时,默认为public访问权限
③ private为私有权限,只在类的内部和伴生对象中可用
④ protected为受保护权限,Scala中受保护权限比Java中更严格,只能子类访问,同包无法访问
⑤ 在Scala中没有public关键字,即不能用public显式的修饰属性和方法
⑥ 包访问权限(表示属性有了限制,同时包也有了限制),这点和Java不一样,体现出Scala包使用的灵活性
package com.hlk.scala
class Person {
/**
* 增加包访问权限后
* 1.private同时起作用,不仅同类可以使用
* 2.同时com.hlk.scala中包下其他类也可以使用
*/
private[scala] val name="hello"
//当然,也可以将可见度延展到上层包,private也可以变化,比如protected[hlk], 非常的灵活
private[hlk] val description="zhangsan"
}
3.2 Scala引入包的细节和注意事项
① 在Scala中,import语句可以出现在任何地方,并不仅限于文件顶部,import语句的作用一直延伸到包含该语句的块末尾。这种语法的好处是:在需要时在引入包,缩小import 包的作用范围,提高效率
② Java中如果想要导入包中所有的类,可以通过通配符 * ,Scala中采用下 _
③ 如果不想要某个包中全部的类,而是其中的几个类,可以采用选取器(大括号{})
def testPackage(): Unit = {
import scala.collection.mutable.{HashMap, HashSet}
var map = new HashMap()
var set = new HashSet()
}
④ 如果引入的多个包中含有相同的类,那么可以将不需要的类进行重命名进行区分,这个就是重命名
import java.util.{ HashMap=>JavaHashMap, List}
import scala.collection.mutable._
var map = new HashMap() // 此时的HashMap指向的是Scala中的HashMap
var map1 = new JavaHashMap(); // 此时使用的java中hashMap的别名
⑤ 如果某个冲突的类根本就不会用到,那么这个类可以直接隐藏掉
import java.util.{ HashMap=>_, _} //含义为引入java.util包的所有类,但是忽略HahsMap类
//此时的HashMap指向的是scala中的HashMap, 而且idea工具的提示也不会显示java.util的HashMaple
var map = new HashMap()
4.Scala中的面向对象编程-继承、封装、多态
4.1 Scala中类型检查和转换
在Scala中,要测试某个对象是否属于某个给定的类,可以用isInstanceOf方法,用asInstanceOf方法将引用转换为子类的引用。classOf获取对象的类名。
① classOf[String]就如同Java的 String.class 。
② obj.isInstanceOf[T]就如同Java的obj instanceof T 判断obj是不是T类型。
③ obj.asInstanceOf[T]就如同Java的(T)obj 将obj强转成T类型
//获取对象类型
println(classOf[String])
val s = "zhangsan"
println(s.getClass.getName)
//这种是Java中反射方式得到类型
println(s.isInstanceOf[String])
println(s.asInstanceOf[String]) //将s显示转换成String
var p = new Person2
val e = new Emp
p = e //将子类对象赋给父类
p.name = "xxx"
println(e.name)
p.asInstanceOf[Emp].sayHi()
4.2 Scala中父类的构造器
① 类有一个主构造器和任意数量的辅助构造器,而每个辅助构造器都必须先调用主构造器(也可以是间接调用)
class Person {
var name = "zhangsan"
println("Person...")}
class Emp extends Person {
println("Emp ....")
def this(name : String) {
this // 必须调用主构造器
this.name = name
println("Emp 辅助构造器~")
}
}
② 只有主构造器可以调用父类的构造器。辅助构造器不能直接调用父类的构造器。在Scala的构造器中,你不能调用super(params)
class Person(name: String) { //父类的构造器
}
class Emp (name: String) extends Person(name) { //将子类参数传递给父类构造器,这种写法√
//super(name) (×) 没有这种语法
def this() {
super("abc") // (×)不能在辅助构造器中调用父类的构造器
}
}
4.3 Scala中重写父类属性
在Java中只有方法的重写,没有属性/字段的重写。在Scala中,子类改写父类的字段,我们称为覆写/重写字段。覆写字段需使用 override修饰。
class A {
val age : Int = 10
}
class B extends A {
override val age : Int = 20
}
val obj1 : A = new B()
val obj2 : A = new A()
val obj3 : B = new B()
printf("obj1: %d , obj2: %d, obj3: %d",obj1.age,obj2.age,obj3.age) //obj1: 20 , obj2: 10, obj3: 20
① 一个属性没有初始化,那么这个属性就是抽象属性
② 抽象属性在编译成字节码文件时,属性并不会声明,但是会自动生成抽象方法,所以类必须声明为抽象类
③ 如果是覆写一个父类的抽象属性,那么override 关键字可省略 [原因:父类的抽象属性,生成的是抽象方法,因此就不涉及到方法重写的概念,因此override可省略]
4.4 Scala中的抽象类
抽象类基本语法:
abstract class Person() { // 抽象类
var name: String // 抽象字段, 没有初始化
def printName // 抽象方法, 没有方法体
}
Scala抽象类使用的注意事项和细节讨论:
① 抽象类不能被实例化
② 抽象类不一定要包含abstract方法。也就是说,抽象类可以没有abstract抽象方法
③ 一旦类包含了抽象方法或者抽象属性,则这个类必须声明为abstract
④ 抽象方法不能有主体,不允许使用abstract修饰
⑤ 如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法和抽象属性,除非它自己也声明为abstract类
⑥ 抽象方法和抽象属性不能使用private、final 来修饰,因为这些关键字都是和重写/实现相违背的
⑦ 抽象类中可以有实现的方法
⑧ 子类重写抽象方法不需要override,写上也不会错
⑨ 在子类中覆盖抽象类的抽象方法时,可以指定 override 关键字也可以不指定,但是在子类中重写抽象父类中已实现的非抽象方法时,必须指定override关键字
⑩ 抽象类中声明的抽象字段,子类在继承抽象父类时必须在子类自己的构造器或大括号{}中对抽象字段进行初始化或重写
4.5 Scala 匿名子类
Scala和Java一样,可以通过包含带有定义或重写的代码块的方式创建一个匿名的子类
abstract class Monster {
var name: String
def cry()
}
var monster = new Monster {
override var name: String = "牛魔王"
override def cry (): Unit = {
println ("牛魔王哼哼叫唤..")
}
}
4.6 Scala 继承
Scala中的继承和Java中的基本一致,关键词也是extends, 在子类覆盖父类方法之后,如果我们在子类中就是要调用父类的被覆盖的方法,就可以使用 super 关键字,显式地指定要调用父类的方法。
5.Scala静态概念——伴生对象
Java中有静态属性也有静态方法以及静态类,那Scala中有静态类或者静态属性方法吗?在某些应用场景下,我们可能不需要创建对象,而是想直接调用方法,但是 Scala 语言并不支持静态成员,没有静态方法和静态字段,Scala 通过单例对象 object 来解决该问题,因为在object对象中声明的所有成员都是静态成员,可以直接通过object名.成员名的方式调用,跟Java中类的静态成员使用一样,需要注意的是,object中默认声明的成员变量或方法的权限修饰符为public,可以在其他的非同名的class或非同名的object中通过object.成员名方式调用,但是权限修饰符为private或者protected的成员,在其他非同名的class或非同名的object中不能访问使用,但是在该object同名的伴生类中可以使用该object中声明为任何权限修饰符的成员。 因为不需要去 new,所以object本身就是一个单例对象。
伴生对象的小结:
① Scala中伴生对象采用object关键字声明,伴生对象中声明的全是 "静态"内容,可以通过伴生对象名称直接调用
② 伴生对象对应的类称之为伴生类,伴生对象的名称应该和伴生类名一致
③ 伴生对象中的属性和方法都可以通过伴生对象名(类名)直接调用访问
④ 从语法角度来讲,所谓的伴生对象其实就是类的静态方法和成员的集合
⑤ 从技术角度来讲,Scala还是没有生成静态的内容,object伴生对象中声明的属性或方法,底层仍然是一个非static静态的属性或方法,只不过是将伴生对象生成了一个新的类进行了封装,实现属性和方法的调用
⑥ 从底层原理看,伴生对象实现静态特性是依赖于 public static final MODULE$ 实现的
⑦ 伴生对象的声明应该和伴生类的声明在同一个源码文件中(如果不在同一个文件中会运行错误),但是如果没有伴生类,也就没有所谓的伴生对象了,所以放在哪里就无所谓了
⑧ 如果 class A 独立存在,那么A就是一个类, 如果 object A 独立存在,那么A就是一个"静态"性质的对象[即类对象], 在 object A中声明的属性和方法可以通过 A.属性 和 A.方法 来实现调用
⑨ 在Scala中,如果声明一个Person.class伴生类,再声明一个同名的伴生对象object Person,底层会生成Person.class和Person$.class两个文件,其中Person$.class就是object Person伴生对象生成的字节码文件,如果只声明一个名为Person的object伴生对象,没有伴生类,底层也会生成Person.class和Person$.class两个文件
伴生对象-apply方法:
在伴生对象中定义apply方法,可以实现:val 对象名 = 类名(参数...) 方式来创建对象实例:
在Scala集合和数组中,可以通过 val intList = List(1,2,3) 这种方式创建初始化一个列表对象,其实它相当于调用 val in = List.apply(1,2,3)方法,只不过 val intList = List(1,2,3) 这种创建方式更简洁一点,但我们必须明确的是这种创建方式仍然避免不了 new,它后面的实现机制仍然是 new 的方式,只不过我们自己在使用的时候可以省去 new 的操作。通常我们会在类的伴生对象中定义 apply 方法,当遇到【类名(参数 1,...参数 n)】时 apply 方法会自动被调用。总之apply方法主要是用来方便用户去初始化一些对象时更加方便简洁。
注:如果博主的博客对你有所帮助,记得帮忙点个赞,因为整理不易啊!