1 面向对象的三大特征

  1. 封装
  2. 继承
  3. 多态


2 封装

  • 封装(Encapsulation)的定义

在面向对象程序设计方法中,封装是指一种将抽象性函数接口的实现细节包装、隐藏起来的方法。
封装可以被认为是一个保护屏障,防止类的代码和数据被外部代码随机访问。
要访问类的代码和数据,必须通过严格的接口控制。

  • 封装的优点
    • 良好的封装能够减少耦合
    • 类内部的结构可以自由修改,而不用修改那些调用类的代码
    • 更改器方法可以完成错误检查。

setSalary()方法可以检查工资是否小于0。

  • 实现Java封装的步骤

    1. 修改实例字段的可见性为private
    2. private字段提供对外的公共方法,即根据需要提供访问器方法(getter)和更改器方法(setter)
  • 不要编写返回可变对象引用的访问器方法,否则会破坏封装性,如下实例: ```java public class MyClass { public static void main(String[] args) {

    1. User user = new User();
    2. user.printPasswd(); //out: [1, 2, 3, 4]
    3. int[] p = user.getPasswd();
    4. p[0] = 999;
    5. user.printPasswd(); //out: [999, 2, 3, 4]

    } }

class User { private int[] passwd;

  1. User() {
  2. passwd = new int[]{1, 2, 3, 4};
  3. }
  4. public int[] getPasswd() {
  5. return passwd;
  6. }
  7. public void printPasswd() {
  8. System.out.println(Arrays.toString(passwd));
  9. }

}

  1. - 数组是可变对象,返回数组破坏了封装性。
  2. 上例中,在外部改变p就可以改变user对象的私有状态,因为`getPasswd()`方法的返回值和user的私有成员passwd引用的是同一个数组对象。
  3. - **如果需要返回一个可变对象的引用,首先应该对它进行克隆(clone)。**
  4. 上述代码中的`return`语句应该改为`return (int[])passwd.clone();`
  5. - `clone()`方法返回Object类型对象,需要进行强制类型转换。
  6. - 一个方法可以访问**所属类的所有对象的私有数据**
  7. ```java
  8. class Employee {
  9. private String name;
  10. ...
  11. public boolean equals(Employee other) {
  12. return name.equals(other.name);
  13. }
  14. }
  • 如在外部调用if(employee.equals(boss)) ...

equals方法不但访问了employee的私有字段,还访问了boss的私有字段。

3 继承

  • 生活中的继承

面向对象三特性 - 图1

  • 兔子和羊属于食草动物类,狮子和豹属于食肉动物类。
  • 食草动物和食肉动物又是属于动物类。
  • 虽然食草动物和食肉动物都是属于动物,但是两者的属性和行为上有差别,所以子类会具有父类的一般特性也会具有自身的特性。
  • 继承的定义

    • 从已有的类派生出新的类,称为继承。
    • 继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
    • 父类更通用,子类更具体。
    • 子类不是父类的子集,子类一般比父类包含更多的数据域和方法。
    • 继承需要符合的关系是:**is-a**,否则无法使用继承,如狮子is-a食肉动物类,
    • 父类也称为超类、基类;子类也称为次类、派生类、扩展类
  • 继承实现的具体功能

    • 子类可以访问父类非**private**的属性、方法。

子类还可以定义自己的属性和方法,即子类可以对父类进行扩展。

  • 子类可以重新实现父类的方法,称为方法的重写
  • 提高了类之间的耦合性继承的缺点,耦合度高就会造成代码之间的联系越紧密,代码独立性越差)。
  • 使用继承的意义
    • 在不同的类中也可能会有共同的特征和动作,可以把这些共同的特征和动作放在一个类中,让其它类共享。因此可以定义一个通用类,然后将其扩展为其它多个特定类,这些特定类继承通用类中的特征和动作。
    • 使用继承可以使得代码维护性提高、更加简洁、复用性提高


  • 类的继承格式——extends关键字

在Java中通过extends关键字可以申明一个类是从另外一个类继承而来的,一般形式如下:

  1. class father {
  2. }
  3. class son extends father {
  4. }
  • Java的继承类型

面向对象三特性 - 图2

  • Java的继承是单继承,不支持多继承,但是支持多重继承

