Java面向对象编程之 接口 | Java OOP Interface

Made by Mike_Zhang


Java主题:


OOP Introduction

请阅读本博客的Java面向对象编程详细介绍 - Java OOP Detailed Introduction


1. Review

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
class Triangle { // 三角形类
void Print(){
System.out.println("A Triangle printed!"); // 父类方法
}
}

class RightTriangle extends Triangle{ //定义直角三角形子类
void Print(){
System.out.println("A Right Triangle printed!"); // 重写父类方法
}
}

class IsoscelesRightTriangle extends Triangle{ //定义等腰直角三角形子类
void Print(){
System.out.println("A Isosceles Right Triangle printed!"); // 重写父类方法
}
}

class EquilateralTriangle extends Triangle{ //定义等边三角形子类
void Print(){
System.out.println("A Isosceles Equilateral Triangle printed!"); // 重写父类方法
}
}

public class PrintTriangle { // 定义一个打印三角形的类
public static void PrintOut(Triangle x){ // 打印三角形的方法,使用父类当作参数类型
x.Print();
}

public static void main(String[] args){ // 主函数 main()
Triangle aNewRightTriangle = new RightTriangle(); // upcasting
PrintOut(aNewRightTriangle);; // 打印直角三角形

Triangle aNewIsoscelesRightTriangle = new IsoscelesRightTriangle(); // upcasting
PrintOut(aNewIsoscelesRightTriangle); // 打印等腰直角三角形

Triangle aNewEquilateralTriangle = new EquilateralTriangle(); // upcasting
PrintOut(aNewEquilateralTriangle); // 打印等边三角形
}
}

以上案例是在在之前文章Java面向对象编程(OOP)的继承性(Inheritance)Java面向对象编程(OOP)的多态性(Polymorphism)中提到了,运用了OOP的两大特性。

看以下例子:

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
44
45
46
47
48
class Shape{
public void name(){
System.out.println("Shape");
}
public void print(){
System.out.println("A Shape printed.");
}
}

class Rectangle extends Shape{
public void name(){
System.out.println("Rectangle");
}
public void print(){
System.out.println("A Rectangle printed.");
}
}


class Circle extends Shape{
public void name(){
System.out.println("Circle");
}
public void print(){
System.out.println("A Circle printed.");
}
}

class Triangle extends Shape{
public void name(){
System.out.println("Triangle");
}
public void print(){
System.out.println("A Triangle printed.");
}
}

public class PrintAbsShape {
static void PrintOut(Shape s){
s.print();
}
public static void main(String[] args){
PrintOut(new Rectangle());
PrintOut(new Triangle());
PrintOut(new Circle());
}
}

但是仔细观察发现,第一个例子中的父类Triangle与第二个例子中的父类Shape中的方法始终没有被引用,显得十分多余。因为此Shape父类本来就是提供一个入口,并让其子类继承并对其方法进行重写等操作。
因此父类中的方法不需要被定义的十分具象,只需要告诉衍生类方法的大概模样就足够了,因此父类可以变的 抽象(abstract) 或者只成为一个连通子类的 接口(interface)


2. Abstract class & method

在Java中,通过修饰符abstract来修饰一个方法为抽象的。此修饰的方法是残缺的,不完整的只包含方法的声明并没有方法体,语法:

1
abstract void f();

abstract应用到上面的例子中:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
abstract class Shape{ // abstract 类
public abstract void name(); // abstract 方法
public abstract void print(); // abstract 方法
}

class Rectangle extends Shape{
public void name(){
System.out.println("Rectangle"); // 重写父类 abstract 方法, 为其添加方法体
}
public void print(){}
}

class Circle extends Shape{
public void name(){
System.out.println("Circle"); // 重写父类 abstract 方法, 为其添加方法体
}
public void print(){}
}

class Triangle extends Shape{
public void name(){
System.out.println("Triangle"); // 重写父类 abstract 方法, 为其添加方法体
}
public void print(){}
}

class RightTriangle extends Triangle{
public void name(){
System.out.println("Right Triangle"); // 重写父类方法
}
public void print(){
System.out.println("A Right Triangle printed"); // 重写父类方法
}
}

class IsoscelesTriangle extends Triangle{
public void name(){
System.out.println("Isosceles Triangle"); // 重写父类方法
}
public void print(){
System.out.println("A Isosceles Triangle printed"); // 重写父类方法
}
}

