Live2D

Kotlin基础

1.kotlin的简介

Kotlin是由JetBrains开发的针对JVM、Android和浏览器的静态编程语言,目前,在Apache组织的许可下已经开源。使用Kotlin,开发者可以很方便地开发移动Android应用、服务器程序和JavaScript程序。Kotlin可以将代码编译成Java字节码,也可以编译成JavaScript,方便在没有JVM的设备上运行。

Kotlin来源于一个岛屿的名字,全称是Kotlin Island,是英语【科特林岛】之意,这个小岛位于俄罗斯的圣彼得堡附近。之所以要命名为Kotlin,是因为Kotlin的主要开发工作就是由位于圣彼得堡分公司团队完成的。

2.kotlin的基础语法

参考kotlin官方文档

函数定义

函数定义使用关键字fun,参数格式为: 参数:类型

fun sum2(x: Int, y: Int): Int {
    return x + y;
}

表达式作为函数体,返回值类型自动推断

fun sum(x: Int, y: Int) = x + y;

public 方法必须明确返回值类型,否则会报错

public fun sum3(x: Int, y: Int): Int = x + y;

无返回值的函数(Unit类似于java中的void,但是不等同于void)

fun sum4(x: Int, y: Int): Unit {
    println(x + y)
}

在定义方法的时候给定参数默认值

fun defaultValue(a: Int = 10) { // 定义参数的时候给定默认值
    println(a)
}
可变长参数函数

函数的变长参数可以用 vararg 关键字进行标识, 此时参数是以数组的形式展示

// 打印参数
fun vararg(vararg variable: String) {
    for (vars in variable) {
        println(vars)
    }
}

main函数在1.3版本之后,定义可以不带参数

image

lamdba表达式
val list: MutableList<String> = ArrayList()
list.add("a")
list.add("b")
list.add("c")
list.add("d")
list.forEach(
    Consumer { l: String ->
        println(l)
    }
)
fun lambda(){
    val list=listOf(1,2,3,4,5,6,7)
    // 类似于java中的list.stream().filter()
    println(list.filter { l ->
        // 这里是条件
        l > 3
    })
}

image

可以看出这是四大函数式接口中断定型接口类型

定义常量使用 const val 关键字,表示编译时常量,只能写在外部
定义只读变量和可修改变量

可变变量定义: 使用var关键字

var 标识符:类型=初始化值

不可以变量定义: 使用val关键字

val 标识符:类型=初始化值

常量和变量都可以没有初始值,但是在引用前必须初始化
编译器支持自动类型判断,即声明时可以不指定类型,由编译器判断

/**
 * @定义只读变量和可修改变量
 *   可变变量定义:    使用var关键字
 *   不可以变量定义:   使用val关键字
 *   var 标识符:类型=初始化值
 *   val 标识符:类型=初始化值
 * 编译器支持自动类型判断,即声明时可以不指定类型,由编译器判断
 */

var a: Int = 1
var b = 1
println(a)
println(b)//系统会自动推断变量类型为Int
val c: Int// 初始化时候必须提供变量类型
c = 2// 明确赋值
println(c)
var x = 5
x += 2 // 变量是可以被修改
println(x)
字符串模板

$ 表示一个变量名或者变量值
$varName表示变量值
$(varName.fun())表示变量的方法返回值

/**
 * @字符串模板
 * $ 表示一个变量名或者变量值
 * $varName表示变量值
 * $(varName.fun())表示变量的方法返回值
 */

// 1.$varName表示变量值
var s = "wangliang";
var s1 = "i am $s";
println(s1) // 打印出 i am wangliang
// 2.$(varName.fun())表示变量的方法返回值
var s2 = "i  am ${s.replace("wangliang", "wl")}";
println(s2)
Null检查机制

Kotlin的空安全设计对于声明可为空的参数,在使用时要进行空判断处理,有两种处理方式:
1.字段后加!!像Java一样抛出空异常
2.另一种字段后加?可不做处理返回值为 null或配合?:做空判断处理

    /**
     * @Null检查机制
     * Kotlin的空安全设计对于声明可为空的参数,在使用时要进行空判断处理,有两种处理方式:
     *      1.字段后加!!像Java一样抛出空异常
     *      2.另一种字段后加?可不做处理返回值为 null或配合?:做空判断处理
     */

    // 类型后面加?表示可以为空
    var age: String? = null
    // 抛出空指针异常