单继承就是一个子类只能继承一个父类,多重继承就是,例如B类继承A类,C类继承B类,所以按照关系就是B类是C类的父类,A类是B类的父类,这是Java继承区别于C++继承的一个特性。

1 与继承相关的关键字

1 extends

子类使用该关键字实现单继承父类

  • 由于所有的类都是继承于java.lang.Object,当一个类没有关键字extendsimplements,则默认继承Object祖先类。

2 super

  • **super**用于指代父类的对象实例本身,我们可以通过super关键字来实现对父类成员的访问
  • superthis一样,只能在非静态方法中使用
  • super可用于
    • 调用父类的构造器
    • 调用父类的方法
    • 访问父类可以被访问的数据域**super**无法访问**private**字段)
  • 实际使用中
    • **super**主要用于调用父类的构造器
    • 从父类继承的方法没必要使用**super**调用,使用**this**也可以调用

如果子类重写了父类的方法,此时只有使用**super**才能在子类中调用父类被重写的方法

  • 一般不访问父类的数据域 ```java public class MyClass{ public static void main(String[] args) { Son son = new Son(); son.fun(); //out } }

class father { private String a;

  1. public void setA(String a){
  2. this.a = a;
  3. }
  4. public String getA(){
  5. return a;
  6. }

}

class Son extends father{ public void fun(){ super.setA(“父类的实例字段值”); System.out.println(super.getA()); } }

  1. <a name="QE7pL"></a>
  2. #### 3 final
  3. `final`除了可以声明变量,令变量不能被修改,还能声明方法和类,对继承进行限制
  4. - **修饰类:被**`**final**`**修饰的类不能被继承,即最终类;**
  5. - **修饰方法:被**`**final**`**修饰的方法不能被子类重写:**
  6. <br />
  7. <a name="l0plV"></a>
  8. #### 4 this
  9. 父类中的this实际上指代的是子类对象
  10. ```java
  11. class Father{
  12. void printClass(){
  13. System.out.println(this.getClass());
  14. }
  15. }
  16. class Son extends Father{ }
  17. public class Main {
  18. public static void main(String[] args) {
  19. new Son().printClass(); // out: class Son
  20. }
  21. }

2 继承中的构造器

  • 由于子类是不继承父类的构造器的,所以它需要调用父类构造器(隐式或显式)。

  • 子类调用父类构造器的方式分为隐式和显式

    • 显式

如果父类的构造器带有参数,则必须在子类的构造器中显式地通过**super**关键字调用父类的构造器并配以适当的参数列表。

  1. - `**super**`**语句必须是子类构造方法的第一条语句。**
  • 隐式

如果父类构造器没有参数,则在子类的构造器中不需要使用super关键字调用父类构造器,系统会自动调用父类的无参构造器。

  • 在执行子类的构造函数前,总是会先执行父类中的构造函数,无论是显式还是隐式调用父类构造器
  • 实例 ```java class SuperClass { private int n;

    SuperClass() {

    1. System.out.println("SuperClass()");

    }

    SuperClass(int n) {

    1. System.out.println("SuperClass(int n):" + n);
    2. this.n = n;

    } }

class SubClass extends SuperClass { private int n;

  1. SubClass() {
  2. //隐式调用父类无参构造器
  3. System.out.println("SubClass()");
  4. }
  5. public SubClass(int n) {
  6. super(300); //显式调用父类中带有参数的构造器
  7. System.out.println("SubClass(int n):" + n);
  8. this.n = n;
  9. }

}

class SubClass2 extends SuperClass { private int n;

  1. SubClass2() {
  2. super(300); //显式调用父类中带有参数的构造器
  3. System.out.println("SubClass2()");
  4. }
  5. public SubClass2(int n) {
  6. //隐式调用父类的无参数构造器
  7. System.out.println("SubClass2(int n):" + n);
  8. this.n = n;
  9. }

}

public class MyClass { public static void main(String[] args) { System.out.println(“———SubClass———“); SubClass sc1 = new SubClass(); SubClass sc2 = new SubClass(100); System.out.println(“———SubClass2———“); SubClass2 sc3 = new SubClass2(); SubClass2 sc4 = new SubClass2(200); } }

  1. - 上述代码的输出为
  2. ```bash
  3. ------SubClass------
  4. SuperClass()
  5. SubClass()
  6. SuperClass(int n):300
  7. SubClass(int n):100
  8. ------SubClass2------
  9. SuperClass(int n):300
  10. SubClass2()
  11. SuperClass()
  12. SubClass2(int n):200

