Scala 第三篇 OOP篇
上接: Scala 第二篇 算子篇
前序
1、Scala 为纯粹OOP
1.1、不支持基本类型:一切皆为对象 Byte, Int,...
1.2、不支持静态关键字:static
1.3、支持类型推断,和类型预定,动静结合
一、类
关键字:class
创建对象:new
内含:成员变量和方法
区别:
1、默认访问修饰符为 public,也支持 private 和 protected
2、没有构造方法,通过构造参数列表实现对象创建
1、修饰符
修饰符 | 类(class) | 伴生对象(object) | 子类(subclass) | 同包(package) | 全局(world) |
---|---|---|---|---|---|
public(default) | Y | Y | Y | Y | Y |
protected | Y | Y | Y | N | N |
private | Y | Y | N | N | N |
2、创建类示例
-
创建类
scala// 类自身:就是类的【主】构造器 class Student(name:String, age:Int) { // 属性 private var _name:String = name private var _age:Int = age // 辅助构造器:基于主构造器的重载 def this() = this("Unknown", 0) def this(name:String) = this(name, 18) // 方法 def play(): Unit = { println("play computer game") } def getName:String = _name def getAge:Int = _age def setName(name: String):Unit = _name = name def setAge(age: Int):Unit = _age = age override def toString(): String = s"Student{name=${_name}, age=${_age}}" }
-
创建对象与方法调用
scalaval stu1 = new Student("张三", 20) val stu2 = new Student() val stu3 = new Student("leaf") stu1.play() // play computer game stu2.setName("aaa") stu2.setAge(19) println(stu2) // Student{name=aaa, age=19} println(stu3.getAge) // 18
3、类的继承
-
类的继承
scala// 继承关键字:extents, 继承Student类 class EliteStudent(name: String, age: Int, score: Int) extends Student(name, age){ // 增添属性 private var _score: Int = score // 重载方法 override def play(): Unit = { println("Do homework") } override def toString(): String = s"${super.toString()}, score: ${score}" }
-
调用
scalaval eliteStudent = new EliteStudent("李四", 12, 100) eliteStudent.play() // 输出:Do homework println(eliteStudent) // Student{name=李四,age=12}, score: 100
二、抽象类
关键字:abstract
- 抽象类中可以有抽象方法(没有方法体的方法即抽象方法)
- 无法实例化
- 使用 abstract 关键字修饰
- 子类重写父类【抽象】方法可以省略 override 关键字,但不推荐省略
- 子类重写父类【非抽象】方法必须写 override 关键字
示例代码
scala
// 抽象类
abstract class Animal {
def eat(): Unit // 抽象方法
def play(): Unit
}
class Dog extends Animal {
override def eat(): Unit = {
println("大口吃肉")
}
override def play(): Unit = {
println("抓蝴蝶")
}
}
class Cat extends Animal{
override def eat(): Unit = {
println("优雅的吃")
}
override def play(): Unit = {
println("滚毛球")
}
}
三、单例对象
代替 Java 中的 static 关键字
1、关键字:object
2、可以包含属性和方法,且可以通过单例对象名直接调用
3、采取惰性模式,第一次被访问时创建
4、无构造器,且不能 new
5、程序入口方法必须定义在单例对象中
6、同一个文件中同名的类和单例对象形成绑定关系,并称之为伴生类和伴生对象
-
伴生类,伴生对象(单例对象)的创建
scala// 伴生类:与伴生对象具有相同名称的类被称为伴生类。 class Student(name:String, age:Int) { private var _name:String = name private var _age:Int = age def this() = this("Unknown", 0) def this(name:String) = this(name,18) def getName:String = _name def getAge:Int = _age def setName(name: String):Unit = _name = name def setAge(age: Int):Unit = _age = age def play(): Unit = { println("play computer game") } override def toString(): String = s"Student{name=${_name},age=${_age}}" } // 伴生对象:伴生对象是一个单例对象,它与一个类具有相同的名称。 // 通常用于存放与该类相关的静态方法或字段。 object Student{ var products:Array[String] = Array("BigData","Cloud") // apply方法常用于作为创建类实例的工厂方法,省去了使用new关键字的麻烦 def apply(name: String, age: Int): Student = new Student(name, age) def add(a:Int,b:Int) = a + b }
-
调用
scalaStudent.products.foreach(e => print(e + " ")) // 输出静态属性:BigData Cloud val stu = Student("张三", 12) // 省略 new print("\n" + Student.add(1, 5)) // 调用静态方法
四、特质
类似 java 的接口 (interface)
关键字:trait
1、包含字段、方法,亦可包含字段和方法的实现
2、类、单例对象、普通对象都可以扩展特质
3、没有构造器,不能实例化
4、单根继承,借助 with 实现多混入
scala
// 定义抽象类 Animal
abstract class Animal {
var _name: String
}
// 定义特质 ByFoot
trait ByFoot {
def eat(): Unit
def play(): Unit
}
// 定义类 Cat,实现了 ByFoot 特质
class Cat extends ByFoot {
override def eat(): Unit = println("吃西瓜")
override def play(): Unit = println("抓老鼠")
}
// 定义类 Dog,继承了 Animal 抽象类并实现了 ByFoot 特质
class Dog(name: String) extends Animal with ByFoot {
override var _name: String = name
override def eat(): Unit = println("大口吃肉")
override def play(): Unit = println("抓蝴蝶")
}
1、动态混入
-
特质类的定义
scalatrait Animal { val name:String def cry():Unit } trait ByFoot { def jog():Unit def run():Unit } // 动态强制混入特质:只能定义一个强制混入特质,且必须位于类内首行 // self 是 this 的别名 class Penguin { self: Animal => // 强制混入特质语法 val brand: String = "企鹅" } // 动态非强制混入特质 with,支持多混入 class Bear(nickName: String) { val _name: String = nickName }
-
调用,动态混入
scala// 复合类型 Penguin with Animal val penguin: Penguin with Animal = new Penguin() with Animal { override val name: String = "阿童木" override def cry(): Unit = println(s"$brand $name is crying") } penguin.cry() val bear: Bear = new Bear("熊大") with Animal with ByFoot { override val name: String = "狗熊" override def cry(): Unit = println(s"$name ${_name} is crying") override def jog(): Unit = println(s"$name ${_name} is jogging") override def run(): Unit = println(s"$name ${_name} is running") }
2、抽象类 VS 特质
一般【优先使用特质】:
1、抽象类在于多类公共属性和行为的抽象,重点在于封装思想,本质为类,单继承,不支持多混入
2、接口在于一类事物的属性和行为标准定义,重点在于多态思想,支持多混入,动态混入
若需要带参构造,只能使用抽象类
五、内部类
内部类是定义在另一个类内部的类。内部类可以访问外部类的成员,包括私有成员。
内部类的主要优点之一是它们可以更轻松地访问外部类的状态,而不需要显式地传递引用
Java中内部类是【外部类的成员】:
InClass ic = new OutClass.InClass()
Scala中内部类是【外部类对象的成员】:
val oc = new OutClass();
val ic = new oc.InClass();
-
创建
scalaclass OutClass(name:String,age:Int,gender:String,school:String,major:String) { class InnerClass(age:Int,gender:String){ private var _age:Int = age private var _gender:String = gender def getAge = _age def getGender = _gender def setAge(age:Int) = _age = age def setGender(gender:String) = _gender = gender } private val _name:String = name private var _in:InnerClass = new InnerClass(age, gender) var _in2:OutClass.Inner2Class = new OutClass.Inner2Class(school, major) def setAge(age:Int) = _in.setAge(age) def setGender(gender:String) = _in.setGender(gender) def setIn(in:InnerClass) = _in = in def setIn2(in2:OutClass.Inner2Class) = _in2 = in2 override def toString: String = s"${_name},${_in.getAge},${_in.getGender},${_in2._school},${_in2._major}" } object OutClass{ class Inner2Class(school:String,major:String){ val _school:String = school val _major:String = major } }
调用
scalaval oc = new OutClass("henry",22,"male","xx","通信") oc.setAge(33) oc.setGender("female") println(oc) val in = new oc.InnerClass(30, "female") // 外部类对象.内部类(...) oc.setIn(in) val in2 = new OutClass.Inner2Class("xxx","人工智能") oc.setIn2(in2)
六、样例类
描述【不可变值】的对象
样例类构造参数默认声明为 val,自动生成 getter
样例类的构造参数若声明为 var,自动生成 getter & setter
样例类自动生成伴生对象
样例类自动实现的其他方法:toString,copy,equals,hashCode
样例类伴生对象实现的方法:apply, unapply(用于模式匹配)
普通类的模式匹配案例
scala
case class Student(name:String, age:Int) // 构造参数默认 val
case class Point(var x:Int,var y:Int) // var 需要显式声明
scala
// val obj: Any = Student("张三", 18)
val obj: Any = Point(10, 20)
val info = obj match {
case Student(_, 22) => "年龄22"
case Student(name, _) if name.startsWith("张") => "姓张"
case Point(a, _) => s"$a"
}
println(info)
scala
// 追加伴生对象并实现 apply & unapply
object Point{
def apply(x: Int, y: Int): Point = new Point(x, y)
def unapply(arg: Point): Option[(Int, Int)] = Some((arg._x,arg._y))
}
七、枚举
单例对象通过继承 Enumeration 实现枚举创建,简单易用,但不可扩展
通常用于定义一个有限取值范围的常量
scala
object WeekDay extends Enumeration {
val MON = Value(0)
val TUE = Value(1)
val WEN = Value(2)
val THU = Value(3)
val FRI = Value(4)
val SAT = Value(5)
val SUN = Value(6)
}
scala
val wd = WeekDay.WEN
val info = wd match {
case WeekDay.MON => "星期一"
case WeekDay.TUE => "星期二"
case WeekDay.WEN => "星期三"
case _ => "不需要"
}
八、泛型
类型参数化,主要用于集合
不同于 Java 泛型被定义在 [] 中
-
测试类
scalaclass GrandFather(name:String) { val _name:String = name override def toString: String = _name } object GrandFather{ def apply(name: String): GrandFather = new GrandFather(name) } class Father(name:String) extends GrandFather(name:String) {} object Father{ def apply(name: String): Father = new Father(name) } class Son(name:String) extends Father(name:String) {} object Son{ def apply(name: String): Son = new Son(name) } class GrandSon(name:String) extends Son(name:String){} object GrandSon{ def apply(name: String): GrandSon = new GrandSon(name) }
-
泛型边界定义
scala// 上边界:T<:A 泛型为某个类型的子类 // 下边界:T>:A 泛型为某个类型的父类 class MyArray[T <: Father](items:T*) { def join(sep:String) = items.mkString(sep) } // Type GrandFather does not conform to 【 upper bound 】 Father of type parameter T val arr:MyArray[GrandFather] = ... class MyArray[T >: Son](items:T*) { def join(sep:String) = items.mkString(sep) } // Type GrandSon does not conform to 【 lower bound 】 Son of type parameter T val arr:MyArray[GrandSon] = ...
-
型变:多态
协变:[+T] 若A是B的子类,则 C[A]为C[B]的子类
逆变:[-T] 若A是B的子类,则 C[B]为C[A]的子类
不变:[T] 默认
scalaclass MyArray[+T](items:T*) { def join(sep:String) = items.mkString(sep) } // Father 是 Son 的父类,则 MyArray[Father] 就是 MyArray[Son] 的父类 val arr:MyArray[Father] = new MyArray[Son](Son("henry"),Son("ariel")) class MyArray[-T](items:T*) { def join(sep:String) = items.mkString(sep) } // Father 是 Son 的子类,则 MyArray[Son] 就是 MyArray[Father] 的子类 val arr:MyArray[Son] = new MyArray[Father](Son("henry"),Son("ariel")) class MyArray[T](items:T*) { def join(sep:String) = items.mkString(sep) } // 所有泛型都必须为 Son val arr:MyArray[Son] = new MyArray[Son](Son("henry"),Son("ariel"))
九、隐式类
用implicit关键字修饰的类,扩展其主构造器唯一参数类型的功能
只能在类、Trait、对象(单例对象、包对象)内部定义
构造器只能携带一个非隐式参数
隐式类不能是 case class
在同一作用域内,不能有任何方法、成员或对象与隐式类同名
隐式类必须有主构造器且只有一个参数
-
隐式参数:隐式传入
场景:多个函数共享同一个参数,选择柯里化,将最后一个列表设计为该共享参数的唯一参数,并将该参数设置为 implicit
scalaimplicit order:Ordering[Int] = Ordering.Int.reverse val sorted = Array(8,1,3,2,5).sorted(implicit order:Ording[Int])
-
隐式函数:隐式类型转换
scalaimplicit def strToInt(str:String) = str.toInt val a:Int = "12"
-
隐式类:扩展
scala// 字符串的方法扩展,而在 Java 中 String 是final的,无法扩展 implicit class StrExt(str:String){ def incr() = str.map(c=>(c+1).toChar) def isEmail = str.matches("\\w+@[a-z0-9]{2,10}\\.(com(.cn)?|cn|edu|org)") }
scalaval a:String = "12665473@qq.com" val incr: String = a.incr val isEmail: Boolean = a.isEmail
十、包与包对象
包命名规则:字母、数字、下划线、点,不能以数字开头,在【一个类文件中可以定义多个并列的包】
导包的不同方式
scalaimport com.org.Person // 方便使用类 Person import com.org._ // 方便使用 com.kgc 包中的所有类 import com.org.Person._ // 方便使用类 Person 中的所有属性和方法 import com.org.{Person=>PS,Book} // 只导入包中 Person和Book,并将Person重命名为PS
不同于Java:import 导包语句可以出现在任意地方
可以导入包、类、类成员
单个文件多包结构:资源按包名语义分类存放,方便管理和使用
测试样例,import 导包语句可以出现在任意地方
scala
package cha03{
import cha03.util.Sorts // 导包
object PackageTest {
def main(args: Array[String]): Unit = {
val array: Array[Int] = Array(3, 1, 5, 4, 2)
Sorts.insertSort(array)
array.foreach(println)
}
}
}
package cha03.util{
object Sorts{
def insertSort(array: Array[Int]): Unit ={
import scala.util.control.Breaks._ // 导包
for(i<- 1 until array.length){
val t = array(i)
var j = i-1
breakable({
while (j>=0){
if(array(j)>t){
array(j+1) = array(j)
}else{
break()
}
j-=1
}
})
array(j+1) = t
}
}
}
}
包对象
包中可以包含:类、对象、特质...
包对象可以包含:除了类、对象、特质外,还可以包含变量和方法
scala
package test {
// 导包(包在下面定义)
import test.util.Constants._ // 导入 test.util.Constants 包对象中的所有成员,包括 PI 和 getQuarter 方法
import test.util.Constants.{DataFormat => DF} // 导入 test.util.Constants 包对象中的 DataFormat 类,并将其重命名为 DF
object PackageTest {
def main(args: Array[String]): Unit = {
println(PI * 2 * 2)
println(getQuarter(5))
val format: DF = DF(2024, 3, 29)
println(format.stdYMD())
println(format.stdFull())
println(format.timestamp())
}
}
}
package test.util {
import java.util.Calendar
// 包对象
package object Constants {
// 变量
val PI: Float = 3.14f
// 方法
def getQuarter(month: Int) = (month - 1) / 3 + 1
// 类
class DataFormat(
year: Int, month: Int, day: Int,
hour: Int, minute: Int, second: Int,
millis: Int) {
private var _year: Int = year
private var _month: Int = month
private var _day: Int = day
private var _hour: Int = hour
private var _minute: Int = minute
private var _second: Int = second
private var _millis: Int = millis
def this(year: Int, month: Int, day: Int) {
this(year, month, day, 0, 0, 0, 0)
}
def stdYMD(): String = s"${_year}-${_month}-${_day}"
def stdFull(): String = s"${_year}-${_month}-${_day} ${_hour}:${_minute}:${_second}.${_millis}"
def timestamp(): Long = {
val cld = Calendar.getInstance()
cld.set(_year, _month, _day, _hour, _minute, _second)
cld.set(Calendar.MILLISECOND, 555)
cld.getTimeInMillis
}
}
object DataFormat {
def apply(year: Int, month: Int, day: Int,
hour: Int, minute: Int, second: Int, millis: Int): DataFormat
= new DataFormat(year, month, day, hour, minute, second, millis)
def apply(year: Int, month: Int, day: Int): DataFormat
= new DataFormat(year, month, day)
}
}
}
练习
1、练习一
需求说明
- 假设类Book有属性 title(标题) 和 author(作者,多个)
- 实现Book类,同时使用主构造器与辅助构造器
- 实现Book的伴生对象,使用伴生对象创建Book实例
- 创建books,使用List[Book]初始化5个以上Book实例
- 找出books中书名包含"xxx"的书,并打印书名
- 找出books中作者(多个,一个含有即可) 名以"xxx"开头的书,并打印书名
-
类和伴生对象
scala// 主构造器 class Book(title:String, authors:Array[String]) { private val _title:String = title private val _authors:Array[String] = authors // 判断标题是否含有sub子串 def titleContains(sub:String): Boolean = _title.contains(sub) // 多个作者姓名中是否有以name为开头的姓名 def authorContains(name:String): Boolean = _authors.count(_.startsWith(name))>0 // 辅助构造器 def this(title:String, author:String) = this(title, Array(author)) def getTitle: String = _title def getAuthors: Array[String] = _authors override def toString: String = s"$title\t${_authors.mkString("、")}" } // 伴生对象 object Book{ def apply(title: String, authors: Array[String]): Book = new Book(title, authors) def apply(title: String, author: String): Book = new Book(title, author) }
-
创建Book实例,并查找
scaladef main(args: Array[String]): Unit = { // 使用伴生对象创建Book实例(没有new),初始化5个Book实例 val list = List( Book("武侠:最强小保安",Array("张三","李四","王五")), Book("都市:上门赘婿",Array("阿强","洞冥福","花花")), Book("武侠:翔龙会",Array("阿庆嫂","黄世仁")), Book("都市:缘起",Array("徐世明","张丘月")), Book("武侠:小李飞刀",Array("王栋","李宏","张明")), ) // 找到标题中含有 "都市" 的书 打印书名 list.collect({ case book if book.titleContains("都市") => book.getTitle }).foreach(println) // 找到作者有以 "阿" 开头的书 打印书名 list.collect({ case book if book.authorContains("阿") => book.getTitle }).foreach(println) // 找到标题中含有 "武侠" 的书 打印书名 list .filter(_.titleContains("武侠")) .foreach(book=>println(book.getTitle)) }
2、练习二
需求说明(续练习一)
- 现在Book拥有电子版本(EBook类继承Book),可以在多终端上播放
- 属性:作者author:String,书名title:String,类型bookType:String,内容chapters:Array[String]
- 方法:简介 resume():Unit
- 定义特质,方法:play()
- 使EBook动态混入该特质,实现play()方法
-
EBook(电子书) 类和特质创建,与实现
scalaimport scala.collection.mutable.ArrayBuffer import scala.io.StdIn.readLine // 控制台输入 import scala.util.control.Breaks.breakable // 循环控制 import scala.util.control.Breaks.break trait EAction{ // 阅读第 chapterNo 章节 def play(chapterNo: Int): Boolean // 阅读全部,默认从第一章开始阅读 def play(): Unit } class EBook(title:String, authors:Array[String], bookType: String) extends Book(title, authors) with EAction { private val _bookType: String = bookType private val _chapters: ArrayBuffer[String] = ArrayBuffer() // 只有一个作者时的辅助构造器 def this(title:String, authors: String, bookType: String) = this(title, Array(authors), bookType) private def chapterCount = _chapters.size // 添加章节 def addChapter(chapter: String): Unit = { _chapters.append(chapter) } // 简介 readme def readme: String = s"类型: ${_bookType}, 标题: ${getTitle}, 作者: ${getAuthors.mkString("、")}, 章节数: ${chapterCount}" // 去除段落前换行,并且每行30个字符输出 private def showChapter(chapter: String): Unit = { val pat = "[^\n]{1,30}".r val lines = pat.findAllIn(chapter) lines.foreach(println) } override def play(chapterNo: Int): Boolean = { if(chapterNo >= 1 && chapterNo <= chapterCount){ showChapter(_chapters(chapterNo - 1)) true }else{ println("无此章节") false } } override def play(): Unit = { breakable{ var characterNo = 1 while (play(characterNo)) { print("是否继续阅读(y|Y): ") characterNo += 1 if(!readLine().matches("y|Y")){ break() } } } } } // 伴生对象,辅助创建对象 object EBook{ def apply(title: String, authors: String, bookType: String): EBook = new EBook(title, authors, bookType) }
-
调用
scaladef main(args: Array[String]): Unit = { val chapters = Array( "我是一个失败者,几乎不怎么注意阳光灿烂还是不灿烂,因为没有时间。\n\n\n \n我的父母没法给我提供支持,我的学历也不高,孤身一人在城市里寻找着未来。\n\n\n \n我找了很多份工作,但都没能被雇佣,可能是没谁喜欢一个不擅长说话,不爱交流,也未表现出足够能力的人。\n\n\n \n我有整整三天只吃了两个面包,饥饿让我在夜里无法入睡,幸运的是,我提前交了一個月房租,还能继续住在那个黑暗的地下室里,不用去外面承受冬季那异常寒冷的风。\n\n\n \n终于,我找到了一份工作,在医院守夜,为停尸房守夜。\n\n\n \n医院的夜晚比我想象得还要冷,走廊的壁灯没有点亮,到处都很昏暗,只能靠房间内渗透出去的那一点点光芒帮我看见脚下。\n\n\n \n那里的气味很难闻,时不时有死者被塞在装尸袋里送来,我们配合着帮他搬进停尸房内。\n\n\n \n这不是一份很好的工作,但至少能让我买得起面包,夜晚的空闲时间也可以用来学习,毕竟没什么人愿意到停尸房来,除非有尸体需要送来或者运走焚烧,当然,我还没有足够的钱购买书籍,目前也看不到攒下钱的希望。\n\n\n \n我得感谢我的前任同事,如果不是他突然离职,我可能连这样一份工作都没法获得。\n\n\n \n我梦想着可以轮换负责白天,现在总是太阳出来时睡觉,夜晚来临后起床,让我的身体变得有点虚弱,我的脑袋偶尔也会抽痛。\n\n\n \n有一天,搬工送来了一具新的尸体。\n\n\n \n听别人讲,这是我那位突然离职的前同事。\n\n\n \n我对他有点好奇,在所有人离开后,抽出柜子,悄悄打开了装尸袋。\n\n\n \n他是个老头,脸又青又白,到处都是皱纹,在非常暗的灯光下显得很吓人。\n\n\n \n他的头发不多,大部分都白了,衣服全部被脱掉,连一块布料都没有给他剩下。\n\n\n \n对于这种没有家人的死者,搬工们肯定不会放过额外赚一笔的机会。\n\n\n \n我看到他的胸口有一个奇怪的印记,青黑色的,具体样子我没法描述,当时的灯光实在是太暗了。\n\n\n \n我伸手触碰了下那个印记,没什么特别。\n\n\n \n看着这位前同事,我在想,如果我一直这么下去,等到老了,是不是会和他一样......\n\n\n \n我对他说,明天我会陪他去火葬场,亲自把他的骨灰带到最近的免费公墓,免得那些负责这些事的人嫌麻烦,随便找条河找个荒地就扔了。\n\n\n \n这会牺牲我一个上午的睡眠,但还好,马上就是周日了,可以补回来。\n\n\n \n说完那句话,我弄好装尸袋,重新把它塞进了柜子。", "不好意思,我不知道会是这样的情况。莱恩很有礼貌地对卢米安道了声谦。\n\n\n \n卢米安嘿嘿笑道:\n\n\n \n这是不是值又一杯'绿仙女'?\n\n\n \n不等莱恩回答,他转移了话题:\n\n\n \n外乡人,你们来科尔杜做什么,收购羊毛、皮革?\n\n\n \n科尔杜有不少居民以牧羊为生。\n\n\n \n莱恩无声松了口气,抓住这个契机道:\n\n\n \n我们来拜访你们村'永恒烈阳'教会的本堂神甫纪尧姆.贝内,可他既不在家里,也不在教堂。\n\n\n \n不用说是哪个教会的,科尔杜只有一家教会。喝了莱恩免费苦艾酒的皮埃尔好心提醒了一句。\n\n\n \n吧台周围的其他本地人各自喝着酒,没谁回答莱恩的问题,似乎那个名字代表着某种禁忌或者权威,不能随便谈论。\n\n\n \n卢米安喝了口酒,思索了几秒道:\n\n\n \n我大概能猜到本堂神甫在哪里,需要我带你们去吗?\n\n\n \n那就麻烦你了。莉雅没有客气。\n\n\n \n莱恩跟着点了点头:\n\n\n \n等你喝完这一杯。\n\n\n \n好的。卢米安端起酒杯,咕噜咕噜喝完了淡绿色的液体。\n\n\n \n他把杯子一放,站了起来:\n\n\n \n走吧。\n\n\n \n真是太感谢了。莱恩边招呼瓦伦泰和莉雅起身,边向卢米安致意。\n\n\n \n卢米安脸上露出了笑容:\n\n\n \n没关系,你们听了我的故事,我又喝了伱们的酒,大家算是朋友了,对吧?\n\n\n \n是的。莱恩轻轻点头。\n\n\n \n卢米安脸上的笑容愈发灿烂,伸出双臂,似乎要给对方一個拥抱。\n\n\n \n与此同时,他热忱说道:" ) // 创建ebook对象 val ebook = EBook("宿命之环", "爱潜水的乌贼", "玄幻") chapters.foreach(chapter => ebook.addChapter(chapter)) // 给书添加章节 println(ebook.readme) // 输出简介 // ebook.play(1) // 阅读第一章节 ebook.play() // 阅读全部 }