//    var ages=age!!.toInt()
//    // 不作处理 返回null
//    val ages1=ages?.toInt()
    // age为空返回-1
    val ages2 = age?.toInt() ?: -1
    println(ages2)
类型检测及自动类型转换

注:在kotlin中 if是表达式,java中if是语句

/**
 * @类型检测以及自动类型的转换
 * 我们可以使用 is 运算符检测一个表达式是否某类型的一个实例(类似于Java中的instanceof关键字)。
 */
fun getStringLength(obj: Any): Int? {
    if (obj is String) {
        return obj.length;
    }
    return null;
}
区间
/**
 * @区间
 * 1.区间表达式由具有操作符形式 .. 的 rangeTo 函数辅以 in 和 !in 形成。
 * 2.区间是为任何可比较类型定义的,但对于整型原生类型,它有一个优化的实现。
 */

// 输出 "1234"
for (i in 1..4) print(i)
println()
// 什么都不输出
for (i in 4..1) print(i)
println()
// 输出 "4321"
for (i in 4 downTo 1) print(i)
println()
// if 和区间的配合使用
val i=4;
val l=3;
if (i in l..10){ // 等同于 i>=l && i<=10
    println(i)
}
// 使用 step 指定步长
for (i in 1..4 step 2) print(i) // 输出 "13"
println()
for(i in 4 downTo 1 step 2) print(i) // 输出 "42"
println()
for(i in 1 until  10) print(i) // 打印除了10
println()

和java中的StringBuilder 一样功能的 Kotlin的buildString

fun StringBuilder(){
    val builder= buildString {
        for (i in 5 downTo 1){
            append(i)
            appendLine()
        }
    }
    println(builder)
}

image

在Kotlin中,类之间的继承是通过:,而且默认的是被final修饰的,想要使一个类成为可被继承的,要使用open 修饰符

/**
 *@author 没有梦想的java菜鸟
 * @date 2022/01/05 10:16 上午
 */
open class TestKotlin {
    // 有参构造函数
    constructor(d: Double, d1: Double, d2: Double) {

    }

    // 无参构造函数
    constructor() {

    }
   // 表示可被继承的方法 
    open fun hello(){
        println("hello,kotlin")
    }
}

// 继承有参构造函数
class Test(d: Double, d1: Double, d2: Double) : TestKotlin(d, d1, d2) {

}
// 继承无参构造函数
class Test2 : TestKotlin() {
    override fun hello() {
        super.hello()
    }
}

创建POJO DTO实体类

data class Student(
     val name:String,
     val school:String,
     val address:String
)

Kotlin 定义实体类已经帮我们封装好了get()和set()方法,还有toString()方法

image

Sealed 关键字表示这个类是私有的

Object关键字定义类,里面表示这是一个静态类

集合

image

// 创建一个集合,使用方法等于java中的aList
var items= listOf("A","B","C","D")
for (item in items) {
    println(item)
}
// 创建一个集合,使用方法等于java中的aList
var items= listOf("A","B","C","D")
for (index in items.indices) {
    // 取出索引的值
    print(items[index])
    // 取出索引
    print(index)
    println()
}

检查集合中是否包含此元素

fun containWithEle(){
        val list= listOf("String","hello","world")
        if ("hello" in list){
            println("find it")
        }
        if ("world" in list){
            println("hello,world")
        }
  }

map集合

fun foreach(){
    var map = HashMap<String,Any>()
      map.put("1001","胡图图")
      map.put("1002","胡英俊")
      map.put("1003","翻斗家园")

       // 遍历
      for ((k,v) in map){
          println("$k-$v")

      }
      // 获取值
      println(map["1003"])
}
when关键字(相当于 switch case)
  @Test
   fun findString() {
        val obj: Any = "10"
        when (obj) {
            1 -> println("i am kotlin")
            is String ->println("i am String")
            is Long ->println( "i am Long")
            "java" ->println("i am java")
            else -> println("i can't know you")
        }
    }
运算符重载(operator overloading)

在java中想要实现对象排序需要使用 Comparable接口,有两种实现方法 ,一种是实现 Comparable接口,另外一种就是匿名内部类的方式,下面举个例子解释一下 kotlin的 运算符重载

