韩顺平零基础30天学会Java【章8 面向对象编程(高级)】

类变量

P373~P423

类变量也叫静态变量

static变量是同一个类的所有对象共享

static变量在类加载的时候就生成

如何定义类变量

访问修饰符 static 数据类型 变量名;

如何访问类变量

  • 类名.类变量名
  • 对象名.类变量名

静态变量的访问修饰符的访问权限和范围和普通属性一样

类变量使用细节

当我们需要让某个类的所有对象都共享一个变量时,就可以考虑使用类变量

类方法

访问修饰符 static 数据返回类型 方法名(){}

类方法调用

  • 类名.类方法名
  • 对象名.类方法名

类方法使用场景

当方法中不涉及到任何和对象相关的成员,则可以将方法设计成静态方法,提高开发效率。

如果我们不希望创建实例,也可以调用某个方法,这时,把方法做成静态方法非常合适

比如:工具类中的方法

Math类、Arrays类、Collections集合类

类方法注意事项

  • 类方法和普通方法都是随着类的加载而加载,将结构信息存储在方法区,类方法中无this的参数,普通方法中隐含着this的参数
  • 类方法可以通过类名调用,也可以通过对象名调用
  • 普通方法和对象有关,需要通过对象名调用
  • 类方法中不允许使用和对象有关的关键字,比如this和super
  • 类方法中只能访问静态变量或静态方法
  • 普通成员方法既可以访问非静态成员,也可以访问静态成员

main方法

public static void main(String[] args){}

  1. public:java虚拟机需要调用类的main()方法,所以该方法的访问权限必须是public
  2. static:java虚拟机在执行main()方法时不必创建对象,所以该方法必须是static
  3. 该方法接受Sting类型的数组参数,该数组中保存执行java命令时传递给所运行类的参数

注意

main方法中,可以直接调用main方法所在类的静态方法或静态属性

但是不能直接访问该类中的非静态成员,必须创建该类的一个实力对象后,才能通过这个对象去访问类中的非静态成员

代码块

代码块又称为初始化块,属于类中的成员,类似于方法,将逻辑语句封装在方法体中,通过{}包围起来。

但和方法不同,没有方法名,没有返回,没有参数,只有方法体,而且不用通过对象或类显式调用,而是加载类时,或创建对象时隐式调用

基本语法

[修饰符]{
  代码
};

注意

  1. 修饰符可选,但写的话只能是static
  2. 代码块分为两类,static修饰的叫静态代码块,没有static修饰的叫普通代码块
  3. 逻辑语句可以为任何逻辑语句(输入、输出、方法调用、循环、判断)
  4. 分号可以写也可以省略

理解

  1. 相当于另一种形式的构造器(对构造器的补充机制),可以做初始化的操作
  2. 如果多个构造器中都有重复的语句,可以抽取到初始化块中,提高代码的重用性
  3. 不管调用哪个构造器创建对象,都会先调用代码块的内容
  4. 代码块调用的顺序优先于构造器

代码块使用细节,com.codeBlock.CodeBlockDetail.java

  1. static代码块也叫静态代码块,作用是对类进行初始化,且随着类的加载而执行,并且只会执行一次。如果是普通代码块,创建一个对象,就执行一次。
  2. 类什么时候被加载
    1. 创建对象实例时(new)
    2. 创建子类对象实例,父类也会被加载,且父类先加载,子类后加载
    3. 使用类的静态成员时(静态属性,静态方法)
  3. 普通的代码块,在创建对象实例时,会被隐式的调用。被创建一次,就会调用一次,相当于构造器的补充。如果只是使用类的静态成员,普通代码块并不会执行。
  4. 创建一个对象时,在一个类调用顺序是:
    1. 调用静态代码块和静态属性初始化(注意:静态代码块和静态属性初始化调用的优先级一样,如果有多个静态代码块和多个静态变量初始化,则按他们定义的顺序调用)
    2. 调用普通代码块和普通属性的初始化(注意:普通代码块和普通属性初始化调用的优先级一样,如果有多个普通代码块和多个普通属性初始化,则按定义顺序调用)
    3. 调用构造方法
  5. 构造器的最前面其实隐含了super()和调用普通代码块。静态的代码块和属性初始化在类加载时,就执行完毕,因此是优先于构造器和普通代码块执行的。CodeBlockDetail03.java
  6. 创建一个子类时,静态代码块,静态属性初始化,普通代码块,普通属性初始化,构造方法的调用顺序如下:
    1. 父类的静态代码块,静态属性初始化
    2. 子类的静态代码块,静态属性初始化
    3. 父类的普通代码块,普通属性初始化
    4. 父类的构造函数
    5. 子类的普通代码块,普通初始化
    6. 子类的构造函数
  7. 静态代码块只能直接调用静态成员,普通代码块可以调用任意成员
