Fork me on GitHub

抽象方法和抽象类

抽象类:没有直接对应的对象,表达的是抽象概念,一般是具体类的比较上层的父类。

比如,猫是具体对象,动物则是抽象概念;芒果是具体对象,图形是抽象概念。

抽象方法和抽象类

抽象方法和抽象类都是用abstract关键字来声明。

1
2
3
4
public abstract class Shape {
//...其他代码
public abstract void draw();
}

定义了抽象方法的类必须被声明为抽象类,但抽象类可以没有抽象方法

抽象类可以定义具体方法、实例变量等。

抽象类与具体类的核心区别:抽象类不能创建对象,比如不能使用new Shape(),而具体类可以。

抽象类不能创建对象,要创建对象,必须使用它的具体子类。

一个类在继承抽象类后,必须实现抽象类中定义的所有抽象方法,除非它自己也声明为抽象类。

Circle类的实现代码,如下所示:

1
2
3
4
5
6
7
public class Circle extends Shape {
//...其他代码
@Override
public void draw() {
//....
}
}

Circle实现了draw()方法。

与接口类似,抽象类虽然不能使用new,但可以声明抽象类的变量,引用抽象类具体子类的对象。

如下所示:

1
2
Shape shape = new Circle();
shape.draw();

shape是抽象类Shape类型的变量,引用了具体子类Circle的对象,调用draw方法将调用Circle的draw代码。

使用接口替代继承

使用组合替代继承,可以复用代码,但不能统一处理。使用接口,针对接口编程,可以实现统一处理不同类型的对象,但接口没有代码实现,无法复用代码。

将组合和接口结合起来,就既可以统一处理,也可以复用代码了。

举个栗子:

先增加一个接口IAdd:

1
2
3
4
public interface IAdd {
void add(int number);
void addAll(int[] numbers);
}

修改Base类代码,让它实现IAdd接口,代码基本不变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Base implements IAdd {
private static final int MAX_NUM = 1000;
private int[] arr = new int[MAX_NUM];
private int count;

public void add(int number) {
if (count < MAX_NUM) {
arr[count++] = number;
}
}

public void addAll(int[] numbers) {
for (int num : numbers) {
add(num);
}
}
}

修改Child类代码,也是实现IAdd接口,代码基本不变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Child implements IAdd{
private Base base;
private long sum;

public Child() {
base = new Base();
}

public void add(int number) {
base.add(number);
sum+=number;
}

public void addAll(int[] numbers) {
base.addAll(numbers);
for (int i=0;i<numbers.length;i++) {
sum+=numbers[i];
}
}

public long getSum() {
return sum;
}
}

接口的细节

接口的四项细节:

  • 接口中的变量

  • 接口的继承

  • 类的继承与接口

  • instanceof

接口中的变量

接口中可以定义变量:

1
2
3
public interface Interface1 {
public static final int a =0;
}

变量a可以通过“接口名.变量名”的方式使用,如Interface1.a。

接口的继承

接口可以继承别的接口,继承的基本概念与类一样。

与类不同的是,接口可以有多个父接口

1
2
3
4
5
6
7
8
9
10
public interface IBase1 {
void method1();
}

public interface IBase2 {
void method2();
}

public interface IChild extends IBase1,IBase2{
}

接口的继承同样使用extends关键字,多个父接口之间以逗号分割。

类的继承与接口

类的继承与接口可以共存——类可以在继承基类的情况下,同时实现一个或多个接口。

1
2
3
public class Child extends Base implements IChild {
...
}

entends要放在implements之前

instanceof

与类一样,接口也可以使用instanceof关键字,用来判断一个对象是否实现了某接口。

1
2
3
4
Point point = new Point(2,3);
if (point instanceof MyComparable) {
System.out.println("comparable");
}

针对接口而非具体类型的编程

在一些程序中,代码并不知道具体的类型,才是接口发挥威力的地方。