data class Point(val x: Int, val y: Int) {
    // minus 表示 相减  方法里面的是我们自定义的规则
    operator fun minus(o: Point): Point {
        return Point(o.x - x, o.y - y)
    }
}

fun main(){
    val point1=Point(10,20)
    val point2=Point(15,26)
    // 根据自己的规则 输出 Point(x=5, y=6)
    println(point1-point2)
}

image

第二种写法

data class Point(val x: Int, val y: Int) {}
operator fun Point.minus(o:Point)=Point(o.x-x,o.y-y)
fun main(){
    val point1=Point(10,20)
    val point2=Point(15,26)
    // 根据自己的规则 输出 Point(x=5, y=6)
    println(point1-point2)
}

image

一元运算符
表达式 方法
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()
自增自减
表达式 方法
a++ a.inc() + see below
a-- a.dec() + see below
二元运算符
表达式 方法
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.rem(b)
a..b a.rangeTo(b)
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0
kotlin与JVM

在idea中查看kotlin生成的字节码文件

image

查看kotlin的字节码

image

经过对比发现kotlin的Int,Double类型都是一个类,这样编译起来是不是性能会不好,再看反编译后的java代码,发现会修改为java的基本类型 image

查看反编译后的java代码

image

具名函数

具体函数在传参的时候可以指定进行传参,以免出错

fun main() {
    specific(name = "胡图图",username = "大耳兔图图",id = 1001,password = "123456",date = Date())
}
private fun specific(name:String,username:String,id:Int,password:String,date:Date){
  println("name=${name},username=${username},id=${id},password=${password},date=${date}")
}
Nothing关键字

使用Nothing 类似于java中抛出异常并终止程序

fun main() {
    val num = -1
    when (num) {
        -1 -> TODO("非法参数")
        in 0..10 -> println("及格")
        in 10..20 -> println("优秀")
    }
}

image

反引号的使用场景

建一个java类

public class Quote {
    public static final void is(){
        System.out.println("i am is_Function");
    }
    public static  final  void in(){
        System.out.println("i am in_Function");
    }
}

image

kotlin调用

fun main() {
    // 使用场景一 用于测试
    `此方法仅用于测试 日期 2022年2月7日`("没有梦想的java菜鸟")
    // 使用场景二 在调用java类是,如果java中的方法名是kotlin中的关键字则需要加上 ``
    Quote.`in`()
    Quote.`is`()
}

private fun `此方法仅用于测试 日期 2022年2月7日`(name: String) {
    println("测试人:$name")
}
匿名函数

It 代指这个字符串,谁调用 it就代指谁

fun main() {
    // 计算字符串长度
    val length="java".count()
    println(length)
    // 计算指定字符
    val length2="java".count {
        it=='a'
    }
    println(length2)
}

image

隐式返回函数
fun main() {
    // 创建隐式函数 必须指定返回值类型
    val methodAction:(Int,Int,String)->String={
       // num1 num2 name 表示的是参数
        num1,num2,name-> "num1=$num1,num2=$num2,name=$name"
    }
    println(methodAction(1,2,"没有梦想的java菜鸟"))
  
      // it关键字的使用

    val methodIt:(String)->String={"$it"}
    println(methodIt("AA"))
		 
//    fun methodIt(it:String):String{
//        return "$it";
//    }
}

image

类型的自动推断
// 匿名函数类型的自动推断 无需指定返回类型
val methodType = {
    //t1 和 t2 表示的是 参数   后面的表示类型
   t1: Double, t2: String -> "hello"
}
val methodType2 = {
    //表示返回值类型是布尔值
    true
}
在函数中定义的参数是函数
const val USERNAME="张三"
const val PASSWORD="123456"
fun main() {
    // 第一种写法
    login(USERNAME, PASSWORD,{ msg,code-> println("msg=$msg,code=$code")})
    // 第二种写法
    login(USERNAME, PASSWORD,responseResult= {msg,code-> println("msg=$msg,code=$code")})
    // 第三种写法
    login(USERNAME, PASSWORD){
        msg,code->
        println("msg=$msg,code=$code")
    }
}