package com.codeBlock;

public class CodeBlockDetail {
    public static void main(String[] args) {
        //1. static代码块也叫静态代码块,作用是对类进行初始化,且随着类的加载而执行,并且只会执行一次。
        //如果是普通代码块,每创建一个对象,就执行。
        //2. 类什么时候被加载
            //1. 创建对象实例时(new)
            //2. 创建子类对象实例,父类也会被加载
//        AA aa = new AA();   // bb的静态代码块被执行
                             // aa的静态代码块被执行
                             // bb的普通代码块
            //3. 使用类的静态成员时(静态属性,静态方法)
//        System.out.println(Cat.name);   //cat的静态代码块被执行
                                            //mao
        //3. 普通的代码块,在创建对象实例时,会被隐式的调用。被创建一次,就会调用一次。
        //如果只是使用类的静态成员,普通代码块并不会执行。
//        BB.show();    //bb的静态代码块被执行
                        //调用静态属性,普通代码块不调用
//        BB bb1 = new BB();  //bb的静态代码块被执行
                            //bb的普通代码块
    }
}

class BB {
    static {
        System.out.println("bb的静态代码块被执行");
    }
    {
        System.out.println("bb的普通代码块");
    }

    public static void show() {
        System.out.println("调用静态属性,普通代码块不调用");
    }
}

class AA extends BB {
    static {
        System.out.println("aa的静态代码块被执行");
    }
}
class Cat{
    public static String name="mao";
    static {
        System.out.println("cat的静态代码块被执行");
    }
}

设计模式

什么是设计模式

  1. 静态方法和属性的经典使用
  2. 设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。

单例模式

  1. 所谓类的单例设计模式,就是采取一定的方法保证在整个软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法
  2. 单例模式有两种方式:饿汉式、懒汉式

单例模式应用实例

【饿汉式】,com.single_.SingleTon01.java

只要类加载,这个对象就创建,可能只是用一下类变量,即使没有使用到,这个对象也给一起创建了,会浪费资源

步骤:

  1. 构造器私有化:防止直接new

  2. 类的内部创建对象

  3. 向外暴露一个静态的公共方法。getInstance

    (为什么要static?因为如果要不创建实例直接调用类方法,那他是static的,且方法内的属性也是static的)

    (如果调用多次getInstance用不同变量名接收,其实是指向同一个对象,因为static的属性只会初始化一次)

  4. 代码实现

public class SingleTon01 {
    public static void main(String[] args) {
//        GirlFriend xiaohong = new GirlFriend("xiaohong");
//        GirlFriend xiaobai = new GirlFriend("xiaobai");
        GirlFriend instance = GirlFriend.getInstance();
        System.out.println(instance);
    }
}

class GirlFriend {
    private String name;
    //如何保证只能有一个girlfriend
    //步骤:
    //1、构造器私有化
    //2、类的内部创建
    private static GirlFriend gf = new GirlFriend("xiaohong");
    private GirlFriend(String name) {
        this.name = name;
    }
    //3、提供一个公共静态方法
    public static GirlFriend getInstance() {
        return gf;
    }
    @Override
    public String toString() {
        return "GirlFriend{" + "name='" + name + '\'' + '}';
    }
}

【懒汉式】,com.single_.SingleTon02.java

只有用户调用getInstance方法时,才会创建对象,且如果调用过这个方法了,再次调用时,返回的是上次创建的对象

public class SingleTon02 {
    public static void main(String[] args) {
        System.out.println(Cat.getInstance());
    }
}
//希望程序运行过程中只能创建一个Cat
class Cat{
    private String name;
    private static Cat cat;
    //1、构造器私有化
    //2、定义一个static属性对象
    private Cat(String name) {
        this.name = name;
    }
    //3、提供公共static方法,返回cat对象
    