3 继承中的访问控制

  • 子类继承了父类的所有的东西(包括**private**成员),除了构造方法

    • 但是得到不等于可以随便使用:每个父类成员有不同的访问属性,子类通过继承得到了父类所有的成员,但是不同的访问属性使得子类在使用这些成员时有所不同
  • 下表列出了不同访问属性的父类成员在子类中的访问属性,可以说控制由上到下越来越严格: | 父类成员访问属性 | 在父类中的含义 | 在子类中的含义 | | —- | —- | —- | | public | 对所有人开放 | 对所有人开放 | | protected | 只有包内其它类、自己和子类可以访问 | 只有包内其它类、自己和子类可以访问 | | 缺省 | 只有包内其它类可以访问 | 如果子类与父类在同一个包内:只有包内其它类可以访问,否则:相当于private,不能访问 | | private | 只有自己可以访问 | 不能访问 |

    • 父类的**public**的成员直接成为子类的public成员

父类的**protected**的成员也直接成为子类的protected成员

  • 父类的缺省的未定义访问属性的成员是在父类所在的包内可见

如果子类不属于父类的包,那么在子类里面这些缺省属性的成员和private的成员一样不可见。

  • 父类的**private**的成员在子类里仍然是存在的,只是子类中不能直接访问,但是可以通过父类的方法访问 ```java public class MyClass { public static void main(String[] args) { SubClass subClass = new SubClass(); System.out.println(subClass.getN()); //out: 100 } }

class SuperClass { private int n = 100;

  1. public int getN(){
  2. return n;
  3. }

} class SubClass extends SuperClass {

}

  1. <a name="r4yPX"></a>
  2. ### 4 转型问题
  3. - **Java中,父子对象之间的类型可以互相转换。**
  4. 父子类型转换分为了**向上转型**和**向下转型**,它们区别如下:
  5. - **向上转型(常用)**
  6. 通过子类对象(具体)实例化父类引用(通用),这种属于**自动转换**
  7. - **向下转型(使用较少)**
  8. 将指向子类对象的父类引用(通用)强制转换为子类引用(具体),这种属于**强制转换**
  9. - 两种转换方法的核心是:**父类引用可以指向子类对象,但子类引用不可以指向父类对象。**如下转型实例:
  10. ```java
  11. class Father {
  12. }
  13. class Son extends Father {
  14. }
  15. public class MyClass {
  16. public static void main(String[] args) {
  17. Father father = new Son(); //向上转型,father引用指向Son对象
  18. Son son = (Son)father; //向下转型,son引用指向Son对象
  19. Father father2 = new Father();
  20. Son son2 =(Son)father2; //出错,son2引用不能指向Father对象
  21. }
  22. }


  • 向上转型

向上转型就是将子类对象转为父类对象。此处父类对象可以是接口

  • 指向子类对象的父类引用会遗失子类特有的方法,仅保留子类与父类共有的方法
  • 如果子类重写了父类的方法,那么父类引用将调用子类的方法 ```java class Animal { public void eat(){ System.out.println(“animal eatting…”); } } class Bird extends Animal{ public void eat(){ System.out.println(“bird eatting…”); } public void fly(){ System.out.println(“bird flying…”); } }

public class MyClass { public static void main(String[] args) { Animal bird=new Bird(); //向上转型 bird.eat(); //out: bird eatting… not: animal eatting… bird.fly(); //报错,bird虽指向子类对象,但此时丢失子类独有的fly()方法 } }

  1. - **向上转型的作用**
  2. 向上转型体现了Java的抽象编程思想
  3. - 调用某个方法时,**当我们需要多个同父的对象作为参数时,通过向上转换,可以使参数统一**,否则有多少个子类就需要写多少个参数。
  4. ```java
  5. class Human {
  6. public void sleep() {
  7. System.out.println("Human sleep..");
  8. }
  9. }
  10. class Male extends Human {
  11. @Override
  12. public void sleep() {
  13. System.out.println("Male sleep..");
  14. }
  15. }
  16. class Female extends Human {
  17. @Override
  18. public void sleep() {
  19. System.out.println("Female sleep..");
  20. }
  21. }
  22. public class MyClass {
  23. public static void main(String[] args) {
  24. //传参时向上转型
  25. doSleep(new Male()); //out: Male sleep..
  26. doSleep(new Female()); //out: Female sleep..
  27. }
  28. //只需要写一个方法,参数声明为父类
  29. public static void doSleep(Human h) {
  30. h.sleep();
  31. }
  32. }
  • 如果某个类实现了多个接口,可以利用这个类来实例化接口,这样使得接口引用只可以使用类的某个功能领域的方法。如Queue接口:
    1. //Queue接口可以被实现了该接口的LinkedList类实例化
    2. //queue只能使用由LinkedList实现的有关队列的方法
    3. Queue<String> queue = new LinkedList<>();
  • 向下转型与向下转型的作用