// 模拟登陆
inline fun login(username:String,password:String,responseResult:(String,Int)->Unit){
    if (username== USERNAME && password == PASSWORD){
        responseResult("登陆成功",200)
    }else{
        responseResult("用户名或密码错误",300)
    }
}

注:在不同的类中,kotlin中方法不能一样

经过反编译后的java代码如下 image

发现这个方法是被static+final修饰的,说明只会存在一份

内联的使用

经过反编译,在上面的login方法中,发现lambda表达式被作为接口替换掉了,这样会造成性能的损耗

在函数中有lambda表达式作为参数的函数中,需要使用inline关键字。使用了inline关键字,发现将login方法里面的内容放到了main函数里

image

函数的引用

函数作为lambda参数

const val USERNAME="张三"
const val PASSWORD="123456"
fun main() {
    // lambda表达式其实就是函数型对象,如果要把 responseResult函数作为参数传进去,需要把responseResult函数转换为函数类型对象
    login(USERNAME, PASSWORD, ::responseResult)
}
fun responseResult(msg:String,code:Int){
    println("msg=$msg,code=$code")
}

// 模拟登陆
inline fun  login(username:String,password:String,responseResult:(String,Int)->Unit){
    if (username== USERNAME && password == PASSWORD){
        responseResult("登陆成功",200)
    }else{
        responseResult("用户名或密码错误",300)
    }
}
函数类型作为函数的返回值
fun main(){
    // 此时返回的method是 一个匿名函数
    val method=methodReturn("胡图图")
    // 再给调用匿名函数
    println(method("翻斗花园", 1001))
}
fun methodReturn(name:String):(String,Int)->String{
    return {
        address,id-> "address=$address,id=$id,name=$name"
    }
}
具名函数和匿名函数
fun main() {
    // 匿名函数写法
    method1("胡图图", "翻斗花园") {
        msg,code->
        println("msg=$msg,code=$code")
    }
    // 具名函数写法
    method1("张三","北京市", ::ResultImpl)
}
fun ResultImpl(msg:String,code:Double){
    println("msg=$msg,code=$code")
}

inline fun method1(name: String, address: String, Result: (String, Double) -> Unit) {
    val str = "(name=$name,address=$address)"
    Result(str, 0.1)
}

运行截图

image

java模拟

public class Specific {
    public static void main(String[] args) {
        // 第一种   匿名内部类的写法
        method1("胡图图", "翻斗花园", new Result() {
            @Override
            public void result(String msg, double code) {
                System.out.println("msg="+msg+",code="+code);
            }
        });
       // 第二种   接口实现类
       method1("张三","北京市",new ResultImpl());

    }


    public static void method1(String name, String address, Result res) {
        String str = "(name=%s,address=%s)";
        String formatStr = String.format(str, name, address);
        res.result(formatStr, 0.1);
    }
}

interface Result {
    void result(String msg, double code);
}

class ResultImpl implements Result{

    @Override
    public void result(String msg, double code) {
        System.out.println("msg="+msg+",code="+code);
    }
}
内置函数及操作符
非空操作符 ?
fun main() {
    var str:String?="this is kotlin"
    str=null
    // 在str后面加上?,表示的是如果str为空,则不会执行后面的代码(str?.length)则不会被执行
    println(str?.length)
   //在str后面加上!! 表示断言,不管str是否为空都执行 
  println(str!!.capitalize())
}
let内置函数
fun main() {
    val result=listOf(1,2,3,4,5,6).let {
        println(it[0])
        // 最后一行是let函数的返回值
        "$it"
    }
    println(result)
}

image

空合并操作符
fun main() {
    var str:String?="this is kotlin"
    str=null
    // 如果str为空,则输出"空值"
    println(str ?: "空值")
    
}
异常处理和自定义异常

自定义异常

fun main() {
    // 自定义异常
    val str:String?=null
    checkIsNull(str)
}
fun checkIsNull(str:String?){
    str ?: throw NullException()
}
class NullException:IllegalArgumentException("空指针异常了 ^_^")

运行截图

image

自带的异常