    public static Cat getInstance(){
        if(cat==null){
            cat= new Cat("xiaomao");
        }
        return cat;
    }

饿汉式和懒汉式的区别

  1. 两者最主要的区别在于创建对象的时机不同,饿汉式是在类加载就创建了对象实例,而懒汉式是在使用时才创建
  2. 饿汉式不存在线程安全问题,懒汉式存在线程安全问题(见线程学习)
  3. 饿汉式存在浪费资源的可能,因为如果程序员一个对象实例都没有使用,那么饿汉式创建的对象就浪费了,懒汉式是使用时才创建,就不存在这个问题
  4. 在javaSE标准类中,java.lang.Runtime就是经典的单例模式
public class Runtime {
    private static Runtime currentRuntime = new Runtime();
    public static Runtime getRuntime() {
        return currentRuntime;
    }
    /** Don't let anyone else instantiate this class */
    private Runtime() {}
    }

Final关键字

final可以修饰类、属性、方法和局部变量

使用情况:com.final_.Final01.java

  1. 当不希望类被继承时,可以使用final修饰
  2. 当不希望父类的某个方法被子类覆盖/重写时【访问修饰符 final 返回类型 方法名】
  3. 当不希望类的某个属性的值被修改【public final double TAX_RATE=0.08】
  4. 当不希望某个局部变量被修改【final double TAX_RATE=0.08】
package com.final_;
public class Final01 {
    public static void main(String[] args) {
        A a = new A();
        a.TAX_RATE=0.09;
    }
}
//如果要求A不能被其他类继承,使用final修饰A
//final class A{
class A{
    //    不希望类的某个属性的值被修改
//    public final double TAX_RATE=0.08;
    public double TAX_RATE=0.08;


    //    不希望方法被子类覆盖
//    public final void hi(){}
    public void hi(){
        //    不希望某个局部变量被修改
//        final int money=100;
        int money=100;
        money =101;
    }
}

class B extends A{
    @Override
    public void hi() {
        super.hi();
    }
}

final细节

  1. final修饰的属性又叫常量,用XX_XX_XX来命名
  2. final修饰的属性在定义时必须赋初值,并且不能再修改,可以在如下位置赋初值:
    1. 定义时
    2. 在构造器中
    3. 在代码块中
class AA{
  public final double TAX_RATE=0.08;//定义时
  public final double TAX_RATE2;
  public final double TAX_RATE3;
  public AA(){//构造器中赋值
    TAX_RATE2=1.1;
  }
  {//代码块赋值
    TAX_RATE3=8.8;
  }
}
  1. 如果final修饰的属性是静态的,则初始化的位置只能是
    1. 定义时
    2. 静态代码块
class BB{
  public static final double TAX_RATE=0.08;//定义时
  public static final double TAX_RATE2;
  static {//代码块赋值
    TAX_RATE3=8.8;
  }
}
  1. final类不能继承,但是可以实例化对象
  2. 如果类不是final类,但是含有final方法,则该方法虽然不能重写,但是可以被继承
  3. 一般来说,如果一个类是final类了,就不用再把方法写成final的
  4. final不能修饰构造器
  5. final和static往往搭配使用,效率更高,不会导致类加载,底层编译器做了优化处理com.final_.FinalStatic.java
package com.final_;
public class FinalStatic {
    public static void main(String[] args) {

        System.out.println(AA.name);
    }
}

class AA {
    public static final String name = "aaa";
    static {
        System.out.println("静态代码块");
    }
}

  1. 包装类(Integer,Double,Float,Boolean都是final),String也是final类

抽象类

com.abstract_.Abstract01.java

当父类的某些方法,需要声明,但是又不确定如何实现时,可以将其声明为抽象方法,那么这个类就是抽象类

package com.abstract_;

public class Abstract01 {
    public static void main(String[] args) {

    }
}

abstract class Animal {
    private String name;
    public Animal(String name) {
        this.name = name;
    }

    //eat实现了,但没什么意义
    //出现了父类方法不确定性的问题
    //考虑将该方法设计为抽象方法
    //所谓抽象方法就是没有实现的方法
    //所谓没有实现就是没有方法体{}
    //当一个类中存在抽象方法时,需要将该类声明为abstract类
    //一般来说,抽象类会被继承,由其子类来实现抽象方法
    public abstract void eat();
}

介绍

