第一节 子类与继承

基本概念

在 Java 中,一个类可以由其他类派生。如果你要创建一个类,而且已经存在一个类具有你所需要的属性或方法,那么你可以将新创建的类继承该类。

利用继承的方法,可以重用已存在类的方法和属性,而不用重写这些代码。被继承的类称为超类(super class),派生类称为子类(sub class)。

在Java中,由于Java不支持多重继承(即一个类不能同时继承多个类),如果子类需要从两个父类中继承方法,可以使用接口和组合(composition)来实现这种需求。

在Java中,继承是一个 "全有或全无" 的机制,也就是说,子类继承父类时会继承父类的所有非私有属性和方法。

子类如果只想继承父类的部分属性和方法,可以:

  • 使用组合(Composition)
// 父类
class Parent {
    public void methodA() {
        System.out.println("Method A");
    }

    public void methodB() {
        System.out.println("Method B");
    }

    private void methodC() {
        System.out.println("Method C");
    }
}

// 子类使用-组合
public class SubClass {   //注意这里没有使用继承
    private Parent parent = new Parent();

    // 选择性地使用父类的方法
    public void methodA() {
        parent.methodA();
    }

    // 不使用父类的 methodB 和 methodC

    public static void main(String[] args) {
        SubClass subClass = new SubClass();
        subClass.methodA();  // 输出:Method A
        // subClass.methodB();  // 错误:SubClass 中没有定义 methodB
    }
}
  • 使用接口和适配器模式
// 定义接口
interface ParentInterface {
    void methodA();
    void methodB();
}

// 父类实现接口
class Parent implements ParentInterface {  //接口的实现
    @Override
    public void methodA() {
        System.out.println("Method A");
    }

    @Override
    public void methodB() {
        System.out.println("Method B");
    }

    public void methodC() {
        System.out.println("Method C");
    }
}

// 子类继承父类并实现接口
public class SubClass extends Parent {  // 继承父类
    // 重写 methodB,选择不使用它
    @Override
    public void methodB() {  // 不想继承的方法
        // 不做任何事,或抛出异常
        throw new UnsupportedOperationException("MethodB is not supported");
    }

    public static void main(String[] args) {
        SubClass subClass = new SubClass();
        subClass.methodA();  // 输出:Method A
        subClass.methodB();  // 抛出 UnsupportedOperationException
    }
}
  • 使用抽象类
// 抽象父类
abstract class AbstractParent {  // 抽象类
    public void methodA() {
        System.out.println("Method A");
    }

    public abstract void methodB();
}

// 子类继承抽象父类,并选择性实现方法
public class SubClass extends AbstractParent {  // 继承
    @Override
    public void methodB() {  // 不想继承的方法
        // 不做任何事,或抛出异常
        throw new UnsupportedOperationException("MethodB is not supported");
    }

    public static void main(String[] args) {
        SubClass subClass = new SubClass();
        subClass.methodA();  // 输出:Method A
        subClass.methodB();  // 抛出 UnsupportedOperationException
    }
}

继承是java面向对象编程技术的一块基石,因为它允许创建分等级层次的类。

继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。

【添油加醋的Java基础】第六章 继承与多态_夏明亮

兔子和羊属于食草动物类,狮子和豹属于食肉动物类。

食草动物和食肉动物又是属于动物类。

所以继承需要符合的关系是:is-a,父类更通用,子类更具体。

虽然食草动物和食肉动物都是属于动物,但是两者的属性和行为上有差别,所以子类会具有父类的一般特性也会具有自身的特性。

类的继承格式

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

class 父类 {
}
 
class 子类 extends 父类 {
}

为什么需要继承

接下来我们通过实例来说明这个需求。

开发动物类,其中动物分别为企鹅以及老鼠,要求如下:

  • 企鹅:属性(姓名,id),方法(吃,睡,自我介绍)
  • 老鼠:属性(姓名,id),方法(吃,睡,自我介绍)

企鹅类:

public class Penguin { 
    private String name; 
    private int id; 
    public Penguin(String myName, int  myid) { 
        name = myName; 
        id = myid; 
    } 
    public void eat(){ 
        System.out.println(name+"正在吃"); 
    }
    public void sleep(){
        System.out.println(name+"正在睡");
    }
    public void introduction() { 
        System.out.println("大家好!我是"         + id + "号" + name + "."); 
    } 
}

老鼠类:

public class Mouse { 
    private String name; 
    private int id; 
    public Mouse(String myName, int  myid) { 
        name = myName; 
        id = myid; 
    } 
    public void eat(){ 
        System.out.println(name+"正在吃"); 
    }
    public void sleep(){
        System.out.println(name+"正在睡");
    }
    public void introduction() { 
        System.out.println("大家好!我是"         + id + "号" + name + "."); 
    } 
}