举个栗子,一个使用MyComparable接口的类CompUtil,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class CompUtil {
//找出最大值
public static Object max(MyComparable[] objs) {
//objs为空
if (objs == null || objs.length == 0) {
return null;
}
MyComparable max = objs[0];
for (int i=1;i<objs.length;i++) {
if (max.compareTo(objs[i]) < 0) {
max = objs[i];
}
}
return max;
}
//从小到大排序
public static void sort(MyComparable[] objs) {
for (int i=0;i<objs.length;i++) {
int min = i;
for (int j=i+1;j<objs.length;j++) {
if (objs[j].compareTo(objs[min])<0) {
min = j;
}
}
if (min!=i) {
MyComparable temp = objs[i];
objs[i] = objs[min];
objs[min] = temp;
}
}
}
}

CompUtil类提供了两个方法,max获取传入数组中的最大值,sort对数组进行升序排序,参数都是MyComparable类型的的数组。

这个类是针对MyComparable接口编程,它并不知道具体的类型是什么,也并不关心,但却可以对任意实现了MyComparable接口的类型进行操作。

接下来对Point类型进行操作:

1
2
3
4
5
6
7
8
9
Point[] points = new Point[] {
new Point(2,3),
new Point(3,4),
new Point(1,2)
};

System.out.println("max: " + CompUtil.max(points));
CompUtil.sort(points);
System.out.println("sort: " + Arrays.toString(points));

points可以作为MyComparable[]类型的参数。实际上,可以针对任何实现了MyComparable接口的类型数组进行操作。

针对接口而非具体类型进行编程,是计算机程序的一种重要思维方式。

接口的优点:

1.代码复用。同一套代码可以处理多种不同类型的对象,只要这些对象都有相同的能力。

2.降低了耦合,提高了灵活性。使用接口的代码依赖的是接口本身,而非实现接口的具体类型。

接口的本质

很多时候 ,我们实际上关心的,不是对象的类型,而是对象的能力

比如要将冷水加热,只要得到热水即可。至于是用电磁炉还是燃气灶还是电热壶加热,并不重要。重要的是对象是否有加热水的能力。

Java使用接口这个概念来表示能力。

接口声明了一组能力,但它自己并没有实现这个能力,它只是一个约定。

它涉及交互两方对象,一方需要实现这个接口,另一方使用这个接口,但双方对象并不直接互相依赖,它们只是通过接口间接交互。

通过接口间接交互

定义接口

首先定义一个用来比较的接口,叫MyComparable。

1
2
3
public interface MyComparable {
int compareTo(Object other); //比较自己和other,返回-1表示自己更小,0表示相同,1表示自己更大。
}

interface关键字声明接口,修饰符一般都是public。

MyComparable接口定义里,声明了一个方法compareTo,但没有定义方法体,接口都不实现方法。接口方法不需要加修饰符,加与不加都是public的,不能是别的修饰符。

接口与类不同,它的方法没有实现代码。接口还需要至少两个参与者,一个需要实现接口,另一个使用接口。

实现接口

类可以实现接口,表示类的对象具有接口所表示的能力。

假设让Point类计算出到原点的距离,再将距离进行比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Point implements MyComparable{
private int x;
private int y;

public Point(int x, int y) {
this.x = x;
this.y = y;
}

public double distance(){
return Math.sqrt(x*x+y*y);
}

public int getX() {
return x;
}

public int getY() {
return y;
}

@Override
public String toString() {
return "("+x+","+y+")";
}

//实现MyComparable接口
@Override
public int compareTo(Object other) {
if (!(other instanceof Point)) { //判断other是否是Point类型的实例
throw new IllegalArgumentException(); //非法参数异常
}
Point otherPoint = (Point)other;
double delta = distance() - otherPoint.distance();
if(delta < 0) {
return -1;
}else if (delta > 0) {
return 1;
}else {
return 0;
}
}
}

java使用implements关键字表示实现接口,前面是类名,后面是接口名。

java的普通类实现接口,必须要实现接口中声明的每一个方法。

一个类可以实现多个接口,表明类的对象具备多种能力,各个接口之间以逗号分隔,语法如下:

1
2
3
public class Test implements Interface1, Interface2 {
...
}

使用接口

