面向对象编程之基础
Scala的面向对象思想和Java的面向对象思想和概念是一致的。
1. Scala包
基本语法:package 包名 [{}]
Scala 包的三大作用(和Java一样)
- 区分相同名字的类。
- 当类很多时,可以很好的管理类。
- 控制访问范围。
1.1 包的命名
命名规则:只能包含数字、字母、下划线、小圆点"." ,但不能用数字开头,也不要使用关键字。
demo.class.exec1 //错误,因为 class 关键字
demo.12a //错误,数字开头
命名规范: 一般是小写字母+小圆点, 推荐com.公司名.项目名.业务模块名
1.2 包说明(包语句)
Scala有两种包的管理风格,一种方式和Java的包管理风格相同,每个源文件一个包(包名和源文件所在路径不要求必须一致),包名用"."进行分隔以表示包的层级关系. 另一种风格,通过嵌套的风格表示层级关系,如下:
package com{
package jack{
package scala{
}
}
}
第二种风格有以下特点:
- 一个源文件中可以声明多个package。
- 子包中的类可以直接访问父包中的内容,而无需导包。
1.3 包对象
在Scala中可以为每个包定义一个同名的包对象,定义在包对象中的成员,作为其对应包下所有class和object的共享变量,可以被直接访问。
- 若使用Java的包管理风格,如下图所示, IDEA会在在其对应包下的创建package.scala,包对象名与包名保持一致。 文件中
package object oo {
def testPackage():Unit={
println("testPackages.....")
}
}
object OoDemo {
def main(args: Array[String]): Unit = {
testPackage()
}
}
运行结果: 2. 如采用嵌套方式管理包,则包对象可与包定义在同一文件中,但是要保证包对象与包声明在同一作用域中。
package com {
object Outer {
def main(args: Array[String]): Unit = {
println(pageName)
}
}
}
package object com {
val pageName: String = "com包"
}
运行结果:
1.4 导包
- 和Java一样,可以在顶部使用
import
导入,在这个文件中的所有类都可以使用。 - 局部导入:什么时候使用,什么时候导入。在其作用范围内都可以使用。
- 通配符导入, 使用
_
代替Java中的*
, 比如import java.util._
。 - 给类起名, 使用
{}
中指明类名的别名。比如import java.util.{ArrayList=>AL}
。 - 导入相同包的多个类,使用
{}
中指明类名。比如:import java.util.{HashSet, ArrayList}
。 - 屏蔽类,使用
{}
中指明需要屏蔽的类,比如如果我们使用通配符_同时导入java.util和java.sql包,虽然很方便的使用了这两个包中的类,但是Date类使用会出现问题,因为Date类在java.util和java.sql中都存在,需要屏蔽其中一个冲突类,可以导入声明:import java.sql.{Date =>_, _}
。 - 导入包的绝对路径, 使用
_root_
表示顶级路径。比如:new _root_.java.util.HashMap
。
import规则: 以当前包为基准,导入指定包中的类,如果找不到,再从顶级包中查找。如下示例代码:
object OoDemo2 {
def main(args: Array[String]): Unit = {
// 打印出来不是java.util.ArrayList中类
println(new java.util.ArrayList())
import _root_.java.util.ArrayList
println(new ArrayList())
}
}
package java {
package util {
class ArrayList {
}
}
}
运行结果: 8. Scala 中的三个默认导入,无需手动导入:
import java.lang._
import scala._
import scala.Predef._
println()方法可以直接使用,是由于默认导入Predef对象,类似静态导入,被object修饰的对象就是模拟静态导入语法。
def main(args: Array[String]): Unit = {
val user1 = new User
user1.name = "jiebaba"
import user1._ // 导入对象所有方法
test(33)
}
class User() {
var name = "jack"
def test(age: Int) {
println(s"${name}, 2222")
}
}
运行结果:
2. 类和对象
类:可以看成一个模板;
对象:表示具体的事物
2.1 定义类
[修饰符] class 类名 {
类体
}
Scala中声明的类都是public的,类并不需要声明为public。一个Scala源文件可以包含多个类。
2.2 属性
属性是类的一个组成部分。
基本语法: [修饰符] var|val 属性名称 [:类型] = 属性值
属性必须明确初始化,使用下划线_
表示由系统进行默认初始化, 默认初始化不能省略类型声明。
val修饰的属性不能修改,原因是在编译的字节码中,会给对应的属性加上final关键字。
属性编译后,会自动生成private修饰符在属性上面。
属性编译后,还会自动生成属性公共的set/get方法,但是名字不是以set,get方法开头。因此属性可以被访问修改。
当访问属性时,等同于调用属性的get方法,当给属性赋值时,等同于调用对象属性的set方法。
提示
- 不推荐val变量使用
_
提供系统默认值,因为val修饰的变量不能修改。 - 属性若加上@BeanPropetry注解,可以自动生成规范的setXxx/getXxx方法,使其方便于其他框架集成。
- Scala并不推荐@BeanPropetry修饰的属性声明的时候设为private, 因为生成的get/set方法也会变成private修饰,失去了和其他框架集成的意义。
class Person {
var address: String = _ // _表示给属性一个默认值
val age: Int = 33
@BeanProperty var name: String = "jack"
}
object Person {
def main(args: Array[String]): Unit = {
val person = new Person()
person.setName("jiebaba")
// person.age = 18 错误, val修饰的属性不能修改
println(person.address)
person.address = "test"
println(person.address)
}
}
查看编译后文件字节码:
3. 访问权限
分析
clone()
方法被protected修饰,为何user对象无法访问呢❓
方法的提供者: java.lang.Object
方法的调用者: User?
需要注意的是实际调用者应该是从main方法进来,然后执行Object的clone方法,因此是Demo对象✔️,需要说明其中.(点号)并不是调用的意思而是从属关系,比如我们导包:import org.example.oo
,并不是org调用example再调用oo。因此当前代码user.clone()
也不能理解为user调用clone, 而只是说明user和clone有从属关系而已。
问题应该变为为何Demo无法访问clone❓毕竟Demo的父类就是Object, protected修饰可以让子类访问!
原因在于Demo的访问的是user对象的父类Object实例里面的clone方法,而不是自己的父类Object的clone(), 每个实例对象(比如user)都会先实例化父类,直到Object实例化。如图所示,不能访问user的Object对象实例的clone方法: 在Java中访问权限分为:public,private,protected和默认。在Scala中,你可以通过类似的修饰符达到同样的效果。但是使用上有区别。如下图所示,Scala的访问权限:
- Scala中属性和方法的默认访问权限为public,但Scala中无public关键字。
- private为私有权限,只在类的内部和伴生对象中可用。
- protected为受保护权限,Scala中受保护权限比Java中更严格,同类、子类可以访问,同包无法访问。
- private[包名]增加包访问权限,包名下的其他类也可以使用。
class User{
private val name = "jack"
private[oo] val sex = "男"
protected val age = 32
val address = "中国"
def fun1():Unit={
println(this.name) // ✅可以访问
println(this.age) // ✅可以访问
println(this.address) // ✅可以访问
}
}
class Teacher{
def fun1(): Unit = {
val u = new User
println(u.name) //⚠️访问报错
println(u.sex) // ✅可以访问
println(u.age) //⚠️访问报错
println(u.address) // ✅可以访问
}
}
class Admin extends User{
def fun1(): Unit = {
println(this.name) //⚠️访问报错
println(this.age) // ✅可以访问
println(this.address) // ✅可以访问
}
}
package suboo{ // org.example.oo子包中
class Student{
def fun1(): Unit = {
val u = new User
println(u.name) //⚠️访问报错
println(u.age) // ⚠️不同包,访问报错
println(u.address) // ✅可以访问
}
}
}
4. 方法
类的方法其实就是类中声明的函数,方法的生命周期和类、对象有关。方法的访问权限和属性的访问权限一致。但是必须使用对象进行调用。 基本语法:
def 方法名(参数列表) [: 返回值类型] = {
方法体
}
代码示例:
object MethodDemo {
def main(args: Array[String]): Unit = {
val demo = new DemoA()
demo.test1(demo)
}
}
class DemoA{
def test1(demoA: DemoA): Unit = {
println("------------demoA--------------")
}
//重载
def test1(demoB: Object): Unit = {
println("------------DemoB--------------")
}
// 重写
override def toString: String = {
"DemoA"
}
}
运行结果:
扩展
JVM在调用对象的成员方法时,会遵循动态绑定机制:
所谓的动态绑定机制,就是在方法运行时,将方法和当前运行对象的实际内存进行绑定。然后调用。动态绑定机制和属性没有任何关系,属性在哪里声明在哪里使用。
public class MethodDemo1 {
public static void main(String[] args) {
A b = new B();
b.test();
}
}
class A {
public int i = 10;
public void test() {
System.out.println(i + 10);
}
}
class B extends A {
public int i = 20;
public void test() {
System.out.println(i + 20);
}
}
运行结果: 按照动态绑定规则,当前内存中为B对象实例,应该执行B类中的test(), 得到40结果打印。
如果将B类中test方法注释的话,去找B类的test()找不到,就会往上在父类中找, 父类A中有test(),test()执行用到i变量,变量遵循的那里声明那里使用,所以使用的是i=10, 得到10+10为20打印:
5. 创建对象
创建对象在Java中有4种方式:
- 可以通过new关键字创建对象。
- 通过反射构建对象。框架常用,因为框架不知道具体的对象类型,无法new方式创建。
- 通过clone方法在内存中拷贝。
- 通过反序列化方式。 Scala中还可以使用object关键字构建单例对象。
Scala中还可以使用apply()方法, 需要在伴生对象中定义apply(), 编译器可以动态识别,apply()可以省略。
创建对象使用new关键字,格式为:val | var 对象名 [:类型] = new 类型()
object ConstructorDemo {
def main(args: Array[String]): Unit = {
// 使用UserDemo伴生对象创建对象,也就是包装模式应用,里面实际也是调用new创建对象
val userDemo1 = UserDemo.apply()
val userDemo2 = UserDemo() // apply()可以省略
println(userDemo1)
println(userDemo2)
}
}
// UserDemo对象
object UserDemo{
def apply(): UserDemo = new UserDemo()
}
// UserDemo类
class UserDemo {
}
5.1 构造器
和Java一样,Scala构造对象也需要调用构造方法,并且可以有任意多个构造方法。Scala也提供默认构造器,无参公共的。也和Java类似,如果已经编写了构造方法,默认构造方法不再提供。Scala类的构造器包括:主构造器和辅助构造器。
Scala中万物皆函数,声明类等同于声明函数,语法结构如下:
class 类名(形参列表) { // 主构造器
// 类体
def this(形参列表) { // 辅助构造器
}
def this(形参列表) { // 辅助构造器可以有多个...
}
}
主构造器: 用于完成类的初始化。
辅助构造器:在类的初始化后,完成类的辅助功能,比如属性赋值。辅助构造器支持重载的概念。辅助构造器必须或者间接调用主构造器方法。
object Demo1 {
def main(args: Array[String]): Unit = {
val user1 = new User
println("-------------------")
val user2 = new User("jack")
}
class User() {
println("1111")
def this(age: Int) {
this()
println("2222")
}
def this(name: String) {
this(33) // 间接调用主构造器
println("3333")
}
println("4444")
}
}
运行结果:
重要提示
构造器调用其他另外的构造器,要求被调用构造器必须提前声明。
5.2 构造器参数
Scala 类的主构造器函数的形参包括三种类型:未用任何修饰、var修饰、val修饰
- 未用任何修饰符修饰,这个参数就是一个局部变量
- var修饰参数,作为类的成员属性使用,可以修改
- val修饰参数,作为类只读属性使用,不能修改
// 构造器传入参数就是给类中属性初始化, Scala中可以直接将构造器中的参数作为属性来用
def main(args: Array[String]): Unit = {
val user = new User("jack")
println(user.name) // 可以直接作为属性使用
}
class User(var name: String) {
}
运行结果:
6. 继承和多态
和Java一样,Scala中的继承也是单继承,且使用extends关键字。通过继承可以让子类继承父类的属性和方法,从而提高了代码的复用性。
格式: class 子类名 extends 父类名 { 类体 }
多继承有名的【钻石问题】
如果B,C类都继承了A得到了test()方法,编译器是无法知道D类中需要B、C哪个类的test(),因为B、C中test()方法来自A类是完全相同的。
- 如果父类构造方法带有参数,那么需要显式的调用父类的构造方法。
- 构造方法也有访问权限,可以设定为private。
- 如果主构造方法被设定为private,可以通过辅助构造方法或者伴生对象创建对象。
7. 封装
封装就是把抽象出的数据和对数据的操作封装在一起,数据被保护在内部,程序的其它部分只有通过被授权的操作(成员方法),才能对数据进行操作。Java封装操作如下,
- 将属性进行私有化。
- 提供一个公共的set方法,用于对属性赋值。
- 提供一个公共的get方法,用于获取属性的值。
Scala中的属性,底层字节码中实际为private,并通过get方法(obj.field())
和set方法(obj.field_=(value))
对其进行操作。所以Scala并不推荐将属性手动设为private并为其设置public的get和set方法的做法。但由于很多Java框架都利用反射调用getXXX和setXXX方法,有时候为了和这些框架兼容,也会为Scala的属性设置getXXX和setXXX方法(通过@BeanProperty注解实现)。
8. 抽象类
8.1 抽象属性和抽象方法
基本语法:
- 定义抽象类:
abstract class Person{}
//通过abstract关键字标记抽象类。 - 定义抽象属性:
val|var name:String
//一个属性没有初始化,就是抽象属性。 - 定义抽象方法:
def hello():String
//只声明而没有实现的方法,就是抽象方法。
提示
一个类中有抽象方法,那么这个类必是抽象类。
一个抽象类里面不一定有抽象方法。
8.2 继承&重写
因为抽象类中不完整,无法直接创建对象,想要创建对象需要子类继承后补充完整后再构建。
1️⃣ 如果父类为抽象类,那么子类需要将抽象的属性和方法实现,否则子类也需声明为抽象类。
2️⃣ 重写非抽象方法需要用override修饰,重写抽象方法则可以不加override。
3️⃣ 子类中调用父类的方法使用super关键字。
4️⃣ 子类对抽象属性进行实现,父类抽象属性可以用var修饰;子类对非抽象属性重写,父类非抽象属性只支持val类型,而不支持var。
提示
因为var修饰的为可变变量,子类继承之后就可以直接使用,没有必要重写。
abstract class Person {
// java中没有抽象属性概念, 此处用var修饰报错
val name: String
def hello(): Unit
}
class Teacher extends Person {
override val name: String = "teacher"
override def hello(): Unit = {
println("hello teacher")
}
}
反编译字节码文件: 抽象属性的本质:
- 编译器在编译时,抽象属性并不会编译为属性,而是会编译成set/get方法。
- 子类编译时,对应属性编译成set/get方法的实现。
8.3 匿名子类
和Java一样,可以通过包含带有定义或重写的代码块的方式创建一个匿名的子类。
object Demo {
def main(args: Array[String]): Unit = {
val person = new Person {
override val name: String = "teacher"
override def hello(): Unit = println("hello teacher")
}
}
}
abstract class Person {
val name: String
def hello(): Unit
}