class EquilateralTriangle extends Triangle{
public void name(){
System.out.println("Equilateral Triangle"); // 重写父类方法
}
public void print(){
System.out.println("A Equilateral Triangle printed"); // 重写父类方法
}
}
public class PrintAbsShape {
static void PrintOut(Shape s){
s.print(); // late binding
}
public static void main(String[] args){
PrintOut(new RightTriangle());
PrintOut(new IsoscelesTriangle());
PrintOut(new EquilateralTriangle());
}
}

  • 包含abstract方法的类被称为abstract类,此类必须被修饰为abstractabstract类允许在其类中创建0个,一个或者多个abstract方法,

  • abstract类中的所有abstract方法需要被其子类重写以完成方法体。当某一子类继承abstract父类后,如要使用此子类创造对象,必须完成子类中所有从父类abstract方法继承来方法的定义。static方法、private实例方法、构造方法不能被重写

    • 不对父类中所有的abstract方法进行重写,则此子类也包含了从父类继承的abstract方法,则其也是abstract,也需要修饰为abstract
  • 不允许直接用abstract类来创建实例,只可以来定义类型。以下不允许:

    1
    Shape aShape = new Shape(); // Error!
  • 子类可以重写父类中的非abstract方法并定义为abstract,可以使此父类中的方法在子类中失效。

  • 一个abstract类的父类可能是非abstract类。

3. Interface

  • interfaceabstract类更进一步的抽象,使这个类变得完全abstract
  • 在此类中只声明方法名参数列表返回值类型没有方法体,只提供方法的形式,没有定义方法体
    • 对于实例成员(Instance members):
      • 不可以定义属性;
      • 都被publicabstract修饰(隐性修饰,关键字可省略);
    • 对于静态成员(Static members):
      • 所有属性都被public,staticfinal修饰(隐性修饰,关键字可省略);
      • 所有方法都被public(隐性修饰,关键字可省略),并为非abstract类。
  • 任何 实现(implement) 此接口的类都会与interface类相似,都会得知能从此接口调用到什么方法,类似于类之间建立了一个协议。
  • 使用interface时,interface关键字代替原来的class关键字。

interface应用到上面的例子:

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
44
45
46
47
48
49
50
51
52
53
54
55
interface Shape{ // Shape 接口
int NUM = 10; // public, static and final field
void name(); // public abstract 方法
void print(); // public abstract 方法
static String getShapeSuperName(){return "Shape"} // public static 方法
}

class Rectangle implements Shape{ // 实现 Shape 接口
public void name(){
System.out.println("Rectangle"); // 实现父类 abstract 方法, 为其添加方法体
}
public void print(){}
}

class Circle implements Shape{ // 实现 Shape 接口
public void name(){
System.out.println("Circle"); // 实现父类 abstract 方法, 为其添加方法体
}
public void print(){}
}

class Triangle implements Shape{ // 实现 Shape 接口
public void name(){
System.out.println("Triangle"); // 实现父类 abstract 方法, 为其添加方法体
}
public void print(){}
}

class RightTriangle extends Triangle{
public void print(){
System.out.println("A Right Triangle printed"); // 重写父类方法
}
}

class IsoscelesTriangle extends Triangle{
public void print(){
System.out.println("A Isosceles Triangle printed"); // 重写父类方法
}
}

class EquilateralTriangle extends Triangle{
public void print(){
System.out.println("A Equilateral Triangle printed"); // 重写父类方法
}
}
public class PrintShape {
static void PrintOut(Shape s){
s.print(); // late binding
}
public static void main(String[] args){
PrintOut(new RightTriangle());
PrintOut(new IsoscelesTriangle());
PrintOut(new EquilateralTriangle());
}
}
  • 当某一类要使用此接口时,也就是说此类要实现(implements)此接口,就需要使用implements关键字,类似于继承。

    • 此类可以访问父类中constants 和 static 的方法;
    • 此类通过重写来具化声明在接口中的方法;
    • 如果没有初始化父类中所有的abstract方法,则此方法需要被定义为abstract
  • 当此类实现接口后,其就变成了一个常规的类,能够被子类继承,如 RightTriangle继承 Triangle等。

  • interface中的方法是被隐性修饰为public的。当某类实现此interface时,此类中从interface重写的方法必须被修饰为public,否则会变为默认访问权限,会导致此类被继承后的访问权限变小,产生错误。

  • 上面例子main()中发生了upcasting,但是并不用明确到底转型到了哪个Shape,无论是正常的Shapeabstract修饰的Shape还是Shape接口。

  • 以上例子中,还有一个定义在interface中的属性int NUM = 10;,此属性都是隐性修饰为staticfinal


4. Interface inheritance

4.1 Combined interface

  • 在继承中,子类每次只能继承一个父类,因为父类是一个完整的类,有具体的内存空间联系,同时继承多个类会导致冲突,如下:
1
2
3
public class ClassA{...}
public class ClassB{...}
public class ClassAandB extends ClassA, ClassB{...} // Error! 不允许
  • 但是一个interface只是一个形式,没有具体的内存空间与之联系,因此interface与继承不同,一个类可以同时实现多个interface,只需要在implements关键字后列出所有interface名并用逗号隔开。语法:
1
class ClassA implements InterfaceA, InterfaceB, InterfaceC{}

应用到上面的例子:

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
44
45
46
47
48
49
interface Shape{ // Shape 接口
int NUM = 10;
void name(); // abstract 方法
void print(); // abstract 方法
}

interface Perimeter{
void getPerimeter();
}

interface Area{
void getArea();
}

class GreatShape implements Shape, Perimeter, Area{ // GreatShape类结合了3个接口一起实现
public void name(){System.out.println("Great Shape with Perimeter and Area");}
public void print(){System.out.println("A Great Shape printed");}
public void getPerimeter(){System.out.println("Print the Perimeter of A Great Shape");}
public void getArea(){System.out.println("Print the Area of A Great Shape");}
}

class GreatTriangle extends GreatShape{ // 继承父类GreatShape
public void name(){System.out.println("Great Triangle with Perimeter and Area");}
public void print(){System.out.println("A Great Triangle printed");}
public void getPerimeter(){System.out.println("Print the Perimeter of A Great Triangle");}
public void getArea(){System.out.println("Print the Area of A Great Triangle");}
}

public class PrintShape {
static void PrintOut(Shape s){
s.print(); // late binding
}
static void PrintName(Shape x){
x.name();
}
static void PrintPerimeter(Perimeter x){
x.getPerimeter();
}
static void PrintArea(Area x){
x.getArea();
}
public static void main(String[] args){
GreatTriangle g = new GreatTriangle();
PrintOut(g); // 可看作Shape的方法
PrintName(g); // 可看作Shape的方法
PrintPerimeter(g); // 可看作Perimeter的方法
PrintArea(g); // 可看作Area的方法
}
}

输出:

1
2
3
4
A Great Triangle printed
Great Triangle with Perimeter and Area
Print the Perimeter of A Great Triangle printed
Print the Area of A Great Triangle printed
  • 以上例子中,PrintShape类中有4个方法,分别使用了不同接口当作其方法的参数,在其main()中当一个GreatTriangle对象创建并调用这4个方法时,此对象会upcast到这4个接口,并late binding到相应的方法体。

4.2 Inherited interface

  • 当需要给某个interface添加新的方法,或者要结合多个interface时,可以让某个interface对另一个或多个interface进行继承,这会产生新的interface

应用到上面的例子:

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
44
45
46
47
48
49
50
51
52
53
54
55
interface Shape{ // Shape 接口
int NUM = 10;
void name(); // abstract 方法
void print(); // abstract 方法
}

interface Perimeter{
void getPerimeter();
}

interface Area{
void getArea();
}

interface GreatShapeInterface extends Shape, Perimeter, Area{ // 继承了3个接口并产生一个新的接口
public void name(); // 以下结合了3个接口的4个方法
public void print();
public void getPerimeter();
public void getArea();
public void what(); // 并添加了新的方法
}

class GreatTriangle implements GreatShapeInterface{ // 继承新的接口
public void name(){System.out.println("Great Triangle with Perimeter and Area");}
public void print(){System.out.println("A Great Triangle printed");}
public void getPerimeter(){System.out.println("Print the Perimeter of A Great Triangle");}
public void getArea(){System.out.println("Print the Area of A Great Triangle");}
public void what(){System.out.println("Inherited form several interfaces!");} // 完成新方法的定义
}

public class PrintShape {
static void PrintOut(Shape s){
s.print(); // late binding
}
static void PrintName(Shape x){
x.name();
}
static void PrintPerimeter(Perimeter x){
x.getPerimeter();
}
static void PrintArea(Area x){
x.getArea();
}
static void What(GreatShapeInterface x){
x.what(); // 调用新方法
}
public static void main(String[] args){
GreatTriangle g = new GreatTriangle();
PrintOut(g); // 可看作Shape的方法
PrintName(g); // 可看作Shape的方法
PrintPerimeter(g); // 可看作Perimeter的方法
PrintArea(g); // 可看作Area的方法
What(g); // 可看作GreatShapeInterface的方法
}
}

注意:
当通过继承来结合一些接口时,要注意各个接口中的方法名是否相同,为增加可读性以及减少错误的产生,最好避免使用同名的方法