与类不同,接口不能new,不能直接创建一个接口对象,对象只能通过类来创建。但可以声明接口类型的变量,引用实现了接口的类对象

例如:

1
2
3
4
//声明MyComparable类型的变量,引用Point对象。
MyComparable p1 = new Point(2,3);
MyComparable p2 = new Point(1,2);
System.out.println(p1.compareTo(p2));

p1和p2是MyComparable类型的变量,但引用了Point类型的对象,之所以能赋值是因为Point实现了MyComparable接口。

如果一个类型实现了多个接口,那这种类型的对象就可以被赋值给任一接口类型的变量。

p1和p2能且只能调用MyComparable接口的方法。实际执行时,执行的是具体实现类的代码。

破坏封装的继承是一把双刃剑

封装:隐藏实现细节。

继承可能破坏封装是因为子类和父类之间可能存在着实现细节的依赖。子类在继承父类的时候,往往不得不关注父类的实现细节,而父类在修改其内部实现的时候,如果不考虑子类,也往往会影响到子类。

对于子类而言,通过继承实现,是没有安全保障的,父类修改内部实现细节,它的功能就可能会被破坏。

对于父类而言,让子类继承和重写方法,就可能丧失随意修改内部实现的自由。

正确使用继承

编写子类时,需要注意的是:

1.重写方法不要改变预期的行为。

2.阅读文档说明,理解可重写方法的实现机制,尤其是方法之间的调用关系。

3.在基类修改的情况下,阅读其修改说明,相应修改子类。

编写基类时,需要注意的是:

1.使用继承反应真正的“is-a”关系,只将真正公共的部分放到基类。

2.对不希望被重写的公开方法添加final修饰符。

3.写文档,说明可重写方法的实现机制,为子类提供倡导,告诉子类应该如何重写。

4.在基类修改可能影响子类时,写修改说明。

虚方法表

如果继承的层次比较深,要调用的方法位于比较上层的父类,则调用的效率是比较低的,因为每次调用都要进行很多次查找。

大多数系统使用一种称为虚方法表的方法来优化调用的效率。

虚方法表.jpg

虚方法表,就是在类加载的时候,为每个类创建一个表,这个表包括该类的对象所有动态绑定的方法及其地址,包括父类的方法,但一个方法只有一条记录,子类重写了父类方法后只会保留子类的。

虚方法表在类加载的时候生成,当通过对象动态绑定方法的时候,只需要查找这个表就可以了,不需要挨个查找每个父类。

继承实现的基本原理 (不是很懂的一章)

本节主要从概念上来介绍原理。

首先创建一个基类Base,Base中包含实例变量instanceCode、静态变量staticCode、一段静态初始化代码块、一段实例初始化代码块、一个构造方法、step方法和action方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Base {
public static int staticCode;
private int instanceCode;

static {
System.out.println("基类静态代码块,staticCode: "+staticCode);
staticCode = 1;
}

{
System.out.println("基类实例代码块,actualCode: "+instanceCode);
instanceCode = 1;
}

public Base() {
System.out.println("基类构造方法,actualCode: "+instanceCode);
instanceCode = 2;
}

protected void step() {
System.out.println("base staticCode: "+staticCode+", actualCode: "+instanceCode);
}

public void action() {
System.out.println("start");
step();
System.out.println("end");
}
}

接下来设计出继承Base的子类Child,Child同样包含实例变量actualCode、静态变量staticCode、一段静态初始化代码块、一段实例初始化代码块、一个构造方法,并且重写了step方法。

Child类代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Child extends Base{
public static int staticCode;
private int instanceCode;

static {
System.out.println("子类静态代码块,staticCode: "+staticCode);
staticCode = 10;
}

{
System.out.println("子类实例代码块,instanceCode: "+instanceCode);
}

public Child() {
System.out.println("子类构造方法,instanceCode: "+instanceCode);
instanceCode = 20;
}

@Override
protected void step() {
System.out.println("child staticCode: "+staticCode+", instanceCode: "+instanceCode);
}
}

使用这两个类的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
System.out.println("---- new Child()");
Child child = new Child();