这两段代码可以看出,代码存在重复,导致代码量大且臃肿,而且维护性不高(主要表现是后期需要修改的时候,需要修改很多的代码,容易出错)。要从根本上解决这两段代码的问题,就需要继承,将两段代码中相同的部分提取出来组成 一个父类:

企鹅和老鼠的公共父类:Animal

public class Animal { 
    private String name;  
    private int id; 
    public Animal(String myName, int myid) { 
        name = myName; 
        id = myid;
    } 
    public void eat(){ 
        System.out.println(name+"正在吃"); 
    }
    public void sleep(){
        System.out.println(name+"正在睡");
    }
    public void introduction() { 
        System.out.println("大家好!我是"         + id + "号" + name + "."); 
    } 
}

这个Animal类就可以作为一个父类,然后企鹅类和老鼠类继承这个类之后,就具有父类当中的属性和方法,子类就不会存在重复的代码,维护性也提高,代码也更加简洁,提高代码的复用性(复用性主要是可以多次使用,不用再多次写同样的代码) 继承之后的代码:

继承了Animal类的企鹅类:

public class Penguin extends Animal { 
    public Penguin(String myName, int myid) { 
        super(myName, myid); 
    } 
}

继承了Animal类的老鼠类:

public class Mouse extends Animal { 
    public Mouse(String myName, int myid) { 
        super(myName, myid); 
    } 
}

继承类型

需要注意的是 Java 不支持多继承,但支持多重继承。

【添油加醋的Java基础】第六章 继承与多态_接口_02

继承的特性

  • 子类拥有父类非 private 的属性、方法。
  • 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
  • 子类可以用自己的方式实现父类的方法。
  • Java 的继承是单继承,但是可以多重继承,单继承就是一个子类只能继承一个父类,多重继承就是,例如 B 类继承 A 类,C 类继承 B 类,所以按照关系就是 B 类是 C 类的父类,A 类是 B 类的父类,这是 Java 继承区别于 C++ 继承的一个特性。
  • 提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系越紧密,代码独立性越差)。

继承关键字

继承可以使用 extends 和 implements 这两个关键字来实现继承,而且所有的类都是继承于 java.lang.Object,当一个类没有继承的两个关键字,则默认继承 Object(这个类在 java.lang 包中,所以不需要 import)祖先类。

extends关键字

在 Java 中,类的继承是单一继承,也就是说,一个子类只能拥有一个父类,所以 extends 只能继承一个类。

