Java【第八篇】面向对象之高级类特性
static 关键字
当我们编写一个类时,其实就是在描述其对象的属性和行为,而并没有产生实质上的对象,只有通过new关键字才会产生出对象,这时系统才会分配内存空间给对象,其方法才可以供外部调用。
我们有时候希望无论是否产生了对象或无论产生了多少对象的情况下,某些特定的数据在内存空间里只有一份,例如所有的中国人都有个国家名称,每一个中国人都共享这个国家名称,不必在每一个中国人的实例对象中都单独分配一个用于代表国家名称的变量。在Java类中声明变量、方法和内部类时,可使用关键字static做为修饰符。
static标记的变量或方法由整个类(所有实例)共享,如访问控制权限允许,可不必创建该类对象而直接用类名加‘.’调用。static成员也称类成员或静态成员,如:类变量、类方法、静态方法等。
Math类中的方法:
类变量、类方法
若需要一个类的多个对象共享一个变量,则该变量需要使用 static 修饰;
因为 static 修饰的变量为类的所有的实例所共享, 所以 static 成员不属于某个类的实例,而属于整个类,所以在访问权限允许的情况下, 可以使用 "类名." 直接访问静态成员(成员包括属性和方法);
没有对象的实例时,可以用类名.方法名()的形式访问由static标记的类方法;
在静态方法里只能直接调用同类中其它的静态成员(包括变量和方法),而不能直接访问类中的非静态成员。这是因为,对于非静态的方法和变量,需要先创建类的实例对象后才可使用,而静态方法在使用前不用创建任何对象;
静态方法不能以任何方式引用this和super关键字。与上面的道理一样,因为静态方法在使用前不用创建任何实例对象,当静态方法被调用时,this所引用的对象根本就没有产生;
非静态方法中可以直接来访问类的静态成员;
main() 方法是静态的,因此JVM在执行main方法时不创建main方法所在的类的实例对象,因而在main()方法中,我们不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,才能通过这个对象去访问类中的非静态成员。
如果成员变量是静态的,生成的get或者set方法也是静态的。
类属性、类方法的设计思想
类属性作为该类各个对象之间共享的变量。在设计类时,分析哪些类属性不因对象的不同而改变,将这些属性设置为类属性。相应的方法设置为类方法。
如果方法与调用者无关,则这样的方法通常被声明为类方法,由于不需要创建对象就可以调用类方法,从而简化了方法的调用
示例
练习1:编写一个类,实现银行账户的概念,包含的属性有“帐号”、“密码”、“存款余额”、“利率”、“最小余额”,定义封装这些
属性的方法。账号要自动生成。
编写主类,使用银行账户类,输入、输出3个储户的上述信息。
考虑:哪些属性可以设计成static属性。 Bank.java
package com.uncleyong; /** * 编写一个类,实现银行账户的概念,包含的属性有“帐号”、“密码”、“存款余额”、“利率”、“最小余额”,定义封装这些 * 属性的方法。账号要自动生成。 */ public class BankAccount { //初始化的 id private static int initId = 1000; //账号 private String id; //密码 private String password; //余额 private int balance; //利率 private static double rate; //最小余额 private static int minBalance; public BankAccount(String password, int balance) { this.id = "" + (initId++); // initId自增 this.password = password; this.balance = balance; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public int getBalance() { return balance; } public void setBalance(int balance) { this.balance = balance; } public static double getRate() { // 对象有static修饰符,所以对应的get方法也会自动加static return rate; } public static void setRate(double rate) { BankAccount.rate = rate; } public static int getMinBalance() { // 对象有static修饰符,所以对应的get方法也会自动加static return minBalance; } public static void setMinBalance(int minBalance) { BankAccount.minBalance = minBalance; } @Override public String toString() { return "BankAccount{" + "id='" + id + "'" + ", password='" + password + "\'" + ", balance=" + balance + ", rate=" + rate + ", minBalance=" + minBalance + '}'; } }
package com.uncleyong; /** * 编写主类,使用银行账户类,输入、输出3个储户的上述信息。 */ public class TestBank { public static void main(String[] args) { //统一设置 rate 和 minBalance BankAccount.setMinBalance(100); BankAccount.setRate(0.01); BankAccount acc1 = new BankAccount("1234", 100); BankAccount acc2 = new BankAccount("1235", 200); BankAccount acc3 = new BankAccount("1236", 300); System.out.println(acc1); System.out.println(acc2); System.out.println(acc3); } }
静态初始化
静态初始化指对类的静态属性进行初始化。
不应该在构造器中对静态成员进行初始化:
如果在构造器中对静态成员进行初始化,那么每创建一个对象,都会对静态成员赋值;
非静态代码块:
先于构造器执行, 每创建一个对象都会执行一次 { System.out.println("非静态代码块"); }
静态代码块:
一个类中可以使用不包含在任何方法体中的静态代码块(static block )
使用 static 修饰的代码块
//在类被加载时执行一次,且仅执行一次,可以在静态代码块中对静态成员变量进行初始化. //先于非静态代码块和构造器执行. static{ System.out.println("静态代码块"); }
static块通常用于初始化static (类)属性
class Person { public static int total; static { total = 100;//为total赋初值 } …… //其它属性或方法声明 }
单子 Singleton 设计
设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。设计模式就想是经典的棋谱,不同的棋局,我们用不同的棋谱,免得我们自己再去思考和摸索。
所谓类的单态设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。
如果我们要让类在一个虚拟机中只能产生一个对象,我们首先必须将类的构造方法的访问权限设置为private,这样,就不能用new 操作符在类的外部产生类的对象了,但在类内部仍可以产生该类的对象。因为在类的外部开始还无法得到类的对象,只能调用该类的某个静态方法以返回类内部创建的对象,静态方法只能访问类中的静态成员变量,所以,指向类内部产生的该类对象的变量也必须定义成静态的。
所谓类的单态设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例 1 不能在类的外部通过 new 关键字的方式创建新的实例: 构造器私有化. 2 在类的内部创建实例 3 为了让类的外部能够访问到类的内部创建的实例, 该实例必须使用 static 修饰. 4 为了不允许在类的外部修改内部创建的实例的引用. SingleInstance.instance = null; 需要把该属性用 private 修饰 5 为了让外部进行读, 添加对应的 get 方法
class Single{ private static Single onlyone = new Single();//私有的,只能在类的内部访问 private String name; public static Single getSingle() { //getSingle()为static,不用创建对象即可访问;变量是静态的,所以对应的get方法也为静态的 return onlyone; } private Single() {} //private的构造器,不能在类的外部创建该类的对象 } public class TestSingle{ public static void main(String args[]) { Single s1 = Single.getSingle(); //访问静态方法 Single s2 = Single.getSingle(); if (s1==s2){ System.out.println("s1 is equals to s2!"); } } }
理解main方法的语法
由于java虚拟机需要调用类的main()方法,所以该方法的访问权限必须是public,又因为java虚拟机在执行main()方法时不必创建对象,所以该方法必须是static的,该方法接收一个String类型的数组参数,该数组中保存执行java命令时传递给所运行的类的参数。
package com.uncleyong; public class CommandPara{ public static void main(String[] args) { for ( int i = 0; i < args.length; i++ ) { System.out.println("args[" + i + "] = " + args[i]); } } }
结果
也可以这样运行
输入参数
运行结果
然后也可以从这里重新编辑参数
final 关键字
在Java中声明类、属性和方法时,可使用关键字final来修饰。
final标记的变量(成员变量或局部变量)即成为常量,只能赋值一次。final PI=3.14;
final标记的类不能被继承。提高安全性,提高程序的可读性。
final标记的方法不能被子类重写。增加安全性。
final标记的成员变量必须在声明的同时或在每个构造方法中显式赋值,然后才能使用,否则编译出错。
final在访问控制符后面,class的前面
习惯用法:[访问限定符] [static] [final] [类型名]
示例
package com.uncleyong; public class TestFinal { int i = 10; int j; final int m = 10; final int n; public TestFinal() { // 构造方法中赋值 n = 100; } public void test(){ // 可以在方法中为成员变量赋值 i = 10; j = 20; // m = 100; } public static void main(String[] args) { TestFinal testFinal = new TestFinal(); System.out.println(testFinal.i + " "+ testFinal.j); // 10 0 testFinal.test(); System.out.println(testFinal.i + " "+ testFinal.j); // 10 20 } } final class A{ } //class B extends A{ // //} class C{ void method1(){} } class D extends C{ @Override void method1() { super.method1(); } }
抽象类(abstract 关键字)
随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一般,更通用。类的设计应该保证父类和子类能够共享特征。
有时将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做抽象类。
1.abstract 关键字可以修饰类和方法 2.用abstract关键字来修饰一个类时,这个类叫做抽象类;用abstract来修饰一个方法时,该方法叫做抽象方法。 3.抽象类:不能被实例化,抽象类是用来被继承的,抽象类中不一定有抽象方法,如果有,抽象类的子类必须重写父类的抽象方法,并提供方法体。 4.抽象方法:没有方法体的方法,含有抽象方法的类必须被声明为抽象类,抽象方法是用来被子类重写的。抽象方法只有方法的声明,没有方法的实现,以分号结束。abstract int abstractMethod1( int a ); 5.不能用abstract修饰私有方法,构造方法,静态方法。(因为私有方法、构造方法不能被子类继承;静态方法不属于面向对象范畴,直接用类点就可以调用,没有实现) 6.抽象类是用来模型化那些父类无法确定全部实现,而是由其子类提供具体实现的对象的类。
示例
package com.uncleyong; public class TestAbstract { public static void main(String[] args) { E e = new F(); } } abstract class E{ abstract void test(); // 抽象方法没有方法体 private void aa(){} } class F extends E{ @Override void test() { } void aa(){} } abstract class G extends E{ }
接口(interface 关键字)
有时必须从几个类中派生出一个子类,继承它们所有的属性和方法。但是,Java不支持多重继承。有了接口,就可以得到多重继承的效果(一个类可以实现多个无关的接口)。
接口(interface)是抽象方法和常量值的定义的集合,实际上接口更是一种规范(面向接口编程,包括JDBC,spring,用了接口编程的思想)。
从本质上讲,接口是一种特殊的抽象类,这种抽象类中只包含常量和方法的定义,而没有变量和方法的实现。
接口定义举例
public interface Runner { int id = 1; public void start(); public void run(); public void stop(); }
接口的特点:
1.用 interface 来定义。
2.接口中的所有成员变量都默认是由public static final修饰的。在声明变量时必须直接赋值(因为接口中没有构造器),常量标识符的书写要求: 字母都大写, 多个单词使用 _ 连接
3.接口中的所有方法都默认是由public abstract修饰的。接口没有构造方法。
4.实现接口使用 implements 关键字. 若一个类既实现接口, 又继承了父类, 需要把 extends 关键字放在implements 前面, 即先继承父类, 后实现多个接口(可以这样理解:继承只能一个,实现可以多个,从少到多)。一个类可以实现多个无关的接口(模拟实现多继承), 若实现多个接口, 使用逗号分割
5.接口也可以继承另一个接口,使用extends关键字。
关于接口的提示:
a. 实现接口的类中必须提供接口中所有方法的具体实现内容。 若该类为抽象类, 另当别论
b. 多个无关的类可以实现同一个接口
c. 与继承关系类似, 接口与实现类之间存在多态性
如果实现接口的类中没有实现接口中的全部方法,必须将此类定义为抽象类。
如果实现的接口又继承了另外一个接口,那么这个实现类中要实现子接口和父接口的所有方法。
定义Java类的语法格式:
< modifier> class < name> [extends < superclass>][implements < interface> [,< interface>]* ] { < declarations>* }
示例
public class TestInterface { public static void main(String[] args) { System.out.println(Dao.DAO_NAME); } } interface Dao{ String DAO_NAME = "DAONAME"; // 接口中的所有成员变量都默认是由 public static final 修饰的,在声明变量时必须直接赋值 void save(); // 抽象方法 }
输出:
DAONAME
实现接口使用 implements 关键字
public class TestInterface { public static void main(String[] args) { System.out.println(Dao.DAO_NAME); } } interface Dao{ String DAO_NAME = "DAONAME"; // 接口中的所有成员变量都默认是由 public static final 修饰的,在声明变量时必须直接赋值 void save(Object obj); // 抽象方法 Object get(); // 抽象方法,返回Object类型 } class DaoImpl implements Dao{ public void save(Object obj) { } public Object get() { return null; } }
若一个类既实现接口, 又继承了父类, 需要把 extends 关键字放在implements前面(e在i前面,或者这样理解,继承只能一个,实现可以多个,从少到多)
public class TestInterface { public static void main(String[] args) { System.out.println(Dao.DAO_NAME); } } interface Dao{ String DAO_NAME = "DAONAME"; // 接口中的所有成员变量都默认是由 public static final 修饰的,在声明变量时必须直接赋值 void save(Object obj); // 抽象方法 Object get(); // 抽象方法,返回Object类型 } class BaseDao{} class DaoImpl extends BaseDao implements Dao{ public void save(Object obj) { } public Object get() { return null; } }
多个无关的类可以实现同一个接口
public class TestInterface { public static void main(String[] args) { System.out.println(Dao.DAO_NAME); } } interface Dao{ String DAO_NAME = "DAONAME"; // 接口中的所有成员变量都默认是由 public static final 修饰的,在声明变量时必须直接赋值 void save(Object obj); // 抽象方法 Object get(); // 抽象方法,返回Object类型 } class BaseDao{} class DaoImpl extends BaseDao implements Dao{ public void save(Object obj) { } public Object get() { return null; } } // 多个无关的类可以实现同一个接口 class DaoImpl2 implements Dao{ public void save(Object obj) { } public Object get() { return null; } }
一个类可以实现多个无关的接口(模拟实现多继承), 若实现多个接口, 使用逗号分割
public class TestInterface { public static void main(String[] args) { System.out.println(Dao.DAO_NAME); } } interface Dao{ String DAO_NAME = "DAONAME"; // 接口中的所有成员变量都默认是由 public static final 修饰的,在声明变量时必须直接赋值 void save(Object obj); // 抽象方法 Object get(); // 抽象方法,返回Object类型 } interface Test{} class BaseDao{} class DaoImpl extends BaseDao implements Dao, Test{ // 一个类可以实现多个无关的接口(模拟实现多继承), 若实现多个接口, 使用逗号分割 public void save(Object obj) { } public Object get() { return null; } } // 多个无关的类可以实现同一个接口 class DaoImpl2 implements Dao{ public void save(Object obj) { } public Object get() { return null; } }
与继承关系类似, 接口与实现类之间存在多态性
public class TestInterface { public static void main(String[] args) { System.out.println(Dao.DAO_NAME); Dao dao = new DaoImpl(); // Dao是接口类型,后面new DaoImpl()是接口实现类的对象。。。。与继承关系类似, 接口与实现类之间存在多态性 } } interface Dao{ String DAO_NAME = "DAONAME"; // 接口中的所有成员变量都默认是由 public static final 修饰的,在声明变量时必须直接赋值 void save(Object obj); // 抽象方法 Object get(); // 抽象方法,返回Object类型 } interface Test{} class BaseDao{} class DaoImpl extends BaseDao implements Dao, Test{ // 一个类可以实现多个无关的接口(模拟实现多继承), 若实现多个接口, 使用逗号分割 public void save(Object obj) { } public Object get() { return null; } } // 多个无关的类可以实现同一个接口 class DaoImpl2 implements Dao{ public void save(Object obj) { } public Object get() { return null; } }
接口也可以继承另一个接口,使用extends关键字
public class TestInterface { public static void main(String[] args) { System.out.println(Dao.DAO_NAME); Dao dao = new DaoImpl(); // Dao是接口类型,后面new DaoImpl()是接口实现类的对象。。。。与继承关系类似, 接口与实现类之间存在多态性 } } interface Dao{ String DAO_NAME = "DAONAME"; // 接口中的所有成员变量都默认是由 public static final 修饰的,在声明变量时必须直接赋值 void save(Object obj); // 抽象方法 Object get(); // 抽象方法,返回Object类型 } interface Test{} interface Test2 extends Test{} // 接口也可以继承另一个接口,使用extends关键字 class BaseDao{} class DaoImpl extends BaseDao implements Dao, Test{ // 一个类可以实现多个无关的接口(模拟实现多继承), 若实现多个接口, 使用 , 分割 public void save(Object obj) { } public Object get() { return null; } } // 多个无关的类可以实现同一个接口 class DaoImpl2 implements Dao{ public void save(Object obj) { } public Object get() { return null; } }
定义一个接口用来实现两个对象的比较。
interface CompareObject{ public int compareTo(Object o); //若返回值是 0 , 代表相等; 若为正数,代表当前对象大;负数代表当前对象小 }
定义一个Circle类。
定义一个ComparableCircle类,继承Circle类并且实现CompareObject接口。在ComparableCircle类中给出接口中方法compareTo的实现体,用来比较两个圆的半径大小。
定义一个测试类TestInterface,创建两个ComaparableCircle对象,调用compareTo方法比较两个类的半径大小。
思考:参照上述做法定义矩形类Rectangle和ComparableRectangle类,在ComparableRectangle类中给出compareTo方法的实现,比较两个矩形的面积大小。
package com.uncleyong; interface CompareObject{ //若返回值是 0 , 代表相等; 若为正数,代表当前对象大;负数代表当前对象小 public int compareTo(Object o); // 当前对象和对象o比较 }
package com.uncleyong; public class Circle { protected double radius; public double getRadius() { return radius; } public void setRadius(double radius) { this.radius = radius; } public Circle(double radius) { this.radius = radius; } }
package com.uncleyong; /** * 定义一个ComparableCircle类,继承Circle类并且实现CompareObject接口。 * 在ComparableCircle类中给出接口中方法compareTo的实现体,用来比较两个圆的半径大小。 */ public class ComparableCircle extends Circle implements CompareObject{ public ComparableCircle(double radius) { super(radius); // 父类只有一个有参构造函数 } @Override public int compareTo(Object o) { // 当前对象和对象o比较 //1. 检验 o 是否为 Circle 类型 if(o instanceof Circle){ Circle circle = (Circle) o; // 强转 //2. 比较半径 return (int)(this.radius - circle.radius); } return 0; } }
package com.uncleyong; public class TestInterface { public static void main(String[] args) { ComparableCircle cc1 = new ComparableCircle(2); ComparableCircle cc2 = new ComparableCircle(7); ComparableCircle cc3 = new ComparableCircle(2); ComparableCircle cc4 = new ComparableCircle(1); ComparableCircle cc5 = new ComparableCircle(2); ComparableCircle cc6 = new ComparableCircle(2); System.out.println(cc1.compareTo(cc2)); System.out.println(cc3.compareTo(cc4)); System.out.println(cc5.compareTo(cc6)); } }
返回0表示相等,但是如果传入的不是圆,就不严谨了,需要抛出自定义异常
package com.uncleyong; public class Circle { protected double radius; public double getRadius() { return radius; } public void setRadius(double radius) { this.radius = radius; } public Circle(double radius) { this.radius = radius; } }
package com.uncleyong; /** * 定义一个ComparableCircle类,继承Circle类并且实现CompareObject接口。 * 在ComparableCircle类中给出接口中方法compareTo的实现体,用来比较两个圆的半径大小。 */ public class ComparableCircle extends Circle implements CompareObject { public ComparableCircle(double radius) { super(radius); // 父类只有一个有参构造函数 } @Override public int compareTo(Object o) { // 当前对象和对象o比较 //1. 检验 o 是否为 Circle 类型 if(o instanceof Circle){ Circle circle = (Circle) o; // 强转 //2. 比较半径 return (int)(this.radius - circle.radius); }else { throw new NotCircleException("传入的不是一个圆"); // 下面返回0表示相等,但是如果传入的不是圆,就不严谨了 } // return 0; // 上面有else抛出异常了,此行就不能要了,必须注释掉 } }
package com.uncleyong; interface CompareObject{ //若返回值是 0 , 代表相等; 若为正数,代表当前对象大;负数代表当前对象小 public int compareTo(Object o); // 当前对象和对象o比较 }
package com.uncleyong; public class NotCircleException extends RuntimeException { public NotCircleException(){ } public NotCircleException(String msg){ super(msg); } }
package com.uncleyong; public class TestInterface { public static void main(String[] args) { ComparableCircle cc1 = new ComparableCircle(2); ComparableCircle cc2 = new ComparableCircle(7); ComparableCircle cc3 = new ComparableCircle(2); ComparableCircle cc4 = new ComparableCircle(1); ComparableCircle cc5 = new ComparableCircle(2); ComparableCircle cc6 = new ComparableCircle(2); System.out.println(cc1.compareTo(cc2)); System.out.println(cc3.compareTo(cc4)); System.out.println(cc5.compareTo(cc6)); // 出异常 // ComparableCircle cc7 = new ComparableCircle(new Circle(1.8)); // 这样写,直接出编译时异常了,所以写为下面一行的样子 System.out.println(cc1.compareTo(new A())); } } class A{} /** * 程序运行结果: * Exception in thread "main" com.uncleyong.eg31.ex_eg26.NotCircleException: 传入的不是一个圆 * at com.uncleyong.eg31.ex_eg26.ComparableCircle.compareTo(ComparableCircle.java:22) * at com.uncleyong.eg31.ex_eg26.TestInterface.main(TestInterface.java:22) * -5 * 1 * 0 */
内部类及匿名内部类
用得不多,so。。。
内部类也可理解为一个成员变量,内部类享有和内部成员同样的待遇,可以用final,abstract,private等修饰
在Java中,允许一个类的定义位于另一个类的内部,前者称为内部类
内部类和外层封装它的类之间存在逻辑上的所属关系
Inner class一般用在定义它的类或语句块之内,在外部引用它时必须给出完整的名称。 Inner class的名字不能与包含它的类名相同;
Inner class可以使用包含它的类的静态和实例成员变量,也可以使用它所在方法的局部变量;
Inner class可以声明为抽象类 ,因此可以被其它的内部类继承。也可以声明为final的。
和外层类不同,Inner class可以声明为private或protected;
Inner class 可以声明为static的,但此时就不能再使用外层封装类的非static的成员变量;
非static的内部类中的成员不能声明为static的,只有在顶层类或static的内部类中才可声明static成员。
示例
package com.uncleyong; public class OuterClass { int age; // 静态内部类,不可以引用外部类非静态成员,因为有静态内部类的时候,可能还没外部类的对象 static class StaticInnerClass{ void test(){ // System.out.println(age); // 报错 } } String name = "-1"; // 写为static String name = "-1";,也可以被非静态内部类引用 // 非静态内部类,可以引用外部类的非静态和静态成员 class InnerClass{ String name = "0"; public void test(){ String name = "1"; System.out.println(name); //1 System.out.println(this.name); //0 System.out.println(OuterClass.this.name); //-1 } } }
package com.uncleyong; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class TestInnerClass { public static void main(String[] args) { OuterClass.StaticInnerClass sic = new OuterClass.StaticInnerClass(); // 静态内部类的创建: 不再需要外部类的实例 OuterClass oc = new OuterClass(); // 非静态内部类的创建, 先创建外部类的实例, 再通过 外部类名.new 创建内部类的实例 OuterClass.InnerClass in = oc.new InnerClass(); in.test(); // InvocationHandler是一个接口,直接创建接口new InvocationHandler(),同时提供其方法的实现 // 其实是创建了一个内部类对象,但是不知道其名字(有对象名,没内部类名),所以叫匿名内部类对象 InvocationHandler invocationHandler = new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return null; } }; // 写得更确切一点(纯匿名内部类对象,连对象的引用都没有;创建一个接口的对象,直接提供其方法的实现) Proxy.newProxyInstance(null, null, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return null; } }); } }
练习题
https://www.cnblogs.com/uncleyong/p/15828510.html
__EOF__
关于博主:擅长性能、全链路、自动化、企业级自动化持续集成(DevTestOps)、测开等
面试必备:项目实战(性能、自动化)、简历笔试,https://www.cnblogs.com/uncleyong/p/15777706.html
测试提升:从测试小白到高级测试修炼之路,https://www.cnblogs.com/uncleyong/p/10530261.html
欢迎分享:如果您觉得文章对您有帮助,欢迎转载、分享,也可以点击文章右下角【推荐】一下!