  1. 用abstract关键字来修饰一个类时,这个类就叫抽象类

    访问修饰符 abstract 类名{}

  2. 用abstract关键字来修饰一方法时,这个方法就是抽象方法,没有方法体{}

    访问修饰符 abstract 返回类型 方法名(参数列表);

  3. 抽象类的价值更多作用是在于设计,是设计者设计好之后,让子类继承并实现抽象类

抽象类细节

  1. 抽象类不能被实例化
  2. 抽象类不一定包含abstract方法,也就是说抽象类可以没有abstract方法
  3. 但是如果类包含了abstract方法,则这个类必须声明为abstract
  4. abstract只能修饰类和方法,不能修饰属性和其他
  5. 抽象类可以有任意成员,因为抽象类还是类
  6. 抽象方法不能有方法体{}
  7. 如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法,除非他自己也是抽象类
  8. 抽象方法不能使用private、final和static来修饰,因为这些关键字都是和重写相违背的

练习,com.abstract_.AbstractExercise01.java

编写一个Employee类,声明为抽象类,包含三个属性:name、id、salary,提供必要的构造器和抽象方法work()。对于Manager类来说,他既是员工,还有奖金(bonus)的属性,请使用继承思想,设计CommonEmployee和Manager类,要求类中提供必要的方法进行属性访问,实现work(),“经理、普通员工 姓名 工作中。。。”

package com.abstract_;
public class AbstractExercise01 {
    public static void main(String[] args) {
        CommonEmployee xiaoming = new CommonEmployee("小明", 101, 1000);
        Manager daming = new Manager("大明", 99, 1100, 2000);
        xiaoming.work();
        daming.work();
    }
}
abstract class Employee {
    private String name;
    private int id;
    private double salary;
    public Employee(String name, int id, double salary) {
        this.name = name;
        this.id = id;
        this.salary = salary;
    }
    public String getName() {return name;}
    public void setName(String name) {this.name = name;}
    public int getId() {return id;}
    public void setId(int id) {this.id = id;}
    public double getSalary() {return salary;}
    public void setSalary(double salary) {this.salary = salary;}
    public abstract void work();
}
class Manager extends Employee {
    private double bonus;
    public Manager(String name, int id, double salary, double bonus) {
        super(name, id, salary);
        this.bonus = bonus;
    }
    public double getBonus() {return bonus;}
    public void setBonus(double bonus) {this.bonus = bonus;}
    @Override
    public void work() {
        System.out.println("经理 " + getName() + "工作中..");
    }
}
class CommonEmployee extends Employee {
    public CommonEmployee(String name, int id, double salary) {
        super(name, id, salary);
    }
    @Override
    public void work() {
        System.out.println("普通员工" + getName() + "工作中..");
    }
}

接口

接口就是给出一些没有实现的方法,封装到一起,到某个类要使用的时候,再根据具体情况把这些方法写出来

interface 接口名{
//属性
//方法(1抽象方法2默认方法3静态方法)
}
class 类名 implements 接口名{
  自己属性;
  自己方法;
  必须实现接口的抽象方法
}

在JDK7.0前,接口里的所有方法都没有方法体

在JDK8.0后,接口类可以有静态方法需要static修饰,默认方法需要default修饰,也就是说接口中可以有方法的具体实现

快速入门,com.interface_.Interface01.java

package com.interface_;

public class Interface01 {
    public static void main(String[] args) {
        Computer computer = new Computer();
        Camera camera = new Camera();
        Phone phone = new Phone();
        computer.work(camera);
        computer.work(phone);
    }
}
interface UsbInterface {
    public void start();
    public void stop();
}

class Computer {
    public void work(UsbInterface usbInterface) {
        usbInterface.start();
        usbInterface.stop();
    }
}

class Camera implements UsbInterface {
    @Override
    public void start() {
        System.out.println("我是相机,开始工作");
    }

    @Override
    public void stop() {
        System.out.println("我是相机,结束工作");
    }
}

class Phone implements UsbInterface {
    @Override
    public void start() {
        System.out.println("我是手机,开始工作");
    }

    @Override
    public void stop() {
        System.out.println("我是手机,结束工作");
    }
}

接口细节