向下转型是将指向子类对象的父类引用强制转换为子类引用

  • 向下转型是为了调用子类独有的方法
  • 为了保证向下转型的顺利完成,需要通过使用Java提供的关键字**instanceof**来判断某对象引用指向的是否是某类的实例,如果是则返回true,否则为false ```java class Animal { public void eat() { System.out.println(“animal eatting…”); } } class Bird extends Animal { public void eat() { System.out.println(“bird eatting…”); } public void fly() { System.out.println(“bird flying…”); } }

public class MyClass { public static void main(String[] args) { Animal animal = new Bird(); //向上转型 animal.eat(); //out: bird eatting…

  1. //此时调用bird.fly()会报错
  2. if (animal instanceof Bird) { //向下转型前,先使用instanceof判断
  3. Bird bird = (Bird) animal; //向下转型
  4. bird.fly(); //out: bird flying...
  5. }
  6. }

}

  1. <a name="kjmgK"></a>
  2. ## 4 重写(Override)与重载(Overload)
  3. - 方法重载是一个类中定义了多个**方法名相同**,而他们的参数列表不同,则称为方法的重载(Overloading)。
  4. - 方法重写是在子类存在方法与父类的**方法名相同**,而且参数列表相同,返回值类型也相同的方法,就称为重写(Overriding)。
  5. ![](https://cdn.nlark.com/yuque/0/2021/png/1169704/1616227328190-5b2c77f6-afb7-49ac-a74b-2f12a67015c5.png#crop=0&crop=0&crop=1&crop=1&height=305&id=XCs5E&originHeight=305&originWidth=730&originalType=binary&ratio=1&rotation=0&showTitle=false&size=0&status=done&style=none&title=&width=730)
  6. - **何时使用重载与重写**
  7. - 需要同样的一个方法能够根据输入数据的不同,做出不同的处理,就要重载
  8. - 当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,就要重写父类方法
  9. <a name="fFtnC"></a>
  10. ### 1 重写
  11. - **重写的定义**
  12. 重写(Override)是子类对**继承自父类的方法**的实现过程进行重新编写<br />**只改变方法的实现,不改变方法的外观称为重写**(除了static方法)
  13. - **重写的好处在于**子类能够根据需要实现父类的方法。
  14. - **重写实例**
  15. ```java
  16. class Animal{
  17. public void move(){
  18. System.out.println("动物可以移动");
  19. }
  20. }
  21. class Dog extends Animal{
  22. //重写父类Animal的方法,外观(即参数和返回值)必须与父类方法相一致
  23. public void move(){
  24. System.out.println("狗可以跑和走");
  25. }
  26. }
  27. public class MyClass {
  28. public static void main(String[] args) {
  29. Animal a = new Animal(); //Animal对象
  30. Animal b = new Dog(); //Dog对象(向上转型)
  31. a.move();//执行Animal类的move()方法
  32. b.move();//执行Dog类的move()方法
  33. }
  34. }
  • 上述代码输出

    1. 动物可以移动
    2. 狗可以跑和走
  • 在上面的例子中可以看到,尽管b属于Animal类型,但是它运行的是Dog类的move()方法。