5. Interface field

定义在接口中的属性都是隐性修饰为staticfinal,并且是public的。
注意定义在接口中的属性必须为编译时常量 (compile-time constant) 不能是空白常量 (blank final)
此类属性并不是接口的一部分,只是被储存在接口的静态 (static) 内存中。


6. Interface Cloning

6.1 Original copy

一般对一个基础变量进行复制会进行以下语句:

1
2
int int1 = 1;
int1 = int2;

但对一个对象进行复制也进行类似语句:

1
2
classType object1 = new classType();
classType object2 = object1;

这样会使两个对象指向同一个引用,并不是单独的,改变一个对象的属性会影响到另外一个,例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Shape {
private int number;

public int getNumber(){
return number;
}
public void setNumber(int inNum){
number = inNum;
}
}

public class CopyObjectTest {
public static void main(String[] args){
Shape s1 = new Shape();
s1.setNumber(111111);
Shape s2 = s1; // 直接复制对象
s1.setNumber(222222); // 改变一个对象的属性
System.out.printf("s1 number: %d\ns2 number: %d\n\n",s1.getNumber(),s2.getNumber());
System.out.printf("s1 == s2? %b",s1 == s2);
}
}

输出:

1
2
3
4
s1 number: 222222
s2 number: 222222 // 收到另一对象的影响,一起改变

s1 == s2? true // 实际上指向同一引用,为同一对象

6.2 Shallow Clone

为了解决上面所说的问题,就需要用到clone方法,可以使复制的对象一开始有和被复制的对象有相同的成员,但之后也可以被单独对待,有自己的属性。


A shallow copy - C. S. Horstmann, Core Java. Boston: Pearson, 2019.

clone方法是在Object类中被修饰为protected的方法。不能随意的调用。只能进行属性间的复制,也就是说只能对对象中为基础类型(primitive type)的属性进行复制。若对一个引用类型的属性或对象进行复制,则只会使克隆的对象指向相同的引用,和被克隆的对象有相同的信息。
因此clone分为Shallow Clone(浅克隆)和Deep Clone(深克隆)


Shallow Clone只会克隆基础类型的属性,不会克隆引用类型的属性。


A shallow copy - C. S. Horstmann, Core Java. Boston: Pearson, 2019.

Shallow Clone步骤:

  1. 实现Cloneable接口,否则在非Cloneable对象调用clone()方法会抛出CloneNotSupportedException异常;
  2. 重写clone()方法并修饰为public,添加异常处理以处理CloneNotSupportedException异常。

应用到前面的例子:

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
class Shape implements Cloneable{
private int number;

public int getNumber(){
return number;
}
public void setNumber(int inNum){
number = inNum;
}

public Object clone(){
Shape temp = null;
try {
temp = (Shape) super.clone();
}
catch (CloneNotSupportedException e){
e.printStackTrace();
}
return temp;
}
}

public class ShallowCloneTest {
public static void main(String[] args){
Shape s1 = new Shape();
s1.setNumber(11111);
s1.setName("shape1&2");
Shape s2 = (Shape)s1.clone();
System.out.printf("s1 number: %d, s2 number: %d\ns1 name: %s, s2 name: %s\n\n",s1.getNumber(),s2.getNumber(),s1.getName(),s2.getName());

s1.setNumber(22222);
s1.setName("s1 Updated");
System.out.printf("s1 number: %d, s2 number: %d\ns1 name: %s, s2 name: %s\n\n",s1.getNumber(),s2.getNumber(),s1.getName(),s2.getName());

System.out.printf("s1 == s2? %b",s1 == s2);
}
}

输出:

1
2
3
4
5
6
7
s1 number: 11111, s2 number: 11111
s1 name: shape1&2, s2 name: shape1&2

s1 number: 22222, s2 number: 11111 // s1改变 不会影响到克隆的s2
s1 name: s1 Updated, s2 name: shape1&2

s1 == s2? false // 被克隆的对象与克隆对象指向不同的引用

接下来加入一个新的类Position,并当作Shape类的一个属性,进行Shallow Clone:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class Shape implements Cloneable{
private int number;
private String name;
private Position pos; // a reference type field

public int getNumber(){
return number;
}
public void setNumber(int inNum){
number = inNum;
}
public String getName(){
return name;
}
public void setName(String n){
name = n;
}
public String getPos(){
return pos.getPosition();
}
public void setPos(Position inPos){
pos = inPos;
}

public Object clone(){
Shape temp = null;
try {
temp = (Shape) super.clone(); // shallow clone ONLY
}
catch (CloneNotSupportedException e){
e.printStackTrace();
}
return temp;
}
}