  1. 接口不能被实例化

  2. 接口中所有的方法都是public方法,接口中抽象方法可以不用abstract修饰

  3. 一个普通类implements接口,就必须实现接口的所有方法

  4. 抽象类implements接口,可以不用实现接口的方法

  5. 一个类同时可以implements多个接口

  6. 接口中的属性只能是final的,而且是public static final。比如 int a=1;实际上是public static final int a=1;

  7. 接口中属性的访问形式:接口名.属性名

  8. 一个接口不能继承其他的类,但是可以继承多个别的接口

    interface extends B,C{}

  9. 接口的修饰符只能是public和默认,和类的修饰符一样

实现接口vs继承类

接口和继承解决的问题不同

  • 继承的价值在于:解决代码的复用性和可维护性
  • 接口的价值在于:设计,设计各种规范,让其他类去实现这些方法

接口在一定程度上实现代码解耦

抽象类和接口的使用时机

参考

在设计类的时候,首先考虑用接口抽象出类的特性,当你发现某些方法可以复用的时候,可以使用抽象类来复用代码。简单说,接口用于抽象事物的特性,抽象类用于代码复用。

当然,不是所有类的设计都要从接口到抽象类,再到类。程序设计本就没有绝对的范式可以遵循。根据自己的需求设计。

接口的多态特性

  1. 多态参数,InterfacePolyParameter.java
  2. 多态数组,InterfacePloyArr.java
  3. 接口存在多态传递现象,InterfacePolyPass.java

小结

类的五大成员:属性、方法、构造器、代码块、内部类

package 包名;
class 类名 extends 父类 implements 接口名{
  成员变量//属性
  构造方法//构造器
  成员方法//方法
  代码块
 }

内部类

一个类的内部又完整的嵌套了另一个类结构。被嵌套的类称为内部类,嵌套其他类的类称为外部类。

内部类的最大特点就是可以直接访问私有属性,并且可以体现类于类之间的包含关系

基本语法

class Outer{//外部类
  class Inner{//内部类
  }
}
class Other{//外部其他类
}

内部类的分类

定义在外部类局部位置上:比如方法内

  • 局部内部类(有类名)
  • 匿名内部类(没有类名)

定义在外部类的成员位置上:

  • 成员内部类(没用static修饰)
  • 静态内部类(使用static修饰)

定义在外部类局部位置上:

局部内部类,com.innerclass.LocalInnerClass.java

  • 局部内部类是定义在外部类的局部位置,比如方法中,并且有类名
  • 不能添加访问修饰符,因为他的地位就是一个局部变量。局部变量不能使用修饰符,但是可以用final修饰
  • 作用域:仅在定义他的方法或代码块中,相当于一个类型为类的局部变量
  • 局部内部类直接访问外部类成员,包括私有的
  • 外部类需要创建对象再访问内部类成员
  • 外部其他类不能访问局部内部类
  • 如果外部类和局部内部类重名时,默认遵循就近原则,如果吸纳各方那个问外部类的成员,可以使用外部类名.this.成员访问
public class LocalInnerClass {
    public static void main(String[] args) {
    }
}

class Outer {
    private int n1 = 100;
    private void m2() {}
    private void m1() {
        // 局部内部类是定义在外部类的局部位置,比如方法中,并且有类名
        // 不能添加访问修饰符,因为他的地位就是一个局部变量。
        // 局部变量不能使用修饰符,但是可以用final修饰
        class Inner { // 局部内部类
            // 局部内部类可以直接访问外部类成员,包括私有的
            public void f1() {
                System.out.println("n1=" + n1);
            }
        }
    }
}

  1. 局部内部类定义在方法/代码块中
  2. 作用域在方法体/代码块中
  3. 本质是个类

匿名内部类,com.anonymousInner.AnonymousInnerClass.java

匿名内部类是定义在外部类的局部位置,比如方法中,并且没有类名,同时还是一个对象

匿名内部类相当于实现接口或继承父类

匿名内部类的基本语法

new 类或接口(参数列表){
  匿名类类体
};