这是由于在编译阶段,只是检查参数的引用类型。然而在运行时,Java虚拟机(JVM)指定变量引用的对象的类型并且运行该对象的方法。
因此在上面的例子中,之所以能编译成功,是因为Animal类中存在**move()**方法,然而运行时,运行的是特定对象的方法

  • 重写的规则
    • 重写方法的参数列表与被重写方法的参数列表必须完全相同(参数列表如果不同就是重载了)

重写方法的返回类型与被重写方法的返回值类型可以不相同,但是如果不同,重写方法的返回值类型必须是父类返回值类型的子类。

  • 构造方法不能被重写。

  • 重写可以改变访问修饰符

访问权限不能比父类中被重写的方法的访问权限更严格。
例如:如果父类的一个方法被声明为public,那么在子类中重写该方法就不能声明为protected

  • 重写不能改变非访问修饰符

  • 由于父类的private方法不能被子类继承,因此子类定义与父类**private**方法重名的方法不是重写,而是定义了一个新的方法,不需要遵循重写的规则。

    1. class Animal{
    2. private void move(){
    3. System.out.println("动物可以移动");
    4. }
    5. }
    6. class Dog extends Animal{
    7. //声明的新方法,外观随便定义,与父类的private方法无关
    8. public int move(int cc){
    9. System.out.println("狗可以跑和走");
    10. return cc;
    11. }
    12. }
  • 父类中声明为**final**的方法不能被重写,也不能重新声明

父类中声明为**static**的方法也不能被重写,但是可以重新声明一个**static**方法

  1. class Animal {
  2. public static void move() {
  3. System.out.println("father");
  4. }
  5. public final void kick() {}
  6. }
  7. class Dog extends Animal {
  8. //不是重写,而是声明了一个与父类无关的方法
  9. public static void move() {}
  10. //报错
  11. public final void kick() {}
  12. }
  13. public class MyClass {
  14. public static void main(String[] args) {
  15. Animal a = new Dog(); //Dog对象
  16. a.move(); //out: father,印证了子类的move()不是重写
  17. }
  18. }
  • 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为**private****final****static**的方法。

子类和父类不在同一个包中,除了上述不能重写的方法外,也不能重写缺省访问属性的方法,因为这些方法此时相当于**private**方法

  • 重写的方法能够抛出任何非强制异常无论被重写的方法是否抛出异常也可以不抛出异常,即使被重写的方法抛出了异常

但是,重写的方法不能抛出新的强制性异常,只能抛出相同的强制性异常或强制性异常的子类异常
例如: 父类的一个方法申明了一个检查异常IOException,但是在重写这个方法的时候不能抛出 Exception异常,因为ExceptionIOException的父类,只能抛出IOException的子类异常。

  • 当需要在子类中调用父类的被重写方法时,需要使用super关键字。

2 重载

  • 重载的定义

重载(Overload) 是在同一个类里面或者在子类中,定义名字相同,而参数列表不同的方法

  • 重载最常用的地方就是构造器的重载
  • 如果父类的方法被子类继承,那么也是可以被重载的
  • 重载规则
    • 重载的方法参数列表必须不同(参数个数或类型或顺序不同)
    • 重载的方法返回类型可以不同
    • 重载的方法访问修饰符和非访问修饰符都可以不同
    • 重载的方法可以声明新的或更广的强制性异常;

3 重写与重载的区别

面向对象三特性 - 图3

区别点 重载方法 重写方法
参数列表 必须修改 一定不能修改
返回类型 可以修改 可以修改,但仅限于返回值类型的子类
异常 可以修改
- 可以减少或删除异常
- 可以抛出新的非强制性异常
- 不能抛出新的或者更广的强制性异常
访问 可以修改 一定不能做更严格的限制(可以降低限制)

5 抽象类和抽象方法

  • 在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。

  • 抽象类除了不能被实例化为对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。

    • 由于抽象类不能被实例化为对象,所以抽象类必须被继承后才能使用

也是因为这个原因,通常在设计阶段决定要不要设计抽象类。

  • Java使用**abstract**关键字来声明抽象类和抽象方法

1 抽象类

