本章主要知识结构如下:
this
与super
- 构造方法的多态
- 抽象类与接口
- 引用
- 内部类
第一题:实验:利用IDE的debug功能给例6.4和例6.6的new语句设置断点,使用单步调试(step into/step over)跟踪子类对象实例化(初始化)的执行顺序,并总结该过程。
1、例6.4
代码与对象初始化的过程标注如下(注释里面的数字表示对象初始化过程中该句代码的执行顺序):
class AddClass {
public int x = 0, y = 0, z = 0; // 5 当执行到构造函数的定义以后,便执行此句代码
AddClass(int x) { // 4 执行到了AddClass类的第一个构造函数,接下来要执行非静态代码块,注意此时并未执行(this.x=x)这一句
this.x = x; // 6 执行完非静态代码以后再执行构造函数内的实现
}
AddClass(int x, int y) {
this(x); // 3
this.y = y; // 7 步骤3已结束,执行此步
}
AddClass(int x, int y, int z) {
this(x, y); // 2
this.z = z; // 8 步骤2已结束,执行此步
}
public int add() {
return x + y + z;
}
}
public class SonAddClass extends AddClass {
int a = 0, b = 0, c = 0; // 9 已经执行完父类的构造函数,接下来需要先执行本类的非静态代码,再执行本类构造函数内部的代码
SonAddClass(int x) {
super(x);
a = x + 7;
}
SonAddClass(int x, int y) {
super(x, y);
a = x + 5;
b = y + 5;
}
SonAddClass(int x, int y, int z) {
super(x, y, z); // 1 首先执行此构造函数,然后调用父类的构造函数
a = x + 4; // 10 执行本类构造函数内的代码,此时已经执行完本类的非静态代码
b = y + 4; // 11
c = z + 4; // 12
}
public int add() {
System.out.println("super:x+y+z=" + super.add());
return a + b + c;
}
public static void main(String[] args) {
SonAddClass p1 = new SonAddClass(2, 3, 5); // 0 对此句对象初始化进行debug,下面以数字记录初始化过程
SonAddClass p2 = new SonAddClass(10, 20); // 13 上面的new语句共执行了12个步骤,接下来便执行此句初始化过程
SonAddClass p3 = new SonAddClass(1);
System.out.println("a+b+c=" + p1.add());
System.out.println("a+b=" + p2.add());
System.out.println("a=" + p3.add());
}
}
结果输出为:
super:x+y+z=10
a+b+c=22
super:x+y+z=30
a+b=40
super:x+y+z=1
a=8
对例6.4中对象初始化过程进行总结:
- 对象初始化时首先执行构造函数的声明处(不执行构造函数内部的定义代码),然后执行类内部的非静态代码,当执行完非静态代码定义以后再执行构造函数内的代码。
- 子类继承了父类的情况下,先执行子类构造函数,若子类构造函数中通过
spuer
关键字调用了父类的构造函数则执行对应的父类的构造函数,若子类构造函数中没有使用super
显式调用父类的构造函数,则会默认调用父类的无参构造函数。
以下代码可以证明这一点:class New{ public New(){ System.out.println("无参"); } public New(String s){ System.out.println("参数是"+s); } } class SubNew extends New{ public SubNew(){ System.out.println("子类的构造函数"); // 子类中没有显示调用父类构造函数,则会默认调用父类的无参构造函数 } } public class Test{ public static void main(String[] args){ SubNew sn = new SubNew(); } } 输出为: 无参 子类的构造函数
2、例6.6
对例6.6进行探索的代码如下(注释里面的数字表示代码的执行顺序):
class Pare {
int i = 3; // 5
Pare() { // 4
System.out.println("super"); // 6
}
}
public class Construct extends Pare {
int i = 8; // 7
Construct() { // 3
System.out.println("this"); // 8
}
Construct(int num) {
this(); // 2
} // 9
int getSuper() {
return super.i;
};
public static void main(String[] args) {
Construct ct = new Construct(9); // 1 对此句进行探索
System.out.println(ct.i); // 10
System.out.println(ct.getSuper());
}
}
输出为:
super
this
8
3
对例6.6进行总结:
- 第一点与例6.4的总结的第一条相同:对象初始化时先执行到构造函数的声明处,再执行非静态代码,最后执行构造函数里面的代码;
- 当子类中的属性与父类同名时,若直接使用属性名调用属性则默认调用的是子类的属性,等同于
this.属性名
,若需要调用父类的属性则可以使用super.属性名
;
第二题:如何实现两个对象之间互发消息,请举例说明。
可以通过类的this
关键字来实现对象之间互发消息。
举例来说要创建两个类(A, B),A类中含有B类的对象引用,B类中含有A类的对象引用,这样A类的对象便可以调用B类的对象引用,从而像B类对象发消息。
第三题:谈谈组合与继承的区别以及两者的使用场景(即什么时候宜用组合?什么时候宜用继承?)。
组合:是指在一个类中拥有另一个类的对象引用,这样便可以通过对象引用调用另一个类的公有属性和方法。
继承:子类继承父类时,便继承了父类所有的属性和方法(也包括private修饰的属性和方法,只是子类看不到private修饰的属性和方法)。
二者的使用场景:
继承通常是用来对父类的属性和方法进行完善,来创建出很具体的类;
在仅仅需要使用某个类的某些方法时,而不对该类的这些方法进行修改的情况下使用组合最方便。
第四题:Java中的运行时多态的含义是什么?有什么作用?请举例说明。
Java中多态有两种,编译时多态和运行时多态。
编译时多态:是指在编译时就可以确定要执行哪个方法,方法重载就属于编译时多态,此外对象引用指向了本类的对象(与父类的对象引用指向子类对象相对)时也属于编译时多态。
运行时多态:在运行时才可以确定执行哪个方法的多态即为运行时多态。
请看以下例子:
class Person{
String type = "P";
public String getName() {
String name = "Person";
return name;
}
}
class Man extends Person{
String type = "M";
public String getName(){
String name = "Man";
return name;
}
public String newName(){
return "newName";
}
}
public class Test {
public static void main(String[] args) {
Person p = new Man(); // 父类的对象引用指向了子类对象
System.out.println(p.type); //返回结果为P
System.out.println(p.getName()); //返回结果为Man
System.out.println(((Man)p).newName()); //
}
}
当父类的对象引用指向了子类对象时(即上述代码Person p = new Man();
),若子类对象对父类对象的某个方法进行了覆盖,则通过父类对象引用调用被子类覆盖的方法时会调用子类的方法,若子类没有对该方法进行覆盖则会调用父类的方法。
若子类中具有新的父类中没有的方法,则父类的对象引用无法调用子类中新的方法,若父类对象引用想要调用子类中新的方法(即上述main
函数中的最后一句代码),需要进行强制类型转换,将父类对象引用转换为子类对象引用,即必须将Person
对象引用p
转为Man
对象引用类型,因为newName
方法是子类中特有的,父类没有这个方法。注意此处的转换格式!需要使用((Man)p)
整体来调用newName
方法
参考链接:Java 编译时多态和运行时多态
第五题:使用接口改写例6.8中的程序。
例6.8(未改写,使用了抽象类)代码如下:
abstract class Shapes {
protected int x, y, k;
protected double m;
public Shapes(int x, int y, int k, double m) {
this.x = x;
this.y = y;
this.k = k;
this.m = m;
}
abstract public double getArea();
abstract public double getPerimeter();
}
class Rect extends Shapes {
public double getArea() {
return (k * m);
}
public double getPerimeter() {
return (2 * k + 2 * m);
}
public Rect(int x, int y, int width, int height) {
super(x, y, width, height);
}
}
class Triangle extends Shapes {
public double getArea() {
return (Math.sqrt(m * (m - k) * (m - x) * (m - y)));
}
public double getPerimeter() {
return (k + x + y);
}
public Triangle(int baseA, int baseB, int baseC) {
super(baseA, baseB, baseC, 0);
// m充当了周长的一半
m = (baseA + baseB + baseC) / 2.0;
}
}
class Circle extends Shapes {
public double getArea()
{
return (m * m * Math.PI); // Math是java.lang包中的类,PI是静态其属性,其值为Π
}
public double getPerimeter() {
return (2 * Math.PI * m);
}
public Circle(int x, int y, int width) {
// k充当了直径,m充当了半径的角色
super(x, y, width, width / 2.0);
}
}
public class RunShape{
public static void show(Shapes s){
System.out.println(s.getClass().getName()+" Area: "+s.getArea());
System.out.println(s.getClass().getName()+" Perimeter: "+s.getPerimeter());
}
public static void main(String[] args){
Shapes s1 = new Rect(5,15,25,25);
Shapes s2 = new Triangle(5,5,8);
Shapes s3 =new Circle(13,90,25);
show(s1);
show(s2);
show(s3);
}
}
输出结果为:
Rect Area: 625.0
Rect Perimeter: 100.0
Triangle Area: 12.0
Triangle Perimeter: 18.0
Circle Area: 490.8738521234052
Circle Perimeter: 78.53981633974483
上述代码中涉及到了父类引用指向子类对象(即代码Shapes s1 = new Rect(5,15,25,25);
Shapes s2 = new Triangle(5,5,8);
Shapes s3 =new Circle(13,90,25);
),这种引用是非常方便的,即所有父类引用可以引用子类对象,所有接口可以引用接口的实现类。
使用接口改写以后的代码如下:
- 首先在Shapes.java文件中写入以下代码:
package myinterface; public interface Shapes { abstract public double getArea(int x, int y, int k, double m); abstract public double getPerimeter(int x, int y, int k, double m); } class Rect implements Shapes { public double getArea(int x, int y, int k, double m) { return (k * m); } public double getPerimeter(int x, int y, int k, double m) { return (2 * k + 2 * m); } } class Triangle implements Shapes { public double getArea(int x, int y, int k, double m) { m = (x + y + k) / 2.0; return (Math.sqrt(m * (m - k) * (m - x) * (m - y))); } public double getPerimeter(int x, int y, int k, double m) { return (k + x + y); } } class Circle implements Shapes { public double getArea(int x, int y, int k, double m) { m = k / 2.0; return (m * m * Math.PI); // Math是java.lang包中的类,PI是静态其属性,其值为Π } public double getPerimeter(int x, int y, int k, double m) { m = k / 2.0; return (2 * Math.PI * m); } }
- 然后在RunShape.java文件中写入如下代码:
package myinterface; public class RunShapeIn{ public static void show(Shapes s, int x, int y, int k, double m){ System.out.println(s.getClass().getName()+" Area: "+s.getArea(x,y,k,m)); System.out.println(s.getClass().getName()+" Perimeter: "+s.getPerimeter(x,y,k,m)); } public static void main(String[] args){ Shapes s1 = new Rect(); Shapes s2 = new Triangle(); Shapes s3 =new Circle(); show(s1, 5,15,25,25); show(s2, 5,5,8,0); show(s3, 13,90,25,0); } }
改写以后输出结果为:
myinterface.Rect Area: 625.0
myinterface.Rect Perimeter: 100.0
myinterface.Triangle Area: 12.0
myinterface.Triangle Perimeter: 18.0
myinterface.Circle Area: 490.8738521234052
myinterface.Circle Perimeter: 78.53981633974483
可以看到,结果和改写之前是一样的,不同的地方在于类名前面加了包的名字。
第六题:简述运算符instanceof的使用场景。
先说下instanceof
的用法
instanceof
是二元运算符,使用格式为A instanceof B
,目的是判断对象A
是不是类B
的实例,A
和B
的关系不同时一般有以下三种情况:
A
是本类对象,B
是本类或父类:此时A instanceof B
返回布尔值true
;A
是父类对象,B
是子类:此时A instanceof B
返回布尔值false
;A
是对象,B
是与A
类无关(没有继承关系)的类:A instanceof B
不会编译通过,会如下错误:Incompatible conditional operand types A and B
使用场景:
当同一个父类(一般是抽象类)被多个子类继承时,需要定义函数来对不同的子类进行不同的操作,此时需要判断该函数所接收的对象是哪个子类的实例,便可以对不同子类的实例进行不同的操作,防止报错,也是多态的体现。