zoukankan      html  css  js  c++  java
  • 201871010113刘兴瑞《面向对象程序设计(java)》第六七周学习总结 201871010113

    项目

    内容

    这个作业属于哪个课程

    <任课教师博客主页链接> https://www.cnblogs.com/nwnu-daizh/

    这个作业的要求在哪里

    <作业链接地址>https://www.cnblogs.com/nwnu-daizh/p/11605051.html

    作业学习目标

    1.深入理解程序设计中算法与程序的关系;
    2.深入理解java程序设计中类与对象的关系;
    3.理解OO程序设计的第2个特征:继承、多态;
    4.学会采用继承定义类设计程序(重点、难点);
    5.能够分析与设计至少包含3个自定义类的程序;
    6.掌握利用父类定义子类的语法规则及对象使用要求。

     

    第一部分:总结第五章理论知

    继承:用已有类来构建新类的一种机制。当定义了一个新类继承了一个类时,这个新类就继承了这个类的方法和域,同时在新添加新的方法和域以适应新情况。

    继承是Java程序设计中的- -项核心技术,也是面向对象特征之一-

    继承的特点是:具有层次结构,子类继承了父类的域和方法。

    继承的优点:代码可重用性,可以轻松定义子类,父类的域和方法可用于子类,设计应用程序变得更加简单

    类、超类和子类

    继承的格式:class    extends  已有类名

    已有类称为:超类(superclass)、基类(base c1ass)或父类(parent class)

    新类称作:子类(subclass)、派生类(derived class)或孩子类(chi1d c1ass)

    一般来说。子类比超梁拥有的功能更加丰富.

    1.通过扩展超类定义子类时,仅需要指出子类与超类的不同之处。在子类中可以增加域、增加方法或覆盖超类的方法,但绝对能删除超类的任何域和方法。

    super是个指示编译器调用超类方法的特有关键字,它不是一个对象的引用,不能将super赋给另-个对象变量。super关键字一般有两个用途: 是调用超类的方法(格式: super. 方法名() ),二是调用超类的构造器(格式:super())

    若子类构造器没有显式地调用超类的构造器,则将自动地调用超类默认构造器。如果超类只定义了带参数的构造器,若子类构造器没有显式地调用超类的构造器,则Java编译器将报告错误。

    2.继承层次

    从一个超类扩展而来的类集合称为继承层次。在继承层次中,从某个类到其祖先的路径被称为该类的继承链

    ●Java不支持多继承。

    3.多态性

    多态性的概念:多态性泛指在程序中同一个符号在不同的情况下具有不同解释的现象

    超类中定义的域或方法,被子类继承之后,可以具有不同的数据类型或表现出不同的行为。

    这使得在超类及其各个子类中同名的域或方法具有不同的语义。

    超类中的方法在子类中可方法重写。

    4.抽象类

     观察类的继承层次结构。位于上层的类更具遇用性。甚至可能更加抽象。从某种角度看祖先类更加遇用,人们只将它作为派生其他类的基类。而不作为特定的实例类.

    为了提高序清晰度,包含一个或多个抽象法的本身必须被声明为抽象类。除了抽象方法之外,抽象类还可以包含具体数据和具体方法。

    抽象方法充当着占位的角色,它们的具体实现在子类中。扩展抽象类可以有两种选择:一种是在子类中实现部分抽象方法,这样就必须将子类也标记为抽象类;种是实现全部抽象方法,这样子类就可以不是抽象类。此外,类即使不含抽象方法,也可以将类声明为抽象类。

    抽象类不能被实例化,即不能创建对象,只能产生子类。可以创建抽象类的对象变里,只是这个变里必须指向它的非抽象子类的对象。

    5.动态绑定

    又称运行时绑定。即程序在运行时会自动选择调用哪个方法。

    调用对象方法的执行过程:首先,编译器检查对象的声明类型和方法名,搜索相应类(Son)及其父类(Father)的“方法表”,找出所有访问属性为publicmethod方法。接下来,编译器检查方法调用中提供的参数类型,找出一个完全匹配的方法,这个过程称为重载解析。如果方法是privatestatic. final修饰的,或者是构造器,那么编译器能准确地判断应该调用哪个方法,这称为静态绑定。

    程序运行时,如果子类Son中定义了method()方法,则直接调用子类中的相应方法;如果子类Son中没有定义相应的方法,则到其父类中寻找method()方法。

    动态绑定中每次调用方法都要进行搜索,时间开消相当大。因此虚拟机预先为每个类创建了一个方法表,其中列出了所有方法的签名和实际调用的方法。

    方法的名称和参数列表称为方法的签名

    6.组织继承:final类和方法

      不允许继承的类称为fina1类。 在类的定义中用fina1修饰符加以说明。

    类中的方法可定义为fina1.这时子类就不能覆盖该方法

     如果一个类户明为final.属于它的方法会被自动设为final.但不包括城(如果城定义为final.在对象构造以后,fina1域就不能再修改了) private final int Max = 100;

      String类是fina1类的-一个例子。不能扩展该类.

    7.强制类型转换

     如果要把一一个超类对象赋给一个子类对象变量。就必须进行强制类型转换.其格式为:子类对象一(子类) (超类对象)

     类型转换必须在继承层次内进行:而且在超类转换为子类之前。应先使用instanceof操作符进行继承链检查.

    8.受访问保护

     如果希望超类的某些方法或域允许被子类直接访问,就需要在超类定义时,将这些方法或域声明为protected

    protected违背了OP提倡的数据封装原则。实际中要谨慎使用protected的访问属性。

    若定义类时要限制类中某个方法的使用,就可以将它声明为protected。这表明子类得到信任,可以使用这个方法,而其他类不行。

     访问权限修饰符:

     

    0bject:所有类的超类

    ●Object尖是Java中所有类的祖先每一 个类都由它扩展而来.在不给出超类的情况下,Java会自动把0bject

    作为妥定义类的超类。

    可以使用类型为Object的变量指向任意类型的对象.但要对它们进行专门的操作都要进行奖型转换。

    1.equals方法

    0bject类中的equals方法用于测试某个对象是否同另一个对象相等.它在0bject读中的实现是判断两个对象是否具有相同的引用.如果两个对象具有相同的引用。它们一定是相等的.

    如果需要检测两个对象状态的相等性。就需要在新类的定义中需要覆盖equals方法.

    定义子奖的equa1s方法时。可词用超类的eqa1s方法 super.equals (otherObject)

    2. hashCode方法

    0bject类中的hashCode方法导出某个对象的散列码。散列码是任意整数,表示对象的存储地址。

    两个相等对象的散列码相等。

    3. toString方法

    Object类的toString方法返回一个代表该对象城值的字符串

    定义子类的toString方法时。可先词用超类的toString方法.  super. toString( )

    tostrine方法是非常重要的词试工具标准类库中,多数类定义了toString方法。以便用户获得对象状态的必要信息。

    泛型数组列表

    ●Java中,利用ArrayList,可允许程序在运行时确定数组的大小。

    ●ArryList个采用类型参数的泛型类。为指数组列表保存元素的对象类型,需要用一对尖括号将数组元素的对象类名括起来加在后面 ArryList<Employee> staff=new ArrayList<Employee>():

    没有<>ArrayList将被认为是一个删去了类型参数的原始类型。

    对象包装器和自动打包

    所有基本数据类型都有着与之对应的预定义类它们披称为对象包装器

    以下前6个对象包装器类都是从公共包装器类Number继承而来

    Integer  Long  F1oat  Double  Short  Byte  Character  Void  Boolean

    对象包装器类是不可变的。即一且构造了包装器。就不允许更改包装在其中的位。且对象包装器奖还是final.因此不能定文它们的子.

    使用对象包装器的好处: 基本类型转化为对象   定义一些有用的基本方法(static方法)

    Java SE 5.中,可以自动的将基本数据类型转换为包装器类的对象,将这种变换称为自动打包

    相反地,当对个包装器类的对象进行赋值或算法运算时,将会自动地拆包。

    打包和包是编释器认可的。

    参数数量可变的方法

    Java SE 5.以前的版本中,每个Java方法都有固定数里的参数。然而,现在的版本提供了可以用可变的参数数里调用的方法(称为可变参方法)

    用户自己可以定义可变参数的方法,并将参数指定为任意类型,甚至是基本类型。

    枚举类

    要定义一-个成绩类,成绩的范围只能是ABCDE,接受其它类型的值都是违法的,应该如何定义呢?

    如何表示成绩呢,整型、字符型?都不适,因为没有明确的类型对应,即使是字符型,超出了“ABCDE”范围的字符程序需要特别处理,以便保证应用安全

    ●Java SE 5.0以后版本中提供了一种称为校举的类型定义方法。

    1声明枚举类

    public enum Grade { A, B, C,D, E }它包括一个关键字enom,一个新枚举类型的名字Grade以及为Grade定义的一组值,这里的值既非整型,非字符型。

    枚举类说明枚举类是-个类.它的隐含超类是java. Lang. Bnum. 枚举位并不是整数或其它类型。是被户明的枚举类的自身实例,例如AGrade的一个实例. 枚举类不能有publ ic修饰的构造函效。 构造教都是隐含private编译器自动处理.枚举隐含都是由public. static. fina1修饰的。无须自己添加这些修饰符.在比较两个枚举类型的值时。永远不需要词用equals方法。直接使用”=进行相比较

    继承设计的技巧

    将公共操作和域放在超类。

    不要使用受保护的域。

    使用继承实现“is-a”关系。国除非所有继承的方法都有意义,刚就不要使用继承。

    在覆盖方法时,不要改变预期的行为。田使用多态,而非类型信息。

    第二部分:实验部分

    1、实验目的与要求

    (1) 理解继承的定义;

    (2) 掌握子类的定义要求

    (3) 掌握多态性的概念及用法;

    (4) 掌握抽象类的定义及用途。

    2、实验内容和步骤

    实验1:测试程序1

     (1) 在elipse IDE中编辑、调试、运行程序5-1 —5-3(教材152页-153

     (2)掌握子类的定义及用法;

     (3)结合程序运行结果,理解并总结OO风格程序构造特点,理解Employee和Manager类的关系子类的用途,并在代码中添加注释;

    (4)删除程序中Manager类、ManagerTest类,背录删除类的程序代码,在代码录入中理解父类与子类的关系和使用特点。

     5-1程序代码如下:

    package inheritance;
    
    /**
     * This program demonstrates inheritance.
     * @version 1.21 2004-02-21
     * @author Cay Horstmann
     */
    public class ManagerTest
    {
    	public static void main(String[] args)
    	   {
    	      // 构造一个Manager对象,设置她的奖金
    	      var boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
    	      boss.setBonus(5000);
    
    	      var staff = new Employee[3];
    
    	      // 用 Manager和Employee对象填入staff数组
    
    	      staff[0] = boss;  //置换法则:出现的超类对象可以用子类对象置换
    	      staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
    	      staff[2] = new Employee("Tommy Tester", 40000, 1990, 3, 15);
    
    	      // 输出所有Employee对象信息
    	      for (Employee e : staff)
    	         System.out.println("name=" + e.getName() + ",salary=" + e.getSalary());
    	       //当e引用的是Employee对象时, e.getSalary()调用的是Employee类中的getSalary方法。
    	       //当e引用的是Manager对象时, e.getSalary()调用的是Manager类中的getSalary方法。
    	   }
    }
    

    程序运行如图:

    5-2代码如下:

    package inheritance;  //导入inheritance包
    
    import java.time.*; //导入LocalDate类包所在的包路径
    
    public class Employee 
    {
    	//实例域定义
       private String name;
       private double salary;
       private LocalDate hireDay;
       //构造方法
       public Employee(String name, double salary, int year, int month, int day)
       {
          this.name = name; //this调用构造器
          this.salary = salary;
          hireDay = LocalDate.of(year, month, day);
       }
       //方法
       public String getName()
       {
          return name;
       }
    
       public double getSalary()
       {
          return salary;
       }
    
       public LocalDate getHireDay()
       {
          return hireDay;
       }
    
       public void raiseSalary(double byPercent)
       {
          double raise = salary * byPercent / 100;
          salary += raise;
       }
    }
    

    5-3背录后的代码:

    package inheritance;
    
    public class Manager extends Employee //定义子类,关键字extends表示继承
    {
    
    	private int bonus;
    
    	public Manager(String name, double salary, int year, int month, int day) { //构造器
    		super(name, salary, year, month, day); // 用super访问父类私有属性
    		// TODO Auto-generated constructor stub
    		bonus=0;
    	}
    
    	@Override
    	public double getSalary() {      //覆盖父类getSalary方法
    		// TODO Auto-generated method stub
    		double baseSalary = super.getSalary();//用super访问父类方法
    		return baseSalary+bonus;
    	}
    /*
          错误使用
    	public double getSalary() {      
    		// TODO Auto-generated method stub
    		double baseSalary = getSalary();
    		return baseSalary+bonus;
    	}
    */
    	public void setBonus(int b) {
    		bonus=b;
    	}
    
    
    }

    实验1:测试程序2

    (1) 编辑、编译、调试运行教材PersonTest程序(教材163页-165页);

    (2) 掌握超类的定义及其使用要求;

    (3) 掌握利用超类扩展子类的要求;

    (4) 在程序中相关代码处添加新知识的注释;

    (5)删除程序中Person类、PersonTest类,背录删除类的程序代码,在代码录入中理解抽象类与子类的关系和使用特点。

     5-4程序代码如下:

    package abstractClasses;
    
    /**
     * This program demonstrates abstract classes.
     * @version 1.01 2004-02-21
     * @author Cay Horstmann
     */
    public class PersonTest
    {
       public static void main(String[] args)
       {
          var people = new Person[2]; 
    
          //将雇员和学生对象填充到Person引用数组 
          people[0] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
          people[1] = new Student("Maria Morris", "computer science");
    
          //输出所有对象的姓名和信息描述 
          for (Person p : people)
             System.out.println(p.getName() + ", " + p.getDescription());
       }
    }
    

     程序运行如图:

    5-5代码如下:

    package abstractClasses;
    
    public abstract class Person //定义抽象超类Person
    {
       public abstract String getDescription();//定义抽象方法getDesription
       private String name;
       
       //构造方法
       public Person(String name)
       {
          this.name = name;
       }
       
       public String getName()
       {
          return name;
       }
    }
    

    5-6背录代码如下:

    package abstractClasses;
    
    import java.time.*;
    
    public class Employee extends Person //定义Person的子类Employee
    {
    	//实例域定义
    	private double salary;
    	private LocalDate hireDay;
    
    	//构造方法
    	public Employee(String name,double salary,int year,int month,int day) {
    		super(name);  //super访问Person属性name
    		this.salary=salary;
    		hireDay=LocalDate.of(year, month, day);
    		// TODO Auto-generated constructor stub
    	}
    
    	@Override
    	public String getDescription() {
    		// TODO Auto-generated method stub
    		return String.format("an employee with a salary of $%.2f", salary);
    	}
    
    	public double getSalary() {
    		return salary;
    	}
    
    	public void setSalary(double salary) {
    		this.salary = salary;
    	}
    
    	public LocalDate getHireDay() {
    		return hireDay;
    	}
    
    	public void setHireDay(LocalDate hireDay) {
    		this.hireDay = hireDay;
    	}
    	
    	public void raiseSalary(double byPercent) {
    		double raise = salary * byPercent/100;
    		salary += raise;
    	}
    }
    

    5-7背录代码如下:

    package abstractClasses;
    
    public class Student extends Person
    {
        private String major;//定义私有域major
    	public Student(String name,String major) {
    		super(name);
    		this.major = major;
    		// TODO Auto-generated constructor stub
    	}
    
    	@Override
    	public String getDescription() {
    		// TODO Auto-generated method stub
    		return "a student majoring in " + major;
    	}
      
    }

    实验1:测试程序3

    (1)编辑、编译、调试运行教材程序5-8、5-9、5-10,结合程序运行结果理解程序(教材174页-177页);

    (2) 掌握Object类的定义及用法;

    (3)在程序中相关代码处添加新知识的注释。

    5-8代码如下:

    package equals;
    
    /**
     * This program demonstrates the equals method.
     * @version 1.12 2012-01-26
     * @author Cay Horstmann
     */
    public class EqualsTest
    {
       public static void main(String[] args)
       {
    	   //构造Employee对象
          var alice1 = new Employee("Alice Adams", 75000, 1987, 12, 15);
          var alice2 = alice1;
          var alice3 = new Employee("Alice Adams", 75000, 1987, 12, 15);
          var bob = new Employee("Bob Brandson", 50000, 1989, 10, 1);
          
          //输出字符串,判断是否相等
          System.out.println("alice1 == alice2: " + (alice1 == alice2));
    
          System.out.println("alice1 == alice3: " + (alice1 == alice3));
    
          System.out.println("alice1.equals(alice3): " + alice1.equals(alice3));//调用equals方法
    
          System.out.println("alice1.equals(bob): " + alice1.equals(bob));
    
          System.out.println("bob.toString(): " + bob);
    
          //构造Manager对象
          var carl = new Manager("Carl Cracker", 80000, 1987, 12, 15);
          var boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
          boss.setBonus(5000);//调用setBonus方法
          System.out.println("boss.toString(): " + boss);
          System.out.println("carl.equals(boss): " + carl.equals(boss));
          System.out.println("alice1.hashCode(): " + alice1.hashCode());//调用hashCode方法
          System.out.println("alice3.hashCode(): " + alice3.hashCode());
          System.out.println("bob.hashCode(): " + bob.hashCode());
          System.out.println("carl.hashCode(): " + carl.hashCode());
       }
    }

    运行结果如图:

    5-9代码如下:

    package equals;
    
    import java.time.*;
    import java.util.Objects;
    
    public class Employee
    {
    	//实例域定义
       private String name;
       private double salary;
       private LocalDate hireDay;
    
       //构造方法
       public Employee(String name, double salary, int year, int month, int day)
       {
          this.name = name;
          this.salary = salary;
          hireDay = LocalDate.of(year, month, day);
       }
    
       //方法
       public String getName()
       {
          return name;
       }
    
       public double getSalary()
       {
          return salary;
       }
    
       public LocalDate getHireDay()
       {
          return hireDay;
       }
    
       public void raiseSalary(double byPercent)
       {
          double raise = salary * byPercent / 100;
          salary += raise;
       }
    
       //equals方法,返回值为布尔值
       public boolean equals(Object otherObject)
       {
          //快速测试看对象是否相等
          if (this == otherObject) return true;
    
          // 显示参数为空,必须返回false
          if (otherObject == null) return false;
    
          //如果这些类不匹配,他们不可能相等
          if (getClass() != otherObject.getClass()) return false;
    
          //现在我们知道 otherObject是一个非空的Employee
          var other = (Employee) otherObject;
    
          //测试字段是否具有相同的值
          return Objects.equals(name, other.name) 
             && salary == other.salary && Objects.equals(hireDay, other.hireDay);
       }
    
       public int hashCode()
       {
          return Objects.hash(name, salary, hireDay); 
       }
    
       public String toString()
       {
          return getClass().getName() + "[name=" + name + ",salary=" + salary + ",hireDay=" 
             + hireDay + "]";
       }
    }
    

     5-10代码如下:

    package equals;
    
    public class Manager extends Employee//定义Employee的子类Manager
    {
    	//私有域津贴
       private double bonus;
    
       public Manager(String name, double salary, int year, int month, int day)
       {
          super(name, salary, year, month, day);//super调用父类属性
          bonus = 0;
       }
    
       public double getSalary()
       {
          double baseSalary = super.getSalary();
          return baseSalary + bonus;
       }
    
       public void setBonus(double bonus)
       {
          this.bonus = bonus;
       }
    
       public boolean equals(Object otherObject)
       {
          if (!super.equals(otherObject)) return false;
          var other = (Manager) otherObject;
          // super.equals checked that this and other belong to the same class
          return bonus == other.bonus;
       }
    
       //hashCode方法计算散列码,散列码方法返回整型数值
       public int hashCode()
       {
          return java.util.Objects.hash(super.hashCode(), bonus);
       }
    
       public String toString()
       {
          return super.toString() + "[bonus=" + bonus + "]";
       }
    }

    实验2:编程练习

    •  定义抽象类Shape:

        属性不可变常量double PI,值为3.14

        方法:public double getPerimeter();public double getArea())。

    •  RectangleCircle继承自Shape类。
    • 编写double sumAllArea方法输出形状数组中的面积和和double sumAllPerimeter方法输出形状数组中的周长和。
    •  main方法中

             1输入整型值n,然后建立n个不同的形状。如果输入rect,则再输入长和宽。如果输入cir,则再输入半径。
             2) 然后输出所有的形状的周长之和,面积之和。并将所有的形状信息以样例的格式输出。
             3) 最后输出每个形状的类型与父类型使用类似shape.getClass()(获得类型),shape.getClass().getSuperclass()(获得父类型);

          思考sumAllAreasumAllPerimeter方法放在哪个类中更合适?

    • 输入样例:

              3

       rect

       1 1

       rect

       2 2

       cir

       1

    • 输出样例:

       18.28

       8.14

       [Rectangle [width=1, length=1], Rectangle [width=2, length=2], Circle [radius=1]]

       class Rectangle,class Shape

       class Rectangle,class Shape

       class Circle,class Shape

    代码如下:

    import java.util.Scanner;
    public class Sh {
    	public static double sumAllArea(double areaall)
    	{
    		return areaall;
    	}
        public static double sumAllPerimeter(double perimeterall)
        {
        	return perimeterall;
        }
     
    	public static void main(String[] args) {
    		Scanner sc = new Scanner(System.in);
    		int n = sc.nextInt();
    		sc.nextLine();
    		Shape []xz = new Shape[n];
    		double sumAllArea = 0, sumAllPerimeter = 0;
    		for(int i = 0;i < n;i++)
    		{
    			String ss = sc.nextLine();
    			if(ss.equals("rect"))
    			{
    				int a = sc.nextInt(),b = sc.nextInt();
    				sc.nextLine();
    				xz[i] = new Rectangle(a,b);
    			}
    			if(ss.equals("cir"))
    			{
    				int r = sc.nextInt();
    				sc.nextLine();
    				xz[i] = new Circle(r);
    			}
    			sumAllArea += xz[i].getArea();
    			sumAllPerimeter += xz[i].getPerimeter();
    		}
    		System.out.println(sumAllPerimeter(sumAllPerimeter));
    		System.out.println(sumAllArea(sumAllArea));
    		System.out.print("[");
    		for(int i = 0;i < n;i++)
    		{
    			if(i != 0)
    				System.out.print(", ");
    			System.out.print(xz[i].toString());
    		}
    		System.out.println("]");
            for(int i = 0;i < n;i++)
            {
            	System.out.println(xz[i].getClass()+","+xz[i].getClass().getSuperclass());
            }
            sc.close();
    	}
     
    }
     
    abstract class Shape {
    	final double PI = 3.14;
    	public abstract double getPerimeter();
        public abstract double getArea();
    }
     
    class Rectangle extends Shape {
    	public int width;
    	public int length;
    	
    	public Rectangle(int width, int length) {
    		super();
    		this.width = width;
    		this.length = length;
    	}
    	
     
    	public String toString() {
    		return "Rectangle [width=" + width + ", length=" + length + "]";
    	}
     
     
    	public double getPerimeter() {
    		return 2*(width+length);
    	}
     
    	public double getArea() {
    		return width*length;
    	}
     
    }
     
    class Circle extends Shape {
    	public int radius;
    	
    	public Circle(int radius) {
    		super();
    		this.radius = radius;
    	}
    	
    	public String toString() {
    		return "Circle [radius=" + radius + "]";
    	}
     
     
    	public double getPerimeter() {
    		return 2*PI*radius;
    	}
     
    	public double getArea() {
    		return PI*radius*radius;
    	}
     
    }
    

     运行结果如下图:

    3. 实验总结:

    在这次的实验中,结合理解书上案例的理解,我了解到了超类的定义和用法,子类和父类的关系,知道了抽象类,还有Object类的知识,一些方法,如equals方法,hashCode方法的用法。在这一章作业中,我收获很多,但是也意识到我的不足,编写代码没有思路等等,希望以后可以克服这些问题。

  • 相关阅读:
    【leetcode】416. Partition Equal Subset Sum
    【leetcode】893. Groups of Special-Equivalent Strings
    【leetcode】892. Surface Area of 3D Shapes
    【leetcode】883. Projection Area of 3D Shapes
    【leetcode】140. Word Break II
    【leetcode】126. Word Ladder II
    【leetcode】44. Wildcard Matching
    【leetcode】336. Palindrome Pairs
    【leetcode】354. Russian Doll Envelopes
    2017.12.22 英语面试手记
  • 原文地址:https://www.cnblogs.com/lxr0/p/11625425.html
Copyright © 2011-2022 走看看