fun main() {
    // 自定义异常
    val str:String?=null
    requireNotNull(str)
    val flag:Boolean=false
    require(flag)
}
substring和split
fun main() {
    // 传统java用法
    // 截取到指定字符
    val str = "this is kotlin"
    val index = str.indexOf('k')
    val newStr = str.substring(0, index)
    // kotlin用法
    val subStr = str.substring(0 until index)
    println(newStr)
    println(subStr)

    // 分隔字符
    val text = "java,kotlin,python,php,go"
    // 分隔后 是List<String>类型  val list:List<String> = text.split(",")
    val list = text.split(",")
    // 解构
    val (v1,v2,v3,v4,v5)=list
    println("$v1-$v2-$v3-$v4-$v5")
}

运行截图

image

replace的使用
fun main() {
    // 原始数据
    val pwd="ABCDGOPKDAOKDAJOIJOAJMNMKJNFKJSNAKA"
    // Regex("[ANKOD]") 表示 匹配这里面的字母
    val newPwd=pwd.replace(Regex("[ANKOD]")){
        // 对字母进行替换 加密
        when(it.value){
            "A"->"0"
            "N"->"6"
            "K"->"10"
            "O"->"7"
            "D"->"1"
            else ->it.value
        }
    }
    println("加密后的密码是:$newPwd")

    // 进行解密
    val sourcePwd=newPwd.replace(Regex("[061071]")){
        when (it.value){
            "0"->"A"
            "6"->"N"
            "10"->"K"
            "7"->"O"
            "1"->"D"
            else->it.value
        }

    }
    println("原密码是:$sourcePwd")
}
apply内置函数
fun main() {
    // 调用apply返回的是apply本身,类似于建造者模式 关键字 this代表调用者本身
    val str="abc??//"
    str.apply {
       val replaceStr= replace("//","!!")
        println(replaceStr)
    }.apply {
        val subStr=substring(0 until 4)
        println(subStr)
    }.apply {
        val splitStr=split("??")[0]
        println(splitStr)
    }
}

运行截图

image

run内置函数
fun main() {
    val str = "kotlin"
    // run函数的返回值是也是最后一行 
    str.run(::checkOut)
        .run(::printStr)
        .run(::println)
    
}

fun checkOut(str: String) = str == "kotlin"
fun printStr(boolean: Boolean) = if (boolean) "哈哈哈" else ""

运行截图

image

also内置函数
fun main() {
    var line:String?
    val buffer=BufferedReader(FileReader("/Users/wangliang/Documents/Kt_study/src/main/resources/2.txt"))
    // 在读取文件的时候 我们会让一个字符串变量去接收 读取的内容,kotlin 中不可以 (line=buffer.readLine())!=null 这样写 所以使用also 和apply都可以很好地解决
    while (buffer.readLine().also { line=it } !=null){
        println(line)
    }
}

image

takeIf内置函数(takeUnless)
fun main() {
    val username = "user"
    println(checkUsername(username))
}

fun checkUsername(username: String): Int {
    // 如果 takeIf的结果为 true  那么就会返回number本身 否则返回 空合并操作符的值 
    // takeIf 一般都是和空合并操作符 合并用的
    val number= 1
    return number.takeIf { username == "root" } ?: 10
}

image

copy函数的使用及其说明
data class CopyTest(var name: String, var age: Int) {
    private var info=""

    init {
        println("主构造被调用")
    }
    // 次级构造
    constructor(name:String):this(name,10){
        println("次构造被调用")
        info="重要信息"
    }

    override fun toString(): String {
        return "{name=$name,age=$age,info=$info}"
    }
}

fun main() {
    val cp=CopyTest("小明")
    println(cp)
    //  copy 函数内部代码只处理主构造函数的内容
    val cp2=cp.copy()
    println(cp2)

}

image

根据运行记过可以看出,copy 函数内部代码只处理主构造函数的内容

2.kotlin协程

1.协程概念

多线程的不足

多线程看起来是在并行执行,但是cpu在某一个时间片只能执行一个线程,当执行一会儿一个线程,就会立马切换到下一个线程。当线程池中的线程有许多都被阻塞住了,cpu就会将线程挂起,去执行别的线程,这样就会造成线程频繁切换,浪费资源。多线程一旦执行,内部是我们无法控制的。

协程

线程是由操作系统来调度的,而协程依赖于线程,不被操作系统调度。协程是由程序来调度的的,这样就可以大大减少开销。使用协程我们还可以在线程执行时进行控制。总而言之,携程是轻量级

