廉洁文化网站建设方案,做设计找图有哪些网站有哪些问题,做淘宝客没有网站怎么做,合肥seo网站优化培训文章目录 1. 继承1.1 继承概述#xff08;理解#xff09;1.2 如何继承#xff08;掌握#xff09;1.2.1 继承的语法格式1.2.2 具体举例 1.3 继承的相关特性#xff08;掌握#xff09;1.4 对继承自Object类的方法的测试#xff08;理解#xff09;1.5 难点解惑1.5.1 掌… 文章目录 1. 继承1.1 继承概述理解1.2 如何继承掌握1.2.1 继承的语法格式1.2.2 具体举例 1.3 继承的相关特性掌握1.4 对继承自Object类的方法的测试理解1.5 难点解惑1.5.1 掌握类和类继承之后的代码执行顺序1.5.2 子类继承父类之后能使用子类对象调用父类方法吗 1.6 练习1.6.1 第1题1.6.2 第2题 2. 方法覆盖和多态Polymorphism2.1 方法覆盖Override2.1.1 什么时候需要方法覆盖理解2.1.2 怎么进行方法覆盖掌握2.1.3 方法覆盖的条件及注意事项掌握2.1.4 方法覆盖的例子2.1.5 方法覆盖总结2.1.6 方法重载和方法覆盖重写的区别 2.2 多态2.2.1 多态基础语法掌握2.2.2 向下转型的风险以及instanceof2.2.3 多态存在的条件以及静态方法为何不谈方法覆盖2.2.4 私有方法不能覆盖2.2.5 多态在开发中的作用开闭OCP原则理解 2.3 难点解惑2.3.1 有了多态之后方法覆盖的返回值是否可以不一样呢2.3.2 多态机制的理解 2.4 练习2.4.1 实现愤怒的小鸟2.4.2 计算不同类型的员工薪资2.4.3 计算汽车租金 3.super3.1 super概述理解3.1.1 super不能单独使用3.1.2 super不能使用在静态方法中3.1.3 super使用在构造方法中掌握3.1.3.1 super()的默认调用3.1.3.2 父类的构造方法必被子类构造方法调用3.1.3.3 一个 java 对象在创建过程中比较完整的内存图变化3.1.3.4 super()作用的总结 3.1.4 super使用在实例方法中掌握 3.2 难点解惑3.3 练习 1. 继承
1.1 继承概述理解
继承是面向对象三大特征之一封装居首位封装之后形成了独立体独立体 A和独立体B 之间可能存在继承关系。其实程序中的继承灵感来自于现实生活在现实生活中继承处处可见如下
继承时子类继承父类的特征和行为使得子类对象实例具有父类的属性或子类从父类继承方法使得子类具有与父类相同的行为。 兔子和羊属于食草动物类狮子和豹属于食肉动物类。食草动物和食肉动物又是属于动物类。所以继承需要符合的关系是is-aRabbit is-a Animal【符合这种关系的就可以使用继承】父类更通用子类更具体。 虽然食草动物和食肉动物都是属于动物但是两者的属性和行为上有差别所以子类会具有父类的一般特性也会具有自身的特性。
为什么要使用继承机制在不同的类中也可能会有共同的特征和动作可以把这些共同的特征和动作放在一个类中让其它类共享。因此可以定义一个通用类然后将其扩展为其它多个特定类这些特定类继承通用类中的特征和动作。继承是 Java 中实现软件重用的重要手段避免重复易于维护。
1.2 如何继承掌握
1.2.1 继承的语法格式
class 类名 extends 父类名{类体;
}1.2.2 具体举例
接下来用以下例子来说明一下为什么需要继承
public class Account { //银行账户类//账号private String actno;//余额private double balance;//账号和余额的 set 和 get 方法public String getActno() {return actno;}public void setActno(String actno) {this.actno actno;}public double getBalance() {return balance;}public void setBalance(double balance) {this.balance balance;}
}public class CreditAccount { //信用账户类//账号private String actno;//余额private double balance;//账号和余额的 set 和 get 方法public String getActno() {return actno;}public void setActno(String actno) {this.actno actno;}public double getBalance() {return balance;}public void setBalance(double balance) {this.balance balance;}//信誉度特有属性private double credit;//信誉度的 set 和 get 方法public double getCredit() {return credit;}public void setCredit(double credit) {this.credit credit;}
}以上两个类分别描述了“银行账户类”和“信用账户类”信用账户类除了具有银行账户类的特征之外还有自己的特性按照以上代码的编写方式程序将会非常的臃肿可以修改“信用账户类”继承“银行账户类”
public class CreditAccount extends Account{ //信用账户类//信誉度特有属性private double credit;//信誉度的 set 和 get 方法public double getCredit() {return credit;}public void setCredit(double credit) {this.credit credit;}
}public class AccountTest {public static void main(String[] args) {CreditAccount act new CreditAccount();act.setActno(111111111);act.setBalance(9000.0);act.setCredit(100.0);System.out.println(act.getActno() 信用账户余额 act.getBalance() 元 信誉度为 act.getCredit());}
}运行结果 通过以上的代码可以看到继承可以解决代码臃肿的问题。换句话说继承解决了代码复用的问题代码复用就是代码的重复利用这是继承机制最基本的作用。 除此之外继承还有非常重要的两个作用那就是有了继承之后才会衍生出方法的覆盖和多态机制。后续学习
1.3 继承的相关特性掌握
继承需要理解和记忆的特性
B类继承 A类则称 A类为超类(superclass)、父类、基类B类则称为子类(subclass)、派生类、扩展类。java 中的继承只支持单继承不支持多继承C中支持多继承这也是 java 体现简单性的一点换句话说java 中不允许这样写代码class B extends A,C{ }。虽然 java 中不支持多继承但有的时候会产生间接继承的效果例如class C extends Bclass B extends A也就是说C 直接继承 B其实 C 还间接继承 A。java 中规定子类继承父类除构造方法不能继承、private修饰的数据无法在子类中直接访问外剩下都可以继承。java 中的类没有显示的继承任何类则默认继承 Object 类Object 类是 java 语言提供的根类老祖宗类其没有父类也就是说一个对象与生俱来就有 Object 类型中所有的特征。继承也存在一些缺点例如CreditAccount 类继承 Account 类会导致它们之间的耦合度非常高Account 类发生改变之后会马上影响到CreditAccount 类。
1.4 对继承自Object类的方法的测试理解
查看一下Object类的部分源代码 测试一下从Object类中继承过来的toString()方法
public class ExtendsTest{public static void main(String[] args) {ExtendsTest et new ExtendsTest();String s et.toString();System.out.println(s);}
}运行结果 可以看到toString()方法确实被 ExtendsTest 类继承过来了。
1.5 难点解惑
1.5.1 掌握类和类继承之后的代码执行顺序
请看如下代码
public class Test {public static void main(String[] args) {new H2();}
}
class H1{public H1(){System.out.println(父类构造);}static{System.out.println(父类静态代码块);}{System.out.println(父类代码块);}
}
class H2 extends H1{static{System.out.println(子类静态代码块);}{System.out.println(子类代码块);}public H2(){System.out.println(子类构造);}
}运行结果 分析 子类 H2 继承 H1new H2()执行的时候会先进行类加载先加载 H2 的父类 H1所以 H1 当中的静态代码块先执行然后再执行 H2 中的静态代码块静态代码块执行结束之后不会马上执行构造方法代码块会先执行Java 中有一条规则子类构造方法执行前先执行父类的构造方法学习 super 之后就知道了所以父类 H1 的代码块先执行再执行 H1 的构造方法然后再执行 H2 的代码块最后执行 H2 的构造方法。
1.5.2 子类继承父类之后能使用子类对象调用父类方法吗
答这种问法本身就存在问题本质上子类继承父类之后是相当于将从父类继承的方法自己复刻一份归自己所有实际上其调用的就是子类自己的方法不是父类的。
1.6 练习
1.6.1 第1题
定义猴子类猴子有名字和性别等属性并且定义猴子说话的方法定义人类人有名字和性别等属性并且定义人说话的方法。使用继承让代码具有复用性。
代码
class Monkey{public String name;public char sex;public Monkey(){}public Monkey(String name, char sex) {this.name name;this.sex sex;}public String getName() {return name;}public void setName(String name) {this.name name;}public char getSex() {return sex;}public void setSex(char sex) {this.sex sex;}public void talk(){System.out.println(this.name 在吼叫~);}
}
class People01 extends Monkey{public People01(String name, char sex) {this.name name;this.sex sex;}public void talk(){System.out.println(this.name 咿咿呀呀~);}
}
public class InheritTest01 {public static void main(String[] args) {Monkey monkey new Monkey(Dai, 母);monkey.talk();People01 people new People01(Li, 女);people.talk();}
}运行结果
1.6.2 第2题
定义动物类动物有名字属性并且定义动物移动的方法定义鱼类鱼有名字属性有颜色属性并且定义鱼移动的方法。使用继承让代码具有复用性。
代码
class Animal{public String name;public Animal(){}public Animal(String name) {this.name name;}public String getName() {return name;}public void setName(String name) {this.name name;}public void move(){System.out.println(this.name 在移动~);}
}
class Fish extends Animal{public String color;public Fish(String name, String color){this.name name;this.color color;}public void move(){System.out.println(this.name 在游动~);}
}
public class InheritTest02 {public static void main(String[] args) {Animal animal new Animal(蚂蚁);animal.move();Fish fish new Fish(金鱼,橙色);fish.move();}
}
运行结果 以上父类空的构造方法是必须要的否则会编译报错这部分后续再学习
2. 方法覆盖和多态Polymorphism
2.1 方法覆盖Override
2.1.1 什么时候需要方法覆盖理解
首先先来回顾一下方法重载overload
什么情况下考虑使用方法重载呢 在同一个类当中如果功能相似尽可能将方法名定义的相同这样方便调用的同时代码也会美观。代码满足什么条件的时候能够构成方法重载呢 只要在同一个类当中方法名相同参数列表不同类型、个数、顺序即构成方法重载。
带着同样的疑问去学习方法覆盖什么是方法覆盖什么情况下考虑方法覆盖代码怎么写的时候就构成了方法覆盖呢接下来看一段代码
public class People {private String name;public String getName() {return name;}public void setName(String name) {this.name name;}public void speakHi(){System.out.println(this.name 和别人打招呼!);}
}public class ChinaPeople extends People {
//中国人
}public class AmericaPeople extends People {
//美国人
}public class PeopleTest {public static void main(String[] args) {ChinaPeople cp new ChinaPeople();cp.setName(张三);cp.speakHi();AmericaPeople ap new AmericaPeople();ap.setName(jackson);ap.speakHi();}
}运行结果 “中国人”调用 speakHi()方法希望输出的结果是“你好我叫张三很高兴见到你”而“美国人”调用 speakHi()方法更希望输出的结果是“Hi,My name is jackson,Nice to meet you”可见 ChinaPeople 和 AmericaPeople 从父类中继承过来的 speakHi()方法已经不够子类使用了此时就需要使用方法覆盖机制了。
2.1.2 怎么进行方法覆盖掌握
接上情景如何进行方法覆盖如下代码【保持People类PeopleTest测试类不变】修改如下
public class ChinaPeople extends People {public void speakHi(){System.out.println(你好我叫this.getName()很高兴认识你);}
}public class AmericaPeople extends People {public void speakHi(){System.out.println(Hi,My name is this.getName(),Nice to meet you!);}
}修改后的运行结果
以上程序中 ChinaPeople 和 AmericaPeople 将从 People 类中继承过来的 speakHi()方法进行了覆盖我们也看到了当 speakHi()方法发生覆盖之后子类对象会调用覆盖之后的方法不会再去调用之前从父类中继承过来的方法。
在什么情况下我们会考虑使用方法覆盖呢只有当从父类中继承过来的方法无法满足当前子类业务需求的时候需要将父类中继承过来的方法进行覆盖。换句话说父类中继承过来的方法已经不够用了子类有必要将这个方法重新再写一遍所以方法覆盖又被称为方法重写。当该方法被重写之后子类对象一定会调用重写之后的方法。
2.1.3 方法覆盖的条件及注意事项掌握
当程序具备哪些条件的时候就能构成方法覆盖呢【记住】 ① 首要条件方法覆盖发生在具有继承关系的父子类之间 ② 覆盖之后的方法与原方法具有相同的返回值类型、相同的方法名、相同的形式参数列表
另外在使用方法覆盖的时候需要有哪些注意事项呢 ① 由于覆盖之后的方法与原方法一模一样建议在开发的时候采用复制粘贴的方式不建议手写因为手写的时候非常容易出错比如在 Object 类当中有 toString()方法该方法中的 S 是大写的在手写的时候很容易写成小写 tostring()这个时候你会认为 toString()方法已经被覆盖了但由于方法名不一致导致最终没有覆盖这样就尴尬了 ② 私有的方法不能被继承所以不能被覆盖 ③ 构造方法不能被继承所以也不能被覆盖 ④ 覆盖之后的方法不能比原方法拥有更低的访问权限可以更高学习了访问控制权限修饰符之后就明白了 ⑤ 覆盖之后的方法不能比原方法抛出更多的异常可以相同或更少学习了异常之后就明白了 ⑥ 方法覆盖只是和方法有关和属性无关 ⑦ 静态方法不存在覆盖不是静态方法不能覆盖是静态方法覆盖意义不大学习了多态机制之后就明白了
2.1.4 方法覆盖的例子
业务需求定义一个动物类所有动物都有移动的行为其中猫类型的对象在移动的时候输出“猫在走猫步”鸟儿类型的对象在移动的时候输出“鸟儿在飞翔”但是猫类型的对象具有一个特殊的行为抓老鼠这个行为不是所有动物对象都有的是猫类型对象特有的。
public class Animal {public void move(){System.out.println(动物在移动);}
}public class Bird extends Animal {//方法覆盖public void move() { System.out.println(鸟儿在飞翔);}
}public class Cat extends Animal{//方法覆盖public void move() { System.out.println(猫在走猫步);}//子类特有public void catchMouse(){ System.out.println(猫在抓老鼠);}
}public class AnimalTest {public static void main(String[] args) {Cat cat new Cat();cat.move();cat.catchMouse();Bird bird new Bird();bird.move();}
}运行结果
2.1.5 方法覆盖总结
当父类中继承过来的方法无法满足当前子类业务需求的时候子类有必要将父类中继承过来的方法进行覆盖/重写。方法覆盖发生在具有继承关系的父子类之间。方法覆盖的时候要求相同的返回值类型、相同的方法名、相同的形式参数列表。方法覆盖之后子类对象在调用的时候一定会执行覆盖之后的方法。纠正一个错误观念 后续会学到的一个注解override可用于标注于子类覆盖的方法之上但是该注解并不是必须的只是为了增强可读性如上没有该注解只要满足了方法覆盖的条件就仍然是方法覆盖。
2.1.6 方法重载和方法覆盖重写的区别
方法重载是发生在同一个类中而方法覆盖发生在具有继承关系的父子类之间。方法重载是要求方法名相同参数列表不同而方法覆盖要求重写的方法必须和父类的方法一致方法名一致、参数列表一致、返回值类型一致。
2.2 多态
2.2.1 多态基础语法掌握
多态Polymorphism属于面向对象三大特征之一它的前提是封装形成独立体独立体之间存在继承关系从而产生多态机制。多态是同一个行为具有多个不同表现形式或形态的能力。 比如我们按下 F1 键这个动作
如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档如果当前在 Word 下弹出的就是 Word 帮助如果当前在 Windows 下弹出的就是 Windows 帮助和支持。
多态就是“同一个行为”发生在“不同的对象上”会产生不同的效果。
在 java 中允许这样的两种语法出现一种是向上转型Upcasting一种是向下转型Downcasting向上转型是指子类型转换为父类型又被称为自动类型转换向下转型是指父类型转换为子类型又被称为强制类型转换。 记住 在 java 中无论是向上转型还是向下转型两种类型之间必须要有继承关系没有继承关系情况下进行向上转型或向下转型的时候编译器都会报错。 看如下代码
public class Animal {public void move(){System.out.println(Animal move!);}
}public class Cat extends Animal{//方法覆盖public void move(){System.out.println(猫在走猫步);}//子类特有public void catchMouse(){System.out.println(猫在抓老鼠);}
}public class Bird extends Animal{//方法覆盖public void move(){System.out.println(鸟儿在飞翔);}//子类特有public void sing(){System.out.println(鸟儿在歌唱);}
}public class Test01 {public static void main(String[] args) {//创建 Animal 对象Animal a new Animal();a.move();//创建 Cat 对象Cat c new Cat();c.move();//创建鸟儿对象Bird b new Bird();b.move();}
}运行结果 java中还允许这样写代码
public class AnimalTest02 {public static void main(String[] args) {Animal a1 new Cat();a1.move();Animal a2 new Bird();a2.move();}
}运行结果 以上程序演示的就是多态多态就是“同一个行为move”作用在“不同的对象上”会有不同的表现结果。 java 中之所以有多态机制是因为 java 允许一个父类型的引用指向一个子类型的对象。也就是说允许这种写法Animal a2 new Bird()因为 Bird is a Animal 是能够说通的。其中Animal a1 new Cat()或者 Animal a2 new Bird()都是父类型引用指向了子类型对象都属于向上转型Upcasting或者叫做自动类型转换。
分析以上代码片段中的Animal a1 new Cat();a1.move();: java 程序包括编译和运行两个阶段分析 java 程序一定要先分析编译阶段然后再分析运行阶段。
在编译阶段编译器只知道 a1 变量的数据类型是 Animal那么此时编译器会去 Animal.class字节码中查找 move()方法发现 Animal.class 字节码中存在 move()方法然后将该 move()方法绑定到 a1 引用上编译通过了这个过程我们可以理解为“静态绑定”阶段完成了。紧接着程序开始运行进入运行阶段在运行的时候实际上在堆内存中new的对象是 Cat 类型也就是说真正在 move 移动的时候是Cat 猫对象在移动所以运行的时候就会自动执行 Cat 类当中的move()方法这个过程可以称为“动态绑定”。但无论是什么时候必须先“静态绑定”成功之后才能进入“动态绑定”阶段。
简单来说多态就是程序在编译时一种形态在运行时又是另一种形态。
再看一下以下代码
public class AnimalTest03 {public static void main(String[] args) {Animal a new Cat();a.catchMouse();}
}编译报错 分析因为Animal a new Cat();在编译的时候编译器只知道 a 变量的数据类型是 Animal也就是说它只会去Animal.class 字节码中查找 catchMouse()方法结果没找到自然“静态绑定”就失败了编译没有通过。就像以上述的错误信息一样在类型为 Animal 的变量 a 中找不到方法catchMouse()。 可以修改如下
public class AnimalTest04 {public static void main(String[] args) {//向上转型Animal a new Cat();//向下转型为了调用子类对象特有的方法Cat c (Cat)a;c.catchMouse();}
}运行结果 可以看到直接使用 a 引用是无法调用 catchMouse()方法的因为这个方法属于子类 Cat中特有的行为不是所有 Animal 动物都可以抓老鼠的要想让它去抓老鼠就必须做向下转型Downcasting也就是使用强制类型转换将 Animal 类型的 a 引用转换成 Cat 类型的引用cCat c (Cat)a;使用 Cat 类型的 c 引用调用 catchMouse()方法。
结论只有在访问子类型中特有数据的时候需要先进行向下转型。
2.2.2 向下转型的风险以及instanceof
向下转型存在的风险
public class AnimalTest05 {public static void main(String[] args) {Animal a new Bird();Cat c (Cat)a;}
}以上编译可以通过但是运行报错
分析因为编译器只知道 a 变量是Animal类型Animal类和Cat类之间存在继承关系所以可以进行向下转型前面提到过只要两种类型之间存在继承关系就可以进行向上或向下转型语法上没有错误所以编译通过了。但是运行的时候会出问题吗因为毕竟a引用指向的真实对象是一只小鸟。【即是运行阶段的“动态绑定”出现了问题产生的ClassCastException是运行时异常类型转换异常需要记住这个异常。】 为了避免这种异常的发生建议在进行向下转型之前进行运行期类型判断这就需要我们学习一个运算符了它就是 instanceof其语法格式如下
(引用 instanceof 类型)举例
public class Test05 {public static void main(String[] args) {Animal a new Bird();if(a instanceof Cat){Cat c (Cat)a;c.catchMouse();}else if(a instanceof Bird){Bird b (Bird)a;b.sing();}}
}运行结果 在实际开发中java 中有这样一条默认的规范需要大家记住在进行任何向下转型的操作之前要使用 instanceof 进行判断请保持这个良好的编程习惯。
2.2.3 多态存在的条件以及静态方法为何不谈方法覆盖
多态存在的三个必要条件: ① 继承 ② 方法覆盖 ③ 父类型引用指向子类型对象
多态显然是离不开方法覆盖机制的多态就是因为编译阶段绑定父类当中的方法程序运行阶段自动调用子类对象上的方法如果子类对象上的方法没有进行重写这个时候创建子类对象就没有意义了自然多态也就没有意义了只有子类将方法重写之后调用到子类对象上的方法产生不同效果时多态就形成了。实际上方法覆盖机制和多态机制是捆绑的谁也离不开谁多态离不开方法覆盖方法覆盖离开了多态也就没有意义了。
再看看为何有之前的方法覆盖主要是说实例方法静态方法为何不谈方法覆盖 先看看如下的例子
public class OverrideTest {public static void main(String[] args) {Math.sum();MathSubClass.sum();}
}class Math{public static void sum(){System.out.println(Maths sum execute!);}
}class MathSubClass extends Math{//尝试覆盖从父类中继承过来的静态方法public static void sum(){System.out.println(MathSubClasss sum execute!);}
}运行结果
貌似上面的代码也发生了覆盖在程序运行的时候确实也调用了“子类MathSubClass”的sum方法但这种“覆盖”有意义吗其实前面已经学习过方法覆盖和多态机制联合起来才有意义我们来看看这种“覆盖”是否能够达到“多态”的效果请看代码
public class OverrideTest {public static void main(String[] args) {Math m new MathSubClass();m.sum();m null;m.sum();}
}运行结果 通过以上的代码我们发现虽然创建了子类型对象new MathSubClass()但是程序在运行的时候仍然调用的是 Math 类当中的 sum 方法甚至 m null 的时候再去调用 m.sum()也没有出现空指针异常这说明静态方法的执行压根和对象无关既然和对象无关那就表示和多态无关既然和多态无关也就是说静态方法的“覆盖”是没有意义的所以通常我们不谈静态方法的覆盖。
2.2.4 私有方法不能覆盖
举例如下
public class OverrideTest02 {// 私有方法private void doSome(){System.out.println(OverrideTest02s private method doSome execute! );}public static void main(String[] args) {//多态OverrideTest02 ot new T();ot.doSome();}
}class T extends OverrideTest02{//尝试重写父类中的doSome()方法//访问权限不能更低可以更高public void doSome(){System.out.println(Ts doSome execute!);}
}运行结果
2.2.5 多态在开发中的作用开闭OCP原则理解
先来了解一个业务背景请设计一个系统描述主人喂养宠物的场景首先在这个场景当中应该有“宠物对象”宠物对象应该有一个吃的行为另外还需要一个“主人对象”主人对象应该有一个喂的行为请看代码
//宠物狗
public class Dog {String name;public Dog(String name){this.name name;}//吃的行为public void eat(){System.out.println(this.name 在啃肉骨头);}
}//主人
public class Master {//喂养行为public void feed(Dog dog){//主人喂养宠物宠物就吃System.out.println(主人开始喂食儿);dog.eat();System.out.println(主人喂食儿完毕);}
}public class Test {public static void main(String[] args) {//创建狗对象Dog dog new Dog(二哈);//创建主人对象Master master new Master();//喂养master.feed(dog);}
}运行结果 以上程序编译和运行都很正常输出结果也是对的那么存在什么问题吗假设后期用户出了新的需求软件可能面临着功能扩展这个扩展会很方便吗假设现在主人家里又来了一个宠物猫那该怎么办呢新增了一个 Cat 类来表示宠物猫请看代码
//宠物猫
public class Cat {String name;public Cat(String name){this.name name;}//吃的行为public void eat(){System.out.println(this.name 在吃鱼);}
}除了增加一个 Cat 类之外我们还需要“修改”Master 主人类的源代码这件事儿 是我们程序员无法容忍的因为修改之前写好的源代码就面临着重新编译、重新全方位的测试这是一个巨大的工作维护成本很高也很麻烦
//主人
public class Master {//喂养行为public void feed(Dog dog){//主人喂养宠物宠物就吃System.out.println(主人开始喂食儿);dog.eat();System.out.println(主人喂食儿完毕);}//喂养行为public void feed(Cat cat){//主人喂养宠物宠物就吃System.out.println(主人开始喂食儿);cat.eat();System.out.println(主人喂食儿完毕);}
}public class Test {public static void main(String[] args) {//创建狗对象Dog dog new Dog(二哈);//创建主人对象Master master new Master();//喂养master.feed(dog);//创建猫对象Cat cat new Cat(汤姆);//喂养master.feed(cat);}
}运行结果 在软件开发过程中有这样的一个开发原则开闭原则。开闭原则OCP是面向对象设计中“可复用设计”的基石是面向对象设计中最重要的原则之一其它很多的设计原则都是实现开闭原则的一种手段。它的原文是这样“Software entities should be open for extension,but closed for modification”。翻译过来就是“软件实体应当对扩展开放对修改关闭”。把它讲得更通俗一点也就是软件系统中包含的各种组件例如模块Modules、类Classes以及功能Functions等等应该在不修改现有代码的基础上引入新功能。 开闭原则中“开”是指对于组件功能的扩展是开放的是允许对其进行功能扩展的开闭原则中“闭”是指对于原有代码的修改是封闭的即修改原有的代码对外部的使用是透明的。
以上程序在扩展的过程当中就违背了 OCP 原则因为在扩展的过程当中修改了已经写好的 Master 类怎样可以解决这个问题呢多态可以解决请看代码
//宠物类
public class Pet {String name;//吃的行为public void eat(){}
}//宠物猫
public class Cat extends Pet{public Cat(String name){this.name name;}//吃的行为public void eat(){System.out.println(this.name 在吃鱼);}
}//宠物狗
public class Dog extends Pet{public Dog(String name){this.name name;}//吃的行为public void eat(){System.out.println(this.name 在啃肉骨头);}
}//主人
public class Master {//喂养行为public void feed(Pet pet){//主人喂养宠物宠物就吃System.out.println(主人开始喂食儿);pet.eat();System.out.println(主人喂食儿完毕);}
}public class Test02 {public static void main(String[] args) {//创建狗对象Dog dog new Dog(二哈);//创建主人对象Master master new Master();//喂养master.feed(dog);//创建猫对象Cat cat new Cat(汤姆);//喂养master.feed(cat);}
}运行结果 如上使用到多态的部分
在以上程序中Master 类中的方法 feed(Pet pet)的参数类型定义为更加抽象的 Pet 类型而不是具体 Dog 宠物或者 Cat 宠物显然 Master 类和具体的 Dog、Cat 类解耦合了依赖性弱了这就是我们通常所说的面向抽象编程尽量不要面向具体编程面向抽象编程会让你的代码耦合度降低扩展能力增强从而符合 OCP 的开发原则 。假如说这会再来一个新的宠物鸟呢我们只需要这样做新增加一个“宠物鸟类”然后宠物鸟类 Bird 继承宠物类 Pet并重写 eat()方法然后修改一下测试类就行了整个过程我们是不需要修改 Master 类的只是额外增加了一个新的类。
总结一下多态的作用
多态在开发中联合方法覆盖一起使用可以降低程序的耦合度提高程序的扩展力。在开发中尽可能面向抽象编程不要面向具体编程。 好比电脑主板和内存条的关系一样主板和内存条件之间有一个抽象的符合某个规范的插槽不同品牌的内存条都可以插到主板上使用2 个 G 的内存条和 4 个 G 的内存条都可以插上但最终的表现结果是不同的2 个 G 的内存条处理速度慢一些4 个 G 的快一些这就是多态所谓多态就是同一个行为作用到不同的对象上最终的表现结果是不同的主要的要求就是对象是可以进行灵活切换的灵活切换的前提就是解耦合解耦合依赖多态机制。
2.3 难点解惑
2.3.1 有了多态之后方法覆盖的返回值是否可以不一样呢
经过测试结果如下
对于返回值是基本数据类型的子类覆盖的方法的返回值必须与父类方法一致对于返回值是引用数据类型的子类覆盖的方法的返回值可以更小比如换成其子类本身但是意义不大在开发中一般不会修改。
2.3.2 多态机制的理解
多态的代码表现是父类型引用指向子类型对象对于多态的理解一定要分为编译阶段和运行阶段来进行分析
编译阶段只是看父类型中是否存在要调用的方法如果父类中不存在则编译器会报错编译阶段和具体 new 的对象无关。但是在运行阶段就要看底层具体 new 的是哪个类型的子对象了new 的这个子类型对象可以看做“真实对象”自然在运行阶段就会调用真实对象的相关方法。 例如代码:
Animal a new Cat();
a.move();在编译阶段编译器只能检测到a的类型是Animal所以一定会去Animal类中找move()方法如果 Animal 中没有 move()方法则编译器会报错即使 Cat 中有 move()方法也会报错因为编译器只知道 a 的类型是 Animal 类只有在运行的时候实际创建的真实对象是 Cat那么在运行的时候就会自动调用 Cat 对象的 move()方法。这样就可以达到多种形态也就是说编译阶段一种形态运行阶段的时候是另一种形态。这也就是多态的含义。
2.4 练习
2.4.1 实现愤怒的小鸟
我们有很多种小鸟每种小鸟都有飞的行为还有一个弹弓弹弓有一个弹射的行为弹射时把小鸟弹出去之后小鸟使用自己飞行的行为飞向小猪不要求编写小猪的代码。 不同种类的小鸟有不同飞行的方式 1红火红色小鸟飞行方式正常 2蓝冰蓝色小鸟飞行方式分成 3 个 3黄风黄色小鸟飞行方式加速。
代码如下
public class Bird {public void fly(){System.out.println(小鸟在飞翔);}
}public class RedBird extends Bird{public void fly(){System.out.println(红色小鸟正常飞翔);}
}public class BlueBird extends Bird {public void fly(){System.out.println(蓝色小鸟分成三个飞翔);}
}public class YellowBird extends Bird{public void fly(){System.out.println(黄色小鸟加速飞翔);}
}public class Slingshot {public void shot(Bird bird){bird.fly();}
}public class Test {public static void main(String[] args) {Slingshot slingshot new Slingshot();Bird redBird new RedBird();Bird blueBird new BlueBird();Bird yellowBird new YellowBird();slingshot.shot(redBird);slingshot.shot(blueBird);slingshot.shot(yellowBird);}
}运行结果
2.4.2 计算不同类型的员工薪资
1定义员工类 Employee员工包含姓名 name、出生月份 birthMonth 两个属性员工有获取指定月份工资的方法getSalary(int month)如果该月员工生日公司补助 250 元。 2定义有固定工资的员工类 SalariedEmployee有月薪 monthlySalary 属性。 3定义小时工类 HourlyEmployee包含工作小时数 hours 和每小时的工资hourlySalary 属性如果每月工作超过 160 小时超过的部分按 1.5 倍工资发放。 4定义销售人员类 SalesEmployee包含月销售额 sales 和提成比例 comm 属性。
代码如下
/*** 员工类*/
public class Employee {String name; //姓名int birthMonth; // 出生月份public Employee(String name, int birthMonth) {this.name name;this.birthMonth birthMonth;}public String getName() {return name;}public void setName(String name) {this.name name;}public int getBirthMonth() {return birthMonth;}public void setBirthMonth(int birthMonth) {this.birthMonth birthMonth;}public double getSalary(int month){return 0;}
}/*** 固定工资的员工类*/
public class SalariedEmployee extends Employee{double monthlySalary; //月薪public SalariedEmployee(String name, int birthMonth, double monthlySalary) {super(name, birthMonth);this.monthlySalary monthlySalary;}public double getMonthlySalary() {return monthlySalary;}public void setMonthlySalary(double monthlySalary) {this.monthlySalary monthlySalary;}Overridepublic double getSalary(int month){if(birthMonth month){return getMonthlySalary() 250;}return getMonthlySalary();}
}/*** 小时工类*/
public class HourlyEmployee extends Employee{double hours; //工作小时数double hourlyWage; //每小时工资public HourlyEmployee(String name, int birthMonth, double hours, double hourlyWage) {super(name, birthMonth);this.hours hours;this.hourlyWage hourlyWage;}public double getHours() {return hours;}public void setHours(double hours) {this.hours hours;}public double getHourlyWage() {return hourlyWage;}public void setHourlyWage(double hourlyWage) {this.hourlyWage hourlyWage;}Overridepublic double getSalary(int month) {if(birthMonth month hours 160){return hourlyWage*160 hourlyWage*1.5*(hours-160)250;}else if(birthMonth month){return hourlyWage*hours 250;}else if(hours 160){return hourlyWage*160 hourlyWage*1.5*(hours-160);}return hourlyWage*hours;}
}/*** 销售人员类*/
public class SalesEmployee extends Employee {int sales; //月销售额double comm; //提成比例public SalesEmployee(String name, int birthMonth, int sales, double comm) {super(name, birthMonth);this.sales sales;this.comm comm;}public int getSales() {return sales;}public void setSales(int sales) {this.sales sales;}public double getComm() {return comm;}public void setComm(double comm) {this.comm comm;}Overridepublic double getSalary(int month){if(birthMonth month){return comm*sales 250;}return comm*sales;}
}public class Test {public static void main(String[] args) {Employee salariedEmployee new SalariedEmployee(杏子, 2, 2500);Employee hourlyEmployee new HourlyEmployee(栗子, 3, 240, 15);Employee salesEmplyee new SalesEmployee(李子, 4, 20, 200);double salariedSalary salariedEmployee.getSalary(2);double hourlySalary hourlyEmployee.getSalary(2);double saleSalary salesEmplyee.getSalary(2);System.out.println(salariedEmployee.getName() 的月工资 salariedSalary);System.out.println(hourlyEmployee.getName() 的月工资 hourlySalary);System.out.println(salesEmplyee.getName() 的月工资 saleSalary);}
}运行结果
2.4.3 计算汽车租金
某汽车租赁公司有多种汽车可以出租计算汽车租金 1Vehicle 是所有车的父类属性品牌、车牌号有返回总租金的方法public double getSumRent(int days){} 2小轿车类 Car 是 Vehicle 的子类属性车型两厢、三厢、越野两厢每天 300三厢每天 350越野每天 500。 3多座汽车类 Bus 是 Vehicle 的子类属性座位数座位数16 的每天 400座位数16的每天 600。 4编写测试类根据用户选择不同的汽车计算总租金。
代码如下
/*** 车类*/
public class Vehicle {String band; //品牌int carNumber ; //车牌号public Vehicle(String band, int carNumber) {this.band band;this.carNumber carNumber;}public String getBand() {return band;}public void setBand(String band) {this.band band;}public int getCarNumber() {return carNumber;}public void setCarNumber(int carNumber) {this.carNumber carNumber;}public double getSumRent(int days){return 0.0;}
}/*** 小轿车类*/
public class Car extends Vehicle {String type; //车型public Car(String band, int carNumber, String type) {super(band, carNumber);this.type type;}public String getType() {return type;}public void setType(String type) {this.type type;}Overridepublic double getSumRent(int days) {if(getType() 两厢){return 300*days;}else if(getType() 三厢){return 350*days;}return 500*days; //越野}
}/*** 多座汽车类*/
public class Bus extends Vehicle{int seatsNumber; //座位数public Bus(String band, int carNumber, int seatsNumber) {super(band, carNumber);this.seatsNumber seatsNumber;}public int getSeatsNumber() {return seatsNumber;}public void setSeatsNumber(int seatsNumber) {this.seatsNumber seatsNumber;}Overridepublic double getSumRent(int days) {if(getSeatsNumber() 16){return 400*days;}return 600*days;}
}public class Test {public static void main(String[] args) {Vehicle car new Car(奔驰, 232423,两厢);Vehicle bus new Bus(大众, 343525, 17);int rentDays 20;System.out.println(car.getBand() car.getCarNumber() 租 rentDays 天的总租金 car.getSumRent(rentDays));System.out.println(bus.getBand() bus.getCarNumber() 租 rentDays 天的总租金 bus.getSumRent(rentDays));}
}运行结果
3.super
3.1 super概述理解
this 和 super 对比
this
this 是一个引用保存内存地址指向自己。this 出现在实例方法中谁调用这个实例方法this 就代表谁this 代表当前正在执行这个动作的对象。this 不能出现在静态方法中。this 大部分情况下可以省略在方法中区分实例变量和局部变量的时候不能省略。“this(实际参数列表)”出现在构造方法第一行通过当前的构造方法去调用 本类当中其它的构造方法。
super
严格来说super 其实并不是一个引用它只是一个关键字super 代表了当前对象中从父类继承过来的那部分特征。 this 指向一个独立的对象super 并不是指向某个“独立”的对象假设张大明是父亲张小明是儿子有这样一句话大家都说张小明的眼睛、鼻子和父亲的很像。那么也就是说儿子继承了父亲的眼睛和鼻子特征那么眼睛和鼻子肯定最终还是长在儿子的身上。假设this指向张小明那么 super 就代表张小明身上的眼睛和鼻子。换句话说 super 其实是 this 的一部分。如下图所示张大明和张小明其实是两个独立的对象两个对象内存方面没有联系super 只是代表张小明对象身上的眼睛和鼻子因为这个是从父类中继承过来的在内存方面使用了 super 关键字进行了标记对于下图来说“this.眼睛”和“super.眼睛”都是访问的同一块内存空间。 super 和 this 都可以使用在实例方法当中。super 不能使用在静态方法当中因为 super 代表了当前对象上的父类型特征静态方法中没有 this肯定也是不能使用 super 的。super 也有这种用法“super(实际参数列表);”这种用法是通过当前的构造 方法调用父类的构造方法。
3.1.1 super不能单独使用
如下代码
public class SuperTest01 extends Object{//实例方法public void doSome(){System.out.println(this);System.out.println(super);}
}编译报错 通过以上的测试可以看出 this 是可以单独使用的引用但 super无法输出编译器提示super 要使用必须是super.xxx显然 super 并不指向独立的对象并不是保存某个对象的内存地址。
3.1.2 super不能使用在静态方法中
如下代码
public class SuperTest01 extends Object{//静态方法public static void doSome(){System.out.println(this);System.out.println(super.toString());}
}编译报错 通过以上的测试可以看出 this 和 super 都是无法使用在静态方法当中的。
3.1.3 super使用在构造方法中掌握
super 使用在构造方法中语法格式为super(实际参数列表)这行代码和this(实际参数列表)都是只允许出现在构造方法第一行 【记住】 所以这两行代码是无法共存的 。 super(实际参数列表)表示子类构造方法执行过程中调用父类的构造方法。
如下代码
public class People {String idCard;String name;boolean sex;public People(){}public People(String idCard,String name,boolean sex){this.idCard idCard;this.name name;this.sex sex;}
}public class Student extends People {//学号是子类特有的int sno;public Student(){}public Student(String idCard,String name,boolean sex,int sno){this.idCard idCard;this.name name;this.sex sex;this.sno sno;}
}public class StudentTest {public static void main(String[] args) {Student s new Student(12345x,jack,true,100);System.out.println(身份证号 s.idCard);System.out.println(姓名 s.name);System.out.println(性别 s.sex);System.out.println(学号 s.sno);}
}运行结果 观察以上子类和父类的有参构造方法 可以发现子类的构造方法前三行代码和父类构造方法中的代码是一样的接下来把子类的构造方法修改如下然后再运行测试程序运行结果与上一致 public Student(String idCard,String name,boolean sex,int sno){super(idCard, name, sex);this.sno sno;}注意 若此处父类People中三个属性均为私有的则子类直接使用super.属性名调用是不行的私有属性只能在本类中访问即使是子类也不能直接访问。 这时使用super(idCard,name,sex)就非常有必要了解决了这一问题。
总结通过以上学习super(实际参数列表);语法表示调用父类的构造方法代码复用性增强了另外一方面也是模拟现实世界当中的“要想有儿子必须先有父亲”的道理。不过这里的super(实际参数列表)在调用父类构造方法的时候从本质上来说并不是创建一个“独立的父类对象”而是为了完成当前对象的父类型特征的初始化操作。或者说通过子类的构造方法调用父类的构造方法是为了让张小明身上长出具有他父亲特点的鼻子和眼睛鼻子和眼睛初始化完毕之后具有父亲的特点但最终还是长在张小明的身上
3.1.3.1 super()的默认调用
如下代码
public class A {public A(){System.out.println(A 类的无参数构造方法执行);}
}public class B extends A {public B(){System.out.println(B 类的无参数构造方法执行);}
}public class C extends B{public C(){System.out.println(C 类的无参数构造方法执行);}
}public class Test {public static void main(String[] args) {new C();}
}运行结果 等效代码
public class A {public A(){//这里调用的是 Object 类中的无参数构造方法//因为 A类的父类是 Objectsuper(); System.out.println(A 类的无参数构造方法执行);}
}public class B extends A {public B(){super();System.out.println(B 类的无参数构造方法执行);}
}public class C extends B {public C(){super();System.out.println(C 类的无参数构造方法执行);}
}结论当一个构造方法第一行没有显示的调用super(实际参数列表)的话系统默认调用父类的无参数构造方法super()。当然前提是this(实际参数列表)也没有显示的去调用因为 super()和 this()都只能出现在构造方法第一行所以不能并存。
再测试一下如下代码
public class A {//有参数构造方法定义之后//系统则不再ᨀ供无参数构造方法public A(String s){}
}public class B extends A {public B(){}
}编译报错 原因B 类的构造方法第一行默认会调用super()而super()会调用父类 A 的无参数构造方法但由于父类 A 中提供了有参数构造方法导致无参数构造方法不存在从而编译报错了。所以在实际开发中还是建议程序员将无参数构造方法显示的定义出来这样就可以避免对象的创建失败了。
另外通过以上内容的学习还有如下结论在 java 语言当中无论是创建哪个 java对象老祖宗 Object 类中的无参数构造方法是必然执行的。
一个重要结论 当一个构造方法中的第一行既没有this()又没有super()的话默认会有一个super()表示通过当前子类的构造方法调用父类的午餐构造方法所以必须保证父类的无参构造方法是存在的。
3.1.3.2 父类的构造方法必被子类构造方法调用
如下代码
public class People {String name;String sex;public People(String name, String sex){this.name name;this.sex sex;}
}public class Student extends People{String id;public Student(String name, String sex) {this.name name;this.sex sex;}public Student(String id, String name, String sex){this(name,sex);this.id id;}
}编译报错
结论无论如何父类的构造方法一定会执行如果不在子类构造方法中显式调用父类有参构造即使是在子类中调用本类中有参构造该有参构造内也默认会调用父类的无参构造。
3.1.3.3 一个 java 对象在创建过程中比较完整的内存图变化
如下代码
public class People {String name;boolean sex;public People(String name, boolean sex){this.name name;this.sex sex;}
}public class Worker extends People{double salary;public Worker(String name, boolean sex, double salary){super(name,sex);this.salary salary;}
}public class WorkerTest {public static void main(String[] args) {Worker worker new Worker(lili,true, 20000);System.out.println(姓名 worker.name);System.out.println(性别 worker.sex);System.out.println(工资 worker.salary);}
}运行结果 以上程序Worker对象创建时构造方法的执行顺序
先执行 Object 类的无参数构造方法;再执行 People 类的构造方法最后执行 Worker 类的构造方法 注意虽然执行了三个构造方法但是对象实际上只创建了一个 Worker。 以上程序的内存结构图变化如下
3.1.3.4 super()作用的总结
调用父类的构造方法使用这个构造方法来给当前子类对象初始化父类型特征代码复用。
3.1.4 super使用在实例方法中掌握
如下代码
//书
public class Book {//书名String name;//构造方法public Book(){super();}public Book(String name){super();this.name name;}
}//纸质书
public class PaperBook extends Book {//构造方法public PaperBook(){super();}public PaperBook(String name){super();this.name name;}//打印书名public void printName(){System.out.println(this.name-书名 this.name);System.out.println(super.name-书名 super.name);}
}public class BookTest {public static void main(String[] args) {PaperBook book1 new PaperBook(零基础学 Java 卷 I);book1.printName();}
}运行结果
由以上代码发现printName()方法中的 super.name 和 this.name最终输出结果是一样的以上程序执行的内存图如下
1父类构造方法执行结束后的内存图
2子类构造方法执行结束后的内存图 通过以上内存结构图发现 this.name 和 super.name 实际上是同一块内存空间所以它们的输出结果是完全一样的。
修改一下PaperBook类
//纸质书
public class PaperBook extends Book {String name; //在子类中也定义了一个 name 属性//构造方法public PaperBook(){super();}public PaperBook(String name){super();this.name name;//这里的 this.name 代表子类的 name}//打印书名public void printName() {System.out.println(this.name-书名 this.name);System.out.println(super.name-书名 super.name);}
}运行结果 再看一下以上程序的内存图
1父类构造方法执行结束后的内存图
2子类构造方法执行结束后的内存图 可以发现父类Book的构造方法在执行时给 super.name 赋值null子类PaperBook 的构造方法在执行的时候给 this.name 赋值“零基础学 Java 卷 I”由于在子类 PaperBook 中定义了重名的变量 name 导致在当前对象中有两个name一个是从父类中继承过来的一个是自己的如果此时想访问父类中继承过来的 name 则必须使用 super.name当直接访问 name 或者 this.name 都表示访问当前对象自己的 name。
结论当父类中有该实例变量子类中又重新定义了同名的实例变量如果想在子类中访问父类的实例变量super 不能省略。
再测试一下实例方法
package SuperTest.superTest07;public class Vip {//Vip 默认继承 Object//重写从 Object 类中继承过来的 toString()方法public String toString(){return 我是超级会员;}public void test() {System.out.println(super.toString());System.out.println(this.toString());System.out.println(toString());}
}package SuperTest.superTest07;public class VipTest {public static void main(String[] args) {Vip vip new Vip();vip.test();}
}运行结果 由上代码发现在实例方法中也是如此。
最终结论父类和子类中有同名实例变量或者有同名的实例方法想在子类中访问父类中的实例变量或实例方法则super 是不能省略的其它情况都可以省略。
3.2 难点解惑 Java 中 super 存储的是一个父对象的内存地址吗this 保存了内存地址指向了当前对象那么 super也是保存了内存地址指向了当前对象的父对象吗 这个理解是错误的在 Java 程序中创建 Java 对象的时候会调用构造方法在构造方法执行之前会先调用父类的构造方法在这里说明一下调用父类的构造方法实际上并不是创建父类对象只是为了完成初始化当前子类对象的父类型特征。所以严格意义上来说 super 并不指向任何对象super 只是代表了当前对象中的那部分父类型特征单独输出 super例如System.out.println(super);是无法编译的。
3.3 练习
public class Text {public static int k 0;public static Text t1 new Text(t1);public static Text t2 new Text(t2);public static int i print(i);public static int n 99;public int j print(j);static {print(静态块);}public Text(String str) {System.out.println((k) : str i i n n);i;n;}public static int print(String str) {System.out.println((k) : str i i n n);n;return i;}public static void main(String args[]) {new Text(init);}
}程序加载过程总结
方法区进行类加载加载所需类的.class文件根据静态变量和静态代码块出现的顺序若静态代码块在前执行静态代码块中的内容后统一声明静态变量并给静态变量赋默认值再统一进行显式赋值若为静态变量在前先统一声明静态变量并给静态变量赋默认值再统一进行显式赋值然后执行静态代码块中的内容若有new 对象每new一次以下过程走一遍 3.1. 声明实例变量并进行默认初始化 3.2. 实例变量的显示赋值 3.3. 执行构造代码块 3.4. 执行构造方法
如果遇到extends要记住先初始化父类数据【父类也是按以上过程初始化】然后初始化子类数据。如下图
根据以上分析其实以上程序等价于
package SuperTest;
public class Text {public static int k ;public static Text t1;public static Text t2;public static int i;public static int n;public int j print(j);static {k 0;t1 new Text(t1);t2 new Text(t2);i print(i);n 99;print(静态块);}public Text(String str) {System.out.println((k) : str i i n n);i;n;}public static int print(String str) {System.out.println((k) : str i i n n);n;return i;}public static void main(String args[]) {new Text(init);}
}则代码运行过程详解如下
首先进行类加载需要将Text类、Object类、String类等要使用的类由JVM加载到其方法区中JVM加载Text类根据顺序需要先声明静态变量此时k0t1nullt2nulli0,n0;执行静态代码块中的代码k0而后触发t1的实例化【声明实例变量j并为其赋初值j0执行构造方法进入print(“j”)输出1:j i0 n0后k1n1i1返回j1而后输出2:t1 i1 n1后k2i2n2t1实例化完成】触发t2的实例化【声明实例变量j并为其赋初值j0执行构造方法进入print(“j”)输出3:j i2 n2后k3n3i3返回j3而后输出4:t2 i3 n3后k4i4n4t2实例化完成】为i显示赋值进入print(“i”)输出5:i i4 n4而后k5n5i5返回i5为n赋值99执行print(“静态块”)输出6:静态块 i5 n99而后k6n100i6进行new Text(“init”)【声明实例变量j并为其赋初值j0执行构造方法进入print(“j”)输出7:j i6 n100后k7n101i7而后输出8:init i7 n101后k8i8n102实例化完成】main执行结束。
所以代码最终运行结果为