定义抽象类

  1. abstract class Employee {
  2. private String name;
  3. private String address;
  4. private int number;
  5. public Employee(String name, String address, int number) {
  6. System.out.println("Constructing an Employee");
  7. this.name = name;
  8. this.address = address;
  9. this.number = number;
  10. }
  11. public double computePay() {
  12. System.out.println("Inside Employee computePay");
  13. return 0.0;
  14. }
  15. public void mailCheck() {
  16. System.out.println("Mailing a check to " + this.name
  17. + " " + this.address);
  18. }
  19. public String toString() {
  20. return name + " " + address + " " + number;
  21. }
  22. public String getName() {
  23. return name;
  24. }
  25. public String getAddress() {
  26. return address;
  27. }
  28. public int getNumber() {
  29. return number;
  30. }
  31. }
  • Employee类与普通的类相比没有什么不同,它有3个成员变量,7个成员方法和1个构造方法,同时,并不包含抽象方法

但是Employee类无法被实例化为对象:

  1. public class MyClass {
  2. public static void main(String[] args) {
  3. //下方代码报错
  4. Employee e = new Employee("George W.", "Houston, TX", 43);
  5. }
  6. }

继承抽象类

  • 继承自Employee类的Salary类

    1. class Salary extends Employee {
    2. private double salary; //Annual salary
    3. public Salary(String name, String address, int number, double
    4. salary) {
    5. super(name, address, number);
    6. setSalary(salary);
    7. }
    8. public double computePay() {
    9. System.out.println("Computing salary pay for " + getName());
    10. return salary / 52;
    11. }
    12. public void mailCheck() {
    13. System.out.println("Within mailCheck of Salary class ");
    14. System.out.println("Mailing check to " + getName()
    15. + " with salary " + salary);
    16. }
    17. public double getSalary() {
    18. return salary;
    19. }
    20. public void setSalary(double newSalary) {
    21. if (newSalary >= 0.0) {
    22. salary = newSalary;
    23. }
    24. }
    25. }
    • 我们不能实例化一个Employee类的对象,但是我们可以实例化一个继承自Employee类的Salary类对象,该对象将从 Employee类继承7个成员方法,重写了其中2个,然后又新定义了2个成员方法。

执行下述代码

  1. public class MyClass {
  2. public static void main(String[] args) {
  3. Employee e = new Salary("John Adams", "Boston, MA", 2, 2400.00);
  4. System.out.println("\nCall mailCheck using Employee reference--");
  5. e.mailCheck();
  6. }
  7. }

输出结果为

  1. Constructing an Employee
  2. Call mailCheck using Employee reference--
  3. Within mailCheck of Salary class
  4. Mailing check to John Adams with salary 2400.0

2 抽象方法

  • 如果抽象类内的某个方法只能由其子类实现,那么需要在抽象类中声明该方法为抽象方法。

    • 对于一个父类,如果它的某个方法在父类中实现出来没有任何意义,必须根据子类的实际需求来进行不同的实现,那么就可以将这个方法声明为抽象方法,此时这个类也就成为抽象类了。
  • 抽象方法必须使用**abstract**关键字声明

抽象方法没有定义实现,所以方法名小括号后面直接跟一个分号,而不是花括号。

  1. abstract class Employee
  2. {
  3. private String name;
  4. private String address;
  5. private int number;
  6. public abstract double computePay(); //抽象方法
  7. ...
  8. }
  • 抽象方法使用规则

    • 任何子类必须重写父类的抽象方法,或者声明自身为抽象类。

    • 抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类。

    • 抽象方法必须为**public**或者**protected**,建议显式声明为public

    • 抽象方法必须用**abstract**显式声明

    • 子类重写父类的抽象方法时,访问控制不能更严格


  • 构造方法,类方法(用**static**修饰的方法)不能声明为抽象方法
  • 继承抽象方法的子类必须重写该方法。否则,该子类也必须声明为抽象类。

最终,必须有子类实现该抽象方法,否则,从最初的父类到最终的子类都不能用来实例化对象(因为都是抽象类了)

  • 例如:Salary类继承了上面的Employee类,那么它必须实现computePay()方法:

    1. class Salary extends Employee
    2. {
    3. private double salary;
    4. public double computePay(){
    5. System.out.println("Computing salary pay for " + getName());
    6. return salary/52;
    7. }
    8. ...
    9. }

6 接口

  • 接口(interface)定义
    • 接口是一个抽象类型,是抽象方法的集合,接口以interface关键字来声明。

一个类通过实现(implements)接口来实现接口的抽象方法。

  • 接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念。