2.什么是挂起?

协程中的挂起指的不是挂起线程也不是挂起函数,挂起的对象是协程。

当线程执行到挂起函数的时候,这个协程就会被挂起,暂时不再执行剩余的协程代码,跳出协程的代码块,也就是说这个协程正在从执行他的线程上脱离,然后线程会去执行别的任务去了。

当这个协程被挂起以后,继续执行在 Dispatchers 所指定的线程中,当执行完,会再切回原来的线程中。

3.协程的使用

创建协程的方式有三种方式

全局作用域

应用程序还在运行且协程的任务还未结束,协程就可以一直运行

GlobalScope.launch {

    launch {

        println("全局作用域")

    }

}
阻塞式

阻塞调用者线程,直至自己执行完

runBlocking {

    launch {

        println("阻塞式")

    }

}
独立作用域

需要依赖顶层函数,是一个挂起函数,coroutineScope会让所有启动的协程都完成后才结束自身

runBlocking{

coroutineScope {

    launch {

        println("独立作用域")

    }

 }

}

使用协程来读取文件

  • launch 无返回值开启协程
  • async 有返回值开启协程,可以用await方法进行阻塞等待结果
import kotlinx.coroutines.async

import kotlinx.coroutines.delay

import kotlinx.coroutines.launch

import kotlinx.coroutines.runBlocking

import java.io.BufferedReader

import java.io.FileReader

import java.util.*



/**

 *@author 没有梦想的java菜鸟

 * @date 2022/04/19 2:08 下午

 */

fun main() {

    val startTime=Date().time

    runBlocking {

        repeat(5){

            read()

        }



    }

    println(Date().time-startTime)



}



fun read() = runBlocking {

    var line: String?

    val buff = BufferedReader(FileReader("src/main/resources/rawlog.txt"))

    while (buff.readLine().also { line = it } != null) {

        val s = line

       // 无返回值开启协程 

        launch {

            delay(1000)

            println("hhhh")

        }

      // 有返回值开启协程

        val result=async { 

            getStr()

        }

        println(result.await())

    }



}

fun getStr():String{

    return "hh"

}

协程配合线程使用

import cn.hutool.core.date.DateUtil
import kotlinx.coroutines.*
import java.io.BufferedReader
import java.io.FileReader
import java.util.*
import java.util.concurrent.*

/**
 *@author 没有梦想的java菜鸟
 * @date 2022/02/22 1:53 下午
 */
private val threadPool = ThreadPoolExecutor(
    2,
    2,
    200,
    TimeUnit.SECONDS,
    LinkedBlockingDeque(20),
    Executors.defaultThreadFactory(),
    ThreadPoolExecutor.AbortPolicy()
).asCoroutineDispatcher()

fun main() {
    runBlocking {
        // 传参数依赖于 线程池  任务顺序按照 id 或者时间   执行时使用
        listOf<Student>(
            Student(1001, "小明", "****大学", "北京"),
            Student(1002, "小李", "----大学", "上海"),
            Student(1003, "小张", "@@@@大学", "澳门"),
            Student(1004, "小王", "====大学", "西藏")
        ).map {
            launch(threadPool) {
                test(it)
            }
        }
        // 不传参 或者固定参数
//        repeat(2) {
//            launch(Dispatchers.Default) {
//                test(Student())
//            }
//        }


    }

}
// 45997

suspend fun test(stu: Student) {
    println(stu)
    val conn = JdbcUtils.getConnection()
    val stmt = conn.createStatement()
    println(conn)
    withContext(Dispatchers.IO) { // 挂起当前函数 将资源让出  如果要保证顺序执行  不要用
        val buffer = BufferedReader(FileReader("src/main/resources/TableauDesktop-2022-2-0.dmg"))
        while (buffer.readLine() != null) {

        }
        println("insert into student values(${stu.id},'${stu.name}','${stu.school}','${stu.address}')")
//    stmt.execute("insert into student values(${stu.id},'${stu.name}','${stu.school}','${stu.address}')")
    }
}

data class Student(var id: Int? = 0, var name: String? = "", var school: String? = "", var address: String? = "")
posted @ 2021-12-13 11:58  没有梦想的java菜鸟  阅读(212)  评论(0编辑  收藏  举报