  1. 匿名内部类既是一个类的定义,同时他也是一个对象,因此从语法上看,他既有类的特征,也有创建对象的特征,因此可以调用匿名内部类方法
  2. 可以直接访问外部类的所有成员,包含私有的
  3. 不能添加访问修饰符,因为他的地位就是一个局部变量
  4. 作用域:仅在定义他的方法或代码块中
  5. 匿名内部类直接访问外部类的成员
  6. 外部其他类不能访问匿名内部类
package com.anonymousInner;
public class AnonymousInnerClass {
    public static void main(String[] args) {
        Outer04 outer04 = new Outer04();
        outer04.method();
    }
}
class Outer04 {//外部类
    private int n1 = 10;//属性
    public void method() {//方法
        //基于接口的匿名内部类
        //1、需求:想使用接口IA,并创建对象
        //2、传统方式:写一个类,实现该接口,然后创建对象
        //3、需求是Tiger类只使用一次,后面再不使用
//        IA tiger = new Tiger();
//        tiger.cry();
        //4、可以用匿名内部类来简化开发

        //演示基于接口的匿名内部类
        //tiger的编译类型:IA
        //tiger的运行类型:com.anonymousInner.Outer04$1
        //底层:class Outer04$1 implements IA{}
        IA tiger = new IA() {
            @Override
            public void cry() {
                System.out.println("老虎叫");
            }
        };
        tiger.cry();
        System.out.println("tiger的运行类型"+tiger.getClass());

        //演示基于类的匿名内部类
        //jack的编译类型:
        //jack的运行类型:com.anonymousInner.Outer04$2
        //底层:class Outer04$2 extends Father{}
        //和""Father jack = new Father("jack");"" 有区别,区别在于"{};",
        //实际是匿名内部类继承了Father类,应该是叫向上转型
        Father jack = new Father("jack") {};
        System.out.println("jack的运行类型"+jack.getClass());
    }
}
interface IA {
    public void cry();
}
// 传统方法:写一个类,实现该接口,然后创建对象
class Tiger implements IA {
    @Override
    public void cry() {
        System.out.println("老虎叫");
    }
}
class Father {
    public Father(String name) {super();}
    public void test() {}
}

内部类练习,com.anonymousInner.InnerClassExercise02.java

  1. 有一个铃声接口Bell,里面有个ring方法
  2. 有一个手机类Cellphone,具有闹钟功能alarmclock,参数是Bell类型
  3. 测试手机类的闹钟功能,通过匿名内部类作为参数,打印:懒猪起床了
  4. 再传入另一个匿名内部类,打印:小伙伴上课了
//1. 有一个铃声接口Bell,里面有个ring方法
//2. 有一个手机类Cellphone,具有闹钟功能alarmclock,参数是Bell类型
//3. 测试手机类的闹钟功能,通过匿名内部类作为参数,打印:懒猪起床了
//4. 再传入另一个匿名内部类,打印:小伙伴上课了
public class InnerClassExercise02 {
    public static void main(String[] args) {
        new Cellphone().alarmclock(new Bell(){
            @Override
            public void ring() {
                System.out.println("懒猪起床了");
            }
        });
        new Cellphone().alarmclock(new Bell() {
            @Override
            public void ring() {
                System.out.println("小伙伴上课了");
            }
        });
    }
}

interface Bell{
    public void ring();
}
class Cellphone{
    public void alarmclock(Bell bell){bell.ring();}
}

定义在外部类的成员位置上:

成员内部类

  1. 可以直接访问外部类的所有成员,包括私有的
  2. 可以添加任意访问修饰符 public、protected、默认、private,地位相当于成员
  3. 作用域:和外部类的其他成员一样,为整个类体
  4. 成员内部类直接访问外部类
  5. 外部类访问内部类:创建对象在访问
  6. 外部其他类访问成员内部类
  7. 如果外部类和内部类的成员重名时,内部类访问的话,默认遵循就近原则,如果想访问外部类的成员,则可以使用(外部类名.this.成员)访问

静态内部类

静态内部类是定义在外部类的成员位置,并且有static修饰

  1. 可以直接访问外部类的所有静态成员,包括私有的,但不能直接访问非静态成员
  2. 可以添加任意访问修饰符
  3. 作用域:同其他成员,为整个类体
  4. 静态内部类直接访问外部类
  5. 外部类创建对象访问静态内部类

猜你喜欢

转载自blog.csdn.net/weixin_65656674/article/details/126416751