类描述对象的属性和方法;接口则包含类要实现的方法。

  • 接口内可以包含变量和方法
  • 接口无法被实例化,但是可以被类实现。


  • 接口变量

接口类型可用来声明一个变量,变量将成为一个空指针,或是引用实现此接口的对象。

  1. public class MyClass {
  2. public static void main(String[] args) {
  3. Animal dog = new Dog(); //接口类型变量
  4. }
  5. }
  6. interface Animal {
  7. void eat();
  8. }
  9. class Dog implements Animal {
  10. public void eat() {}
  11. }

1 接口和接口内方法的声明

  • 接口的声明

接口是隐式抽象的,当声明一个接口的时候,不必使用abstract关键字。
接口的声明语法格式如下:

  1. [访问控制] interface 接口名称 [extends 其他的接口名] {
  2. // 声明变量
  3. // 声明抽象方法
  4. }
  • **interface**关键字用来声明一个接口。(相对应的,class关键字用来声明类)
    1. interface Interface{} //声明一个接口
    2. class Class{} //声明一个类
  • 接口内方法的声明

接口内的方法和抽象方法一样,没有定义实现,方法名后直接是冒号

  1. interface Animal {
  2. void eat();
  3. void travel();
  4. }
  • 接口中的方法默认是**public abstract**,不需要再额外修饰

在接口中,也可以使用**default****static**修饰方法(这也就意味着接口中可以不止有抽象方法)

  • static

Java11前通常将接口需要用到的静态方法放在伴随类中
因此在标准库中有很多成对出现的接口和伴随工具类,如Collection/CollectionsPath/Paths
Java11中,Path接口提供了与伴随类等价的静态方法,这样Paths类就不再是必须的了
因此在实现自己的接口时,也没必要再为工具方法提供另外一个伴随类。

  • default
    • 可以在接口中声明一个default修饰的方法,这样做的一个重要的作用就是接口演化
    • 比如在Java8中为Collection接口增加了一个stream()方法,如果stream()不是默认方法,那么很多实现了该接口的类将不能通过编译,因为没有实现新增的方法。
  • 接口内的变量

接口内可以声明变量,默认是**public static final**,也只能是**public static final**

2 接口的实现

  • 类实现接口

类使用implements关键字实现接口,位置与继承的关键字extends相同

  • 实例 ```java interface Animal { void eat(); void travel(); }

class MammalInt implements Animal { public void eat() { System.out.println(“Mammal eats”); } public void travel() { System.out.println(“Mammal travels”); } }

public class MyClass { public static void main(String[] args) { MammalInt m = new MammalInt(); m.eat(); //out: Mammal eats m.travel(); //out: Mammal travels } }

  1. - **类实现接口的规则**
  2. - **一个实现接口的类,必须实现接口内所描述的所有方法**,否则就必须声明为抽象类。
  3. - **一个类可以实现多个接口**
  4. - **实现接口的类不能继承接口的静态方法,但是继承抽象类的类可以继承抽象类的静态方法**
  5. Java支持实现多个接口,所以如果实现接口的类可以继承接口的静态方法,那么类所实现的多个接口出现同名静态方法时,编译器就不知道要调用哪个方法了
  6. - **重写接口中声明的方法时,需要注意以下规则:**
  7. - 类在实现接口的方法时,**不能抛出强制性异常**,只能在接口中,或者继承接口的抽象类中抛出该强制性异常。
  8. - 类在重写方法时**必须要保持一致的方法名、一致的参数列表、相兼容的返回值类型**。
  9. <a name="2D16W"></a>
  10. ### 3 接口的继承
  11. - **接口的继承**
  12. - **一个接口能继承另一个接口**,和类之间的继承方式比较相似,同样使用`extends`关键字
  13. - **实现一个接口时,同样需要实现其父接口的方法**
  14. - 下面的`Sports`接口被`Hockey``Football`接口继承:
  15. ```java
  16. interface Sports {
  17. void setHomeTeam(String name);
  18. void setVisitingTeam(String name);
  19. }
  20. interface Football extends Sports {
  21. void homeTeamScored(int points);
  22. void visitingTeamScored(int points);
  23. void endOfQuarter(int quarter);
  24. }
  25. interface Hockey extends Sports {
  26. void homeGoalScored();
  27. void visitingGoalScored();
  28. void endOfPeriod(int period);
  29. void overtimePeriod(int ot);
  30. }
  • Hockey接口自己声明了四个方法,从Sports接口继承了两个方法,这样,实现**Hockey**接口的类实际上需要实现六个方法。