// 父类
public class Animal { 
    private String name;   
    private int id; 
    public Animal(String myName, int myid) { 
        //初始化属性值
    } 
    public void eat() {  //吃东西方法的具体实现  } 
    public void sleep() { //睡觉方法的具体实现  } 
} 

// 子类
public class Penguin  extends  Animal{  // 只能继承自一个父类,不允许多个
    // my code
}
implements关键字

使用 implements 关键字可以变相的使java具有多继承的特性,使用范围为类继承接口的情况,可以同时继承多个接口(接口跟接口之间采用逗号分隔)。

public interface A {
    public void eat();
    public void sleep();
}
 
public interface B {
    public void show();
}
 
public class C implements A,B { // 可以同时实现多个接口
}

super 与 this 关键字

super关键字:我们可以通过super关键字来实现对父类成员的访问,用来引用当前对象的父类。

this关键字:指向自己的引用。

class Animal {
  void eat() {
    System.out.println("animal : eat");
  }
}
 
class Dog extends Animal {
  void eat() {
    System.out.println("dog : eat");
  }
  void eatTest() {
    this.eat();   // this 调用自己的方法
    super.eat();  // super 调用父类方法
  }
}
 
public class Test {
  public static void main(String[] args) {
    Animal a = new Animal();
    a.eat();
    Dog d = new Dog();
    d.eatTest();
  }
}

输出结果为:

animal : eat
dog : eat
animal : eat

final 关键字

final 可以用来修饰变量(包括类属性、对象属性、局部变量和形参)、方法(包括类方法和对象方法)和类。

final 含义为 "最终的"。

使用 final 关键字声明类,就是把类定义定义为最终类不能被继承,或者用于修饰方法,该方法不能被子类重写

  • 声明类:
final class 类名 {//类体}
  • 声明方法:
修饰符(public/private/default/protected) final 返回值类型 方法名(){//方法体}

注: final 定义的类,其中的属性、方法不是 final 的;但即使final类中的非final方法也是不能被重写的,不是不能够而是根本无法实现;相当于告诉你房间里的物品你可以随便使用,但是大门被锁上了。

这里就有个炸裂的设计,捋一捋这个逻辑:

虽然Final Class不能被继承,但它的方法不一定需要被声明为final。方法不是final的含义是,如果该类可以被继承,那么这些方法是可以被重写的。但是,由于Final Class本身已经被final修饰,子类根本无法存在,所以重写方法的问题自然不存在。

仔细想想允许final类中的方法不是final有几个原因:

  1. 代码简洁性和可读性:不需要对每个方法都加上final关键字,这样代码更简洁。
  2. 灵活性:开发者可以选择性地将某些方法声明为final,以防止它们在潜在的子类中被重写。如果整个类是final的,这种控制就没有必要了。
  3. 不产生矛盾:虽然方法不是final,但因为类本身是final的,所以重写方法的场景不会发生,不会产生矛盾。

构造器-构造函数

子类是不继承父类的构造器(构造方法或者构造函数)的,它只是调用(隐式或显式)。

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

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

class SuperClass {
  private int n;
  SuperClass(){
    System.out.println("SuperClass()");
  }
  SuperClass(int n) {
    System.out.println("SuperClass(int n)");
    this.n = n;
  }
}
// SubClass 类继承
class SubClass extends SuperClass{
  private int n;
  
  SubClass(){ // 自动调用父类的无参数构造器
    System.out.println("SubClass");
  }  
  
  public SubClass(int n){ 
    super(300);  // 调用父类中带有参数的构造器
    System.out.println("SubClass(int n):"+n);
    this.n = n;
  }
}
// SubClass2 类继承
class SubClass2 extends SuperClass{
  private int n;
  
  SubClass2(){
    super(300);  // 调用父类中带有参数的构造器
    System.out.println("SubClass2");
  }  
  
  public SubClass2(int n){ // 自动调用父类的无参数构造器
    System.out.println("SubClass2(int n):"+n);
    this.n = n;
  }
}
public class TestSuperSub{
  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); 
  }
}

输出结果:

------SubClass 类继承------
SuperClass()
SubClass
SuperClass(int n)
SubClass(int n):100
------SubClass2 类继承------
SuperClass(int n)
SubClass2
SuperClass()
SubClass2(int n):200

第二节 方法覆盖与多态

Java 重写(Override)与重载(Overload)

重写(Override)

重写(Override)是指子类定义了一个与其父类中具有相同名称、参数列表和返回类型的方法,并且子类方法的实现覆盖了父类方法的实现。 即外壳不变,核心重写!

重写的好处在于子类可以根据需要,定义特定于自己的行为。也就是说子类能够根据需要实现父类的方法。这样,在使用子类对象调用该方法时,将执行子类中的方法而不是父类中的方法。

重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常。例如: 父类的一个方法申明了一个检查异常 IOException,但是在重写这个方法的时候不能抛出 Exception 异常,因为 Exception 是 IOException 的父类,抛出 IOException 异常或者 IOException 的子类异常。

在面向对象原则里,重写意味着可以重写任何现有方法。实例如下:

class Animal{
   public void move(){
      System.out.println("动物可以移动");
   }
}
 
class Dog extends Animal{
   public void move(){
      System.out.println("狗可以跑和走");
   }
}
 
public class TestDog{
   public static void main(String args[]){
      Animal a = new Animal(); // Animal 对象
      Animal b = new Dog(); // Dog 对象
 
      a.move();// 执行 Animal 类的方法
 
      b.move();//执行 Dog 类的方法
   }
}

运行结果如下

动物可以移动
狗可以跑和走

例子中可以看到,尽管 b 属于 Animal 类型,但是它运行的是 Dog 类的 move方法。

这是由于在编译阶段,只是检查参数的引用类型。

然而在运行时,Java 虚拟机(JVM)指定对象的类型并且运行该对象的方法。

因此在上面的例子中,之所以能编译成功,是因为 Animal 类中存在 move 方法,然而运行时,运行的是特定对象的方法。

看看这个例子:

class Animal{
   public void move(){
      System.out.println("动物可以移动");
   }
}
 
class Dog extends Animal{
   public void move(){
      System.out.println("狗可以跑和走");
   }
   public void bark(){
      System.out.println("狗可以吠叫");
   }
}
 
public class TestDog{
   public static void main(String args[]){
      Animal a = new Animal(); // Animal 对象
      Animal b = new Dog(); // Dog 对象
 
      a.move();// 执行 Animal 类的方法
      b.move();//执行 Dog 类的方法
      b.bark();  // 将抛出一个编译错误,因为b的引用类型Animal没有bark方法。
   }
}
方法的重写规则
  • 参数列表与被重写方法的参数列表必须完全相同。
  • 返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类(java5 及更早版本返回类型要一样,java7 及更高版本可以不同)。
  • 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为 public,那么在子类中重写该方法就不能声明为 protected。
  • 父类的成员方法只能被它的子类重写。
  • 声明为 final 的方法不能被重写。
  • 声明为 static 的方法不能被重写,但是能够被再次声明。
  • 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法。
  • 子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法。
  • 重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。
  • 构造方法不能被重写。
  • 如果不能继承一个类,则不能重写该类的方法。
Super 关键字的使用

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

例子:

class Animal{
   public void move(){
      System.out.println("动物可以移动");
   }
}
 
class Dog extends Animal{
   public void move(){
      super.move(); // 应用super类的方法
      System.out.println("狗可以跑和走");
   }
}
 
public class TestDog{
   public static void main(String args[]){
 
      Animal b = new Dog(); // Dog 对象
      b.move(); //执行 Dog类的方法
 
   }
}

运行结果如下:

动物可以移动
狗可以跑和走
重载(Overload)

重载(overloading) 是在**一个类**里面,方法名字相同,而参数不同返回类型可以相同也可以不同

每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表

最常用的地方就是构造器的重载。

重载规则:

  • 被重载的方法必须改变参数列表(参数个数或类型不一样);
  • 被重载的方法可以改变返回类型;
  • 被重载的方法可以改变访问修饰符;
  • 被重载的方法可以声明新的或更广的检查异常;
  • 方法能够在同一个类中或者在一个子类中被重载。
  • 无法以返回值类型作为重载函数的区分标准。

例子:

public class Overloading {
    public int test(){
        System.out.println("test1");
        return 1;
    }
 
    public void test(int a){  // 与public int test()构成重载
        System.out.println("test2");
    }   
 
    //以下两个参数类型顺序不同
    public String test(int a,String s){
        System.out.println("test3");
        return "returntest3";
    }   
 
    public String test(String s,int a){// 与public String test(int a,String s)构成重载
        System.out.println("test4");
        return "returntest4";
    }   
 
    public static void main(String[] args){
        Overloading o = new Overloading();
        System.out.println(o.test());
        o.test(1);
        System.out.println(o.test(1,"test3"));
        System.out.println(o.test("test4",1));
    }
}
重写与重载之间的区别

区别点

重载方法

重写方法

参数列表

必须修改

一定不能修改

返回类型

可以修改

一定不能修改

异常

可以修改

可以减少或删除,一定不能抛出新的或者更广的异常

访问

可以修改

一定不能做更严格的限制(可以降低限制)

方法的重写(Overriding)和重载(Overloading)是java多态性的不同表现,重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式。

  • (1)方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
  • (2)方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
  • (3)方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。

【添油加醋的Java基础】第六章 继承与多态_多态_03

【添油加醋的Java基础】第六章 继承与多态_接口_04


多态

多态的概念

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

多态就是同一个接口,使用不同的实例而执行不同操作,如图所示:

【添油加醋的Java基础】第六章 继承与多态_多态_05

多态性是对象多种表现形式的体现。

现实中,比如我们按下 F1 键这个动作:

  • 如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;
  • 如果当前在 Word 下弹出的就是 Word 帮助;
  • 在 Windows 下弹出的就是 Windows 帮助和支持。

同一个事件发生在不同的对象上会产生不同的结果。

多态的优点
  • 消除类型之间的耦合关系
  • 可替换性
  • 可扩充性
  • 接口性
  • 灵活性
  • 简化性
多态存在的三个必要条件
  • 继承
  • 重写
  • 父类引用指向子类对象:Parent p = new Child();

【添油加醋的Java基础】第六章 继承与多态_夏明亮_06

代码:

class Shape {
    void draw() {}
}
  
class Circle extends Shape {
    void draw() {
        System.out.println("Circle.draw()");
    }
}
  
class Square extends Shape {
    void draw() {
        System.out.println("Square.draw()");
    }
}
  
class Triangle extends Shape {
    void draw() {
        System.out.println("Triangle.draw()");
    }
}

当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。

多态的好处:可以使程序有良好的扩展,并可以对所有类的对象进行通用处理。

以下是一个多态实例的演示,详细说明请看注释:

public class Test {
    public static void main(String[] args) {
      show(new Cat());  // 以 Cat 对象调用 show 方法
      show(new Dog());  // 以 Dog 对象调用 show 方法
                
      Animal a = new Cat();  // 向上转型  
      a.eat();               // 调用的是 Cat 的 eat
      Cat c = (Cat)a;        // 向下转型  
      c.work();        // 调用的是 Cat 的 work
  }  
            
    public static void show(Animal a)  {
      a.eat();  
        // 类型判断
        if (a instanceof Cat)  {  // 猫做的事情 
            Cat c = (Cat)a;  
            c.work();  
        } else if (a instanceof Dog) { // 狗做的事情 
            Dog c = (Dog)a;  
            c.work();  
        }  
    }  
}
 
abstract class Animal {  
    abstract void eat();  
}  
  
class Cat extends Animal {  
    public void eat() {  
        System.out.println("吃鱼");  
    }  
    public void work() {  
        System.out.println("抓老鼠");  
    }  
}  
  
class Dog extends Animal {  
    public void eat() {  
        System.out.println("吃骨头");  
    }  
    public void work() {  
        System.out.println("看家");  
    }  
}

输出结果为:

吃鱼
抓老鼠
吃骨头
看家
吃鱼
抓老鼠

虚函数(Java中无此概念,本来来自于C++)

虚函数的存在是为了多态。

Java 中其实没有虚函数的概念,它的普通函数就相当于 C++ 的虚函数,动态绑定是Java的默认行为。如果 Java 中不希望某个函数具有虚函数特性,可以加上 final 关键字变成非虚函数。

在Java中,虚函数(Virtual Function)是指可以在子类中被重写(Override)的方法。Java中的所有非静态方法默认就是虚函数,除非它们被声明为finalstaticprivate

虚函数的主要目的是实现多态性(Polymorphism)。通过多态性,一个父类引用可以指向任何它的子类对象,并且调用子类的重写方法。虚函数机制允许在运行时根据对象的实际类型调用相应的方法。

重写(Override)和隐藏(Hide)
  1. 方法重写(Override)
  • 子类提供了父类方法的具体实现。
  • 方法签名(方法名、参数类型和顺序)必须相同。
  • 子类方法的访问权限不能比父类方法更严格。
  • 子类方法的返回类型必须与父类方法相同,或者是其子类型(协变返回类型)。
  1. 方法隐藏(Hide)
  • 静态方法的重写实际上是隐藏,方法的选择在编译时确定。
  • 静态方法无法实现运行时的多态性。

一个例子:

class Parent {
    static void staticMethod() {
        System.out.println("Parent's staticMethod()");
    }

    void instanceMethod() {
        System.out.println("Parent's instanceMethod()");
    }
}

class Child extends Parent {
    static void staticMethod() {
        System.out.println("Child's staticMethod()");
    }

    @Override  // 注意大小写
    void instanceMethod() {
        System.out.println("Child's instanceMethod()");
    }
}

public class Main {
    public static void main(String[] args) {
        Parent p = new Child();
        p.staticMethod();  // 调用的是 Parent 类的 staticMethod()
        p.instanceMethod();  // 调用的是 Child 类的 instanceMethod()
    }
}

默认虚函数:Java中所有非静态、非final、非private的方法都是虚函数。

多态性:虚函数支持运行时多态,通过父类引用调用子类实现。

重写:子类可以重写父类的方法,提供具体实现。

访问控制:子类重写方法时,访问权限不能比父类方法更严格。

虚函数 VS. 抽象函数

虚函数(Virtual Function)

虚函数的概念源自C++,在Java中没有虚函数这个术语,但Java的所有非静态方法都具有类似于C++虚函数的行为,因为Java支持动态绑定(Dynamic Binding)。

特点:

  1. 动态绑定:方法的调用在运行时解析,而不是在编译时。
  2. 继承与多态:子类可以覆盖(Override)父类的方法,并且在运行时根据实际对象类型调用对应的方法。
  3. 默认行为:Java中的所有非静态方法默认都是虚方法,可以被子类覆盖。

示例:

class Animal {
    void makeSound() { // makeSound 方法是一个虚方法, 因为它不是final/static/private任何一种的
        System.out.println("Animal makes a sound");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() { //makeSound 方法也是一个虚方法, 因为它不是final/static/private任何一种的
        System.out.println("Dog barks");
    }
}

public class TestVirtualFunction {
    public static void main(String[] args) {
        Animal myDog = new Dog();
        myDog.makeSound(); // Output: Dog barks
        // makeSound 方法是一个虚方法,实际调用的是Dog类中的实现。
    }
}
抽象函数(Abstract Method)

抽象函数是在抽象类中声明的,没有具体实现的方法。它们必须在非抽象子类中实现。

Abstract 关键字用来声明抽象方法,抽象方法只包含一个方法名,而没有方法体。

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

特点:

  1. 无方法体:抽象方法没有方法体,只有方法声明。
  2. 必须实现:任何继承抽象类的非抽象子类必须实现所有的抽象方法。
  3. 抽象类:抽象方法必须声明在抽象类中,抽象类本身也不能实例化。

抽象类:(下一节也会再次讲到)

  1. 抽象类不能够生成对象;
  2. 如果一个类当中包含有抽象函数,那么这个类必须被声明为抽象类
  3. 如果一个类当中没有抽象函数,这个类也可以被声明为抽象类。
  4. 构造方法,类方法(用 static 修饰的方法)不能声明为抽象方法。
abstract class Animal {  // 抽象类
    abstract void makeSound();  // 抽象方法,存在抽象方法的类必须被声明为抽象类
}

class Dog extends Animal {  // 子类继承了抽象类,如果这个之类没有使用abstract修饰,那么,它必须实现抽象类中所有的抽象方法;否则子类必须也要被声明为抽象类
    @Override
    void makeSound() {  // 实现了父抽象类中从抽象方法
        System.out.println("Dog barks");
    }
}

public class TestAbstractFunction {
    public static void main(String[] args) {
        Animal myDog = new Dog();
        myDog.makeSound(); // Output: Dog barks
    }
}
区别总结
  1. 定义
  • 虚函数:Java中没有专门的虚函数,但所有非静态可继承的方法都可以看作虚函数,支持动态绑定。
  • 抽象函数:在抽象类中定义,没有实现,必须由子类实现。
  1. 实现
  • 虚函数:可以在父类中实现,子类可以选择覆盖。
  • 抽象函数:没有实现,必须由子类实现。
  1. 用法
  • 虚函数:用于实现多态性,子类可以覆盖父类的方法。
  • 抽象函数:用于定义接口或模板方法,子类必须提供具体实现。
  1. 声明
  • 虚函数:任何非静态方法都可以视为虚函数。
  • 抽象函数:必须在抽象类中声明,并且没有方法体。

第三节 终极类与抽象类

终极类

在Java中,终极类指的是不能被继承的类。通过使用final关键字来声明一个类为终极类,这样的类不能被子类化。final类的主要目的是防止继承,保证类的行为不会在子类中被改变。

定义和使用终极类
public final class FinalClass {  // 使用final修饰
    private String value;  // 成员变量

    public FinalClass(String value) {  // 构造函数
        this.value = value;
    }

    public String getValue() {  // 成员方法
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    public void display() {
        System.out.println("Value: " + value);
    }
}

尝试继承一个终极类将会导致编译错误:

public class SubClass extends FinalClass {  // 编译错误:无法从最终FinalClass继承
    public SubClass(String value) {
        super(value);
    }
}
使用终极类的场景
  1. 安全性:有些类可能包含敏感信息或关键逻辑,不希望被继承和修改。
  2. 设计决策:有些类的设计不适合被继承,可能是因为它们的设计意图是完成某些特定任务,不需要扩展。
  3. 避免不必要的多态:确保类的行为在所有情况下保持一致,不会因为子类化而改变。
终极类的缺点
  1. 不灵活:一旦声明为终极类,就无法通过继承来扩展其功能,这对某些设计模式(如装饰器模式)可能不太友好。
  2. 限制测试:在单元测试中,不能使用子类来模拟或扩展终极类的行为。
final关键字的其他用途

除了用于类,final关键字还可以用于方法和变量:

  1. 终极方法:不能被子类重写的方法。
public class BaseClass { // 父类/基类
    public final void display() {
        System.out.println("Final method cannot be overridden");
    }
}

public class SubClass extends BaseClass { //子类
    // 编译错误:无法重写最终方法
    // public void display() {
    //     System.out.println("Attempt to override final method");
    // }
}
  1. 终极变量:一旦初始化后,不能再被改变的变量(常量)。
public class FinalVariableExample {
    public static final int CONSTANT = 10;

    public void changeConstant() {
        // 编译错误:无法为最终变量赋值
        // CONSTANT = 20;
    }
}

一个综合的例子:

public final class ImmutableClass {
    private final int value;

    public ImmutableClass(int value) {
        this.value = value;  //final变量如果在声明时没有初始化,那么它必须在构造函数中初始化,这是因为构造函数是对象生命周期中唯一可以修改 final 变量的时机。 一旦对象创建完成, final 变量的值就不可更改.
    }

    public int getValue() {
        return value;
    }

    public final void display() {
        System.out.println("Value: " + value);
    }
}

public class Main {
    public static void main(String[] args) {
        ImmutableClass obj = new ImmutableClass(100);
        obj.display();
    }
}

ImmutableClass是一个终极类,display方法是一个终极方法,保证了类和方法的行为不能被改变,从而提供了更高的安全性和一致性。

抽象类

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

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

由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。也是因为这个原因,通常在设计阶段决定要不要设计抽象类。

父类包含了子类集合的常见的方法,但是由于父类本身是抽象的,所以不能使用这些方法。

在 Java 中抽象类表示的是一种继承关系,一个类只能继承一个抽象类,而一个类却可以实现多个接口。

举个例子:

package com.xml.a;

public class Test41 {
	public static void main(String[] args) {
		x_x a = new x_x();
		a.get_x();
		
		y_y b = new y_y();
		b.get_y();
	}
}

abstract class x{ // 抽象类中可以没有抽象方法,但只要是abstract class就不能被实例化
	// void get_x();  // 错误写法;要么改成void get_x(){}  要么改成 abstract void get_x();
	// abstract void get_x(){};  //错误的语法 
	void get_x() {System.out.println("in x");}
}

abstract class y{ // 有抽象方法的类必须是抽象类
	int yy = 10;  // 抽象类也可以有成员变量
	abstract void get_y();
}

abstract class z{ // 有抽象方法的类必须是抽象类
	int zz = 11;  // 抽象类也可以有成员变量
	abstract void get_z();
	void get_z1() {};
}

class x_x extends x{ //继承了抽象类的子类要哦保证:如果自己不再声明为抽象类则必须确保父类中所欲的抽象方法都已实现;否则自己自能仍旧是抽象类
	
}

class y_y extends y{ //继承了抽象类的子类要哦保证:如果自己不再声明为抽象类则必须确保父类中所欲的抽象方法都已实现;否则自己自能仍旧是抽象类
	int yy = 50;
	void get_y() {
		int yy = 100;
		System.out.println("in y_y");
		System.out.println("变量yy的值:" + yy);  // 100;默认指的是离自己最近的yy
		System.out.println("变量yy的值:" + super.yy);  // 10;明确标识是父类的yy
		System.out.println("变量yy的值:" + this.yy);  // 50; 明确代表当前类的yy
	}
}

执行结果:

in x
in y_y
变量yy的值:100
变量super.yy的值:10
变量this.yy的值:50

这个例子需要仔细想清楚,知识点较多。

第四节 接口

接口(英文:Interface),在JAVA编程语言中是一个抽象类型,是抽象方法的集合,接口通常以interface来声明。一个类通过继承接口的方式,从而来继承接口的抽象方法。

接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念。类描述对象的属性和方法。接口则包含类要实现的方法。

除非实现接口的类是抽象类,否则该类要定义接口中的所有方法。

接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。另外,在 Java 中,接口类型可用来声明一个变量,他们可以成为一个空指针,或是被绑定在一个以此接口实现的对象。

在 Java 中,接口可理解为对象间相互通信的协议。接口在继承中扮演着很重要的角色。

接口只定义派生要用到的方法,但是方法的具体实现完全取决于派生类。

接口与类相似点:

  • 一个接口可以有多个方法。
  • 接口文件保存在 .java 结尾的文件中,文件名使用接口名。
  • 接口的字节码文件保存在 .class 结尾的文件中。
  • 接口相应的字节码文件必须在与包名称相匹配的目录结构中。

接口与类的区别:

  • 接口不能用于实例化对象。
  • 接口没有构造方法。
  • 接口中所有的方法必须是抽象方法,Java 8 之后 接口中可以使用 default 关键字修饰的非抽象方法。
  • 接口不能包含成员变量,除了 static 和 final 变量。
  • 接口不是被类继承了,而是要被类实现。
  • 接口支持多继承。

接口特性

  • 接口中每一个方法也是隐式抽象的,接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)。
  • 接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量(并且只能是 public,用 private 修饰会报编译错误)。
  • 接口中的方法是不能在接口中实现的,只能由实现接口的类来实现接口中的方法。
  • 一个接口能继承另一个接口,这和类之间的继承比较相似。
  • 一个类只能继承一个类,但是能实现多个接口。

标记接口

最常用的继承接口是没有包含任何方法的接口。

标记接口是没有任何方法和属性的接口.

没有任何方法的接口被称为标记接口。标记接口主要用于以下两种目的:

  • 建立一个公共的父接口:
    正如EventListener接口,这是由几十个其他接口扩展的Java API,你可以使用一个标记接口来建立一组接口的父接口。例如:当一个接口继承了EventListener接口,Java虚拟机(JVM)就知道该接口将要被用于一个事件的代理方案。
  • 向一个类添加数据类型:
    这种情况是标记接口最初的目的,实现标记接口的类不需要定义任何接口方法(因为标记接口根本就没有方法),但是该类通过多态性变成一个接口类型。

抽象类和接口的区别

  • 抽象类中的方法可以有方法体,就是能实现方法的具体功能,但是接口中的方法不行。
  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。
  • 接口中不能含有静态代码块以及静态方法(用 static 修饰的方法),而抽象类是可以有静态代码块和静态方法。
  • 一个类只能继承一个抽象类,而一个类却可以实现多个接口。

:JDK 1.8 以后,接口里可以有静态方法和方法体了。

:JDK 1.8 以后,接口允许包含具体实现的方法,该方法称为"默认方法",默认方法使用 default 关键字修饰。

:JDK 1.9 以后,允许将方法定义为 private,使得某些复用的代码不会把方法暴露出去。

接口的声明

接口的声明语法格式如下:

[可见度] interface 接口名称 [extends 其他的接口名] {
        // 声明变量
        // 抽象方法
}

Interface关键字用来声明一个接口。下面是接口声明的一个简单例子。

/* 文件名 : NameOfInterface.java */
import java.lang.*;
//引入包
 
public interface NameOfInterface
{
   //任何类型 final, static 字段
   //抽象方法
}

接口有以下特性:

  • 接口是隐式抽象的,当声明一个接口的时候,不必使用abstract关键字。
  • 接口中每一个方法也是隐式抽象的,声明时同样不需要abstract关键字。
  • 接口中的方法都是公有的。

例子:

/* 文件名 : Animal.java */
interface Animal {
   public void eat();
   public void travel();
}

接口的实现

当类实现接口的时候,类要实现接口中所有的方法。否则,类必须声明为抽象的类。

类使用implements关键字实现接口。在类声明中,Implements关键字放在class声明后面。

实现一个接口的语法:

...implements 接口名称[, 其他接口名称, 其他接口名称..., ...] ...

一个例子:

/* 文件名 : MammalInt.java */
public class MammalInt implements Animal{
 
   public void eat(){
      System.out.println("Mammal eats");
   }
 
   public void travel(){
      System.out.println("Mammal travels");
   } 
 
   public int noOfLegs(){
      return 0;
   }
 
   public static void main(String args[]){
      MammalInt m = new MammalInt();
      m.eat();
      m.travel();
   }
}

子类可以同时继承父类和实现接口

Java中,子类可以同时继承某个父类并实现一个或多个接口。Java允许单继承(每个类只能有一个直接父类),但支持多实现(一个类可以实现多个接口)。这为类设计提供了灵活性。

一个例子:

// 定义一个父类
class ParentClass {
    public void parentMethod() {
        System.out.println("This is a method from the parent class.");
    }
}

// 定义一个接口
interface MyInterface {
    void interfaceMethod();
}

// 定义一个子类,继承父类并实现接口
class ChildClass extends ParentClass implements MyInterface {
    // 实现接口中的方法
    @Override
    public void interfaceMethod() {
        System.out.println("This is a method from the interface.");
    }

    // 子类自己的方法
    public void childMethod() {
        System.out.println("This is a method from the child class.");
    }
}

// 主类
public class Main {
    public static void main(String[] args) {
        // 创建子类对象
        ChildClass child = new ChildClass();

        // 调用父类的方法
        child.parentMethod();

        // 调用接口的方法
        child.interfaceMethod();

        // 调用子类自己的方法
        child.childMethod();
    }
}

输出结果:

This is a method from the parent class.
This is a method from the interface.
This is a method from the child class.

在本例中;

继承父类ChildClass 继承了 ParentClass,因此 ChildClass 可以使用 ParentClass 中定义的方法。

实现接口ChildClass 实现了 MyInterface 接口,因此需要提供 interfaceMethod() 的实现。

多重行为:通过这种方式,ChildClass 同时具有父类的行为和接口规定的行为,提供了灵活性。

本章小结

我们以生物学中的一个特殊动物鸭嘴兽为例,我们都知道在生物学上,鸭嘴兽是很特殊的一种动物,它是哺乳动物同时又具有爬行动物的特点,比如卵生;这种情况下单一地将鸭嘴兽作为哺乳动物的子类缺点就很明显了;那再Java中如何处理类似第情况呢?

我们使用以下这段代码示例进行说明:

  1. 定义哺乳动物的父类Mammal
public class Mammal {
    public void breathe() {
        System.out.println("Breathing like a mammal...");
    }

    public void feedMilk() {
        System.out.println("Feeding milk...");
    }
}
  1. 定义爬行动物特征的接口 ReptileFeatures
public interface ReptileFeatures {
    void layEggs();
}
  1. 定义鸭嘴兽子类 Platypus
public class Platypus extends Mammal implements ReptileFeatures { // 继承Mammal父类同时实现爬行动物特征的接口
    @Override
    public void layEggs() {  // 重写/实现接口中的方法
        System.out.println("Laying eggs like a reptile...");
    }

    public void swim() {  // 派生出自己特有的方法
        System.out.println("Swimming...");
    }

    public static void main(String[] args) {
        Platypus platypus = new Platypus();  // 实例化
        
        // 调用继承自Mammal类的方法
        platypus.breathe();  // 通过继承得到的来自父类的方法
        platypus.feedMilk();  // 通过继承得到的来自父类的方法
        
        // 调用实现自ReptileFeatures接口的方法
        platypus.layEggs();    // 自己实现的来自接口的方法
        
        // 调用Platypus类自己的方法
        platypus.swim();  // 自己类本身的方法
    }
}

这个示例执行的结果:

Breathing like a mammal...
Feeding milk...
Laying eggs like a reptile...
Swimming...

继承父类Platypus 类继承了 Mammal 类,因此它可以调用 Mammal 类中的 breathefeedMilk 方法。

实现接口Platypus 类实现了 ReptileFeatures 接口,因此它必须实现 layEggs 方法。

多重行为:通过这种方式,Platypus 类既有哺乳动物的特性(继承自 Mammal),也有爬行动物的特性(实现 ReptileFeatures 接口中的 layEggs 方法)。

通过这个例子,我们可以看到Java中继承和接口的强大之处。继承允许类重用父类的代码,接口允许类定义必须实现的行为。结合这两者,可以创建既具有共同特性又有特定行为的类,这在软件开发中非常有用。鸭嘴兽这个例子很好地说明了如何在Java中使用继承和接口来模拟现实世界中复杂的继承关系和行为。

习题