System.out.println("\n---- child.action()");
child.action();

Base base = child;
System.out.println("\n---- base.action()");
base.action();

System.out.println("\n---- base.staticCode: "+base.staticCode);
System.out.println("\n---- child.staticCode: "+child.staticCode);
}

child.action()的具体执行过程如下:

1.查看child的对象类型,找到Child类型。在Child类型中寻找action方法,发现没有,于是到Base类中寻找。

2.在Base类中找到了action方法,开始执行该方法。

3.action先输出start,再从Child类型寻找step方法。

4.在Child类型中找到了step方法,执行它,然后返回action方法

5.继续执行action方法, 输出end

寻找要执行的实例方法的时候,是从对象的实际类型信息开始查找的,找不到的时候,再查找父类类型信息。

base.action和child.action都是动态绑定,动态绑定实现的机制,就是根据对象的实际类型查找要执行的方法,子类型中找不到的时候再查找父类。由于base和child指向的动态类型都是Child,所以执行结果一样。

屏幕输入结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
---- new Child()
基类静态代码块,staticCode: 0
子类静态代码块,staticCode: 0
基类实例代码块,actualCode: 0
基类构造方法,actualCode: 1
子类实例代码块,instanceCode: 0
子类构造方法,instanceCode: 0

---- child.action()
start
child staticCode: 10, instanceCode: 20
end

---- base.action()
start
child staticCode: 10, instanceCode: 20
end

---- base.staticCode: 1

---- child.staticCode: 10

类的加载

在java中,类是动态加载的。

第一次使用这个类的时候才会加载,加载一个类时,会查看其父类是否已加载。如果没有,则加载其父类。

一个类的信息,主要包括以下部分:

  • static变量

  • 类初始化代码

  • static方法

  • 实例变量

  • 实例初始化代码

  • 实例方法

  • 父类信息引用

类初始化代码包括:

  • 定义static变量时的赋值语句

  • static初始化代码块

实例初始化代码包括:

  • 定义实例变量时的赋值语句
  • 实例初始化代码块
  • 构造方法

类加载过程包括:

  • 分配内存,保存类的信息

  • 给static变量赋默认值

  • 加载父类

  • 设置父子关系

  • 执行类初始化代码

类初始化代码,是先执行父类,再执行子类。

父类执行时,子类static变量是有值的,为默认值。

类的信息放在内存区,在java中称为方法区

继承实现.jpg

如上图,class_init()表示类初始化代码,instance_init()表示实例初始化代码。实例初始化代码包括了实例初始化代码块和构造方法。

对于上方的Base类和Child类,它们的内存布局就像图中的方法区。

继承的细节--防止继承

继承和多态概念还有一些相关的细节,具体包括:

  • 构造方法
  • 重名与静态绑定
  • 重载和重写
  • 父子类型转换
  • 继承访问权限(protected)
  • 可见性重写
  • 防止继承(final) now!

final关键字可以防止类被继承。

一个java类,默认情况下都是可以被继承的。但加了final关键字之后就不能被继承了。

1
2
3
public final class Base {

}

上面代码中的Base类不能被继承。

一个非final的类,类中的public/protected实例方法默认情况下都是可以被重写的。但加了final关键字后就不能被重写了。

1
2
3
4
5
public class Base {
public final void test() {
System.out.println("不能被重写");
}
}

上面代码中的test方法不能被重写。

继承的细节--可见性重写

继承和多态概念还有一些相关的细节,具体包括:

  • 构造方法

  • 重名与静态绑定

  • 重载和重写

  • 父子类型转换

  • 继承访问权限(protected)

  • 可见性重写 now!

  • 防止继承(final)

重写时,子类方法不能降低父类方法的可见性

如果父类是public,则子类也必须是public。

如果父类是protected,子类可以是protected,也可以是public,即子类可以升级父类方法的可见性,但不能降低。

子类必须支持父类所有对外的行为,所以不能降低可见性;子类可以增加父类的行为,所以能提升可见性。

  • Copyrights © 2021 Silver Shaded
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信