相似的,实现Football接口的类需要实现五个方法,其中两个来自于Sports接口。

  • 接口的多继承

类的多继承是不合法,但接口允许多继承
接口的多继承中**extends**关键字只需要使用一次,在其后跟着继承接口。

  1. public interface Hockey extends Sports, Event

4 标记接口

  • 没有任何方法的接口被称为标记接口

例如:java.awt.event包中的MouseListener接口继承的java.util.EventListener接口定义如下

  1. public interface EventListener{}
  • 实现标记接口的目的
    • 给实现标记接口的类一个标签,表明类属于一个特定的类型

常使用**instanceof**进行类型查询,进而判断类是否实现了某些接口

  1. interface Animal { }
  2. class Dog implements Animal{ }
  3. public class MyClass {
  4. public static void main(String[] args) {
  5. Dog dog = new Dog();
  6. System.out.println(dog instanceof Animal); //out: true
  7. }
  8. }
  • 建立一个公共的父接口

正如EventListener接口,这是被几十个其他接口扩展的Java API,你可以使用一个标记接口来建立一组接口的父接口。例如:当一个接口继承了EventListener接口,Java虚拟机(JVM)就知道该接口将要被用于一个事件的代理方案。

7 抽象类和接口的区别

  • 应用场景的不同

    • 抽象类是对一类事物的抽象,即对类抽象,而接口是对行为的抽象
    • 抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象

    • 实例

飞机和鸟是不同类的事物,但是它们都有一个共性,就是都会飞。那么在设计的时候,可以将飞机设计为一个类Airplane,将鸟设计为一个类Bird,但是不能将飞行这个特性也设计为类,因为它只是一个行为特性,并不是对一类事物的抽象描述。此时可以将飞行设计为一个接口Fly,包含方法fly( ),然后AirplaneBird分别根据自己的需要实现Fly这个接口。然后至于有不同种类的飞机,比如战斗机、民用飞机等直接继承Airplane即可,对于鸟也是类似的,不同种类的鸟直接继承Bird类即可。

从这里可以看出,类继承是一个 “是不是”的关系,而接口实现则是 “有没有”的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系,比如鸟是否能飞(或者是否具备飞行这个特点),能飞行则可以实现这个接口,不能飞行就不实现这个接口。

  • 设计层面不同

    • 抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计

    • 模板式设计

什么是模板式设计?最简单例子,大家都用过ppt里面的模板,如果用模板A设计了ppt B和ppt C,ppt B和 ppt C公共的部分就是模板 A了,如果它们的公共部分需要改动,则只需要改动模板A就可以了,不需要重新对ppt B和ppt C进行改动。

  • 辐射式设计

辐射式设计,比如某个电梯都装了某种报警器,一旦要更新报警器,就必须全部更新。

  • 更新
    • 对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更;
    • 对于接口如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动

8 多态

对于接口List<>来说,其方法get()在ArrayList和LinkedList的实现下,内部机制完全不同,这就是多态的具体展现

  • 多态的定义

多态是同一个行为具有多个不同表现形式或形态的能力。

  • 例如:

    1. 现实中,比如我们按下 F1 键这个动作:
    2. 如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;
    3. 如果当前在 Word 下弹出的就是 Word 帮助;
    4. Windows 下弹出的就是 Windows 帮助和支持。
  • 多态一般分为重写式多态和重载式多态。在面向对象编程中,主要讲的是重写式多态

    有的人认为重载不算作多态,而且重载不是面向对象编程特有的。

  • 重写式多态

重写式多态是通过动态绑定技术来实现,是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法
也就是说,只有程序运行起来,你才知道调用的是哪个子类的方法

  • 多态的优点

    1. 消除类型之间的耦合关系
    2. 可替换性、可扩充性
    3. 接口性、灵活性、简化性
  • 多态的必要条件

    • 继承类/抽象类 或 实现接口
    • 重写
    • 向上转型(父类引用指向子类对象**Parent p = new Child()**
  • 当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。