class Position{
private int x;
private int y;

public String getPosition(){
return ("("+x+","+y+")");
}
public void setPosition(int InX, int Iny){
x = InX;
y = Iny;
}
}

public class DeepCloneTest {
public static void main(String[] args){
Position aPos = new Position();
aPos.setPosition(1,1);
Shape s1 = new Shape();
s1.setPos(aPos);
Shape s2 = (Shape)s1.clone(); // clone
System.out.printf("s1 position: %s, s2 position: %s\n\n",s1.getPos(),s2.getPos());

aPos.setPosition(2,2);
s1.setPos(aPos); // 改变s1的 reference type file value
System.out.printf("s1 position: %s, s2 position: %s\n\n",s1.getPos(),s2.getPos());

System.out.printf("s1 == s2? %b",s1 == s2);
}
}

输出:

1
2
3
4
5
s1 position: (1,1), s2 position: (1,1)

s1 position: (2,2), s2 position: (2,2) // 发现s1和s2的reference type filed - pos同时改变了

s1 == s2? false

以上例子印证了Shallow Clone只会克隆基础类型的属性,不会克隆引用类型的属性。

因此需要Deep Clone,不仅仅把reference type的属性克隆,也同时把reference type属性的引用地址克隆,达到彻底的克隆。


6.3 Deep Clone

为了实现Deep Clone,在以上例子中也需要把Position类可克隆化,并且修改其clone()方法:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class Shape implements Cloneable{
private int number;
private String name;
private Position pos;

public int getNumber(){
return number;
}
public void setNumber(int inNum){
number = inNum;
}
public String getName(){
return name;
}
public void setName(String n){
name = n;
}
public String getPos(){
return pos.getPosition();
}
public void setPos(Position inPos){
pos = inPos;
}

public Object clone(){
Shape temp = null;
try {
temp = (Shape) super.clone(); // 浅克隆
temp.pos = (Position) pos.clone(); // 深克隆
}
catch (CloneNotSupportedException e){
e.printStackTrace();
}

return temp;
}
}

class Position implements Cloneable{ // 可克隆化
private int x;
private int y;

public Object clone(){ // 重写clone()方法
Position temp = null;
try {
temp = (Position) super.clone();
}
catch (CloneNotSupportedException e){
e.printStackTrace();
}
return temp;
}

public String getPosition(){
return ("("+x+","+y+")");
}
public void setPosition(int InX, int Iny){
x = InX;
y = Iny;
}
}

public class DeepCloneTest {
public static void main(String[] args){
Position aPos = new Position();
aPos.setPosition(1,1);
Shape s1 = new Shape();
s1.setPos(aPos);
Shape s2 = (Shape)s1.clone();
System.out.printf("s1 position: %s, s2 position: %s\n\n",s1.getPos(),s2.getPos());

aPos.setPosition(2,2);
s1.setPos(aPos);
System.out.printf("s1 position: %s, s2 position: %s\n\n",s1.getPos(),s2.getPos());

System.out.printf("s1 == s2? %b",s1 == s2);
}
}

输出:

1
2
3
4
5
s1 position: (1,1), s2 position: (1,1)

s1 position: (2,2), s2 position: (1,1) // 深克隆进行后 s1改变并不会影响s2

s1 == s2? false

在决定是否使用clone()方法前,考虑:

  1. 默认的clone()方法是否适合;
  2. 若不适合,则重写clone()方法;
  3. 不应该使用clone()方法。

若考虑使用:

  1. 实现Cloneable接口;
  2. 重写clone()方法,并用public修饰。

End

两个使用interface的原因:

  1. 使某一对象upcast至不止一个父类型,使其变得灵活;
  2. 防止这一抽象的类被直接使用

如果明确某一类会被定义为父类,则可以直接让其定义成一个interface(或者abstract,但是优先考虑interface)。
但要注意不能过度使用interface,可以先写出具象的父类,分析必要性之后再将其改成interface


参考

B. Eckel, Thinking in java. Upper Saddle River, N.Y. Prentice Hall, 2014.
C. S. Horstmann, Core Java. Boston: Pearson, 2019.


写在最后

Java的接口还有更深层次的内容,会继续更新.
最后,希望大家一起交流,分享,指出问题,谢谢!


原创文章,转载请标明出处
Made by Mike_Zhang




感谢你的支持

Java面向对象编程之 接口 | Java OOP Interface
https://ultrafish.io/post/Java-oop-interface/
Author
Mike_Zhang
Posted on
October 5, 2021
Licensed under