
继承是面向对象编程中的一个核心特性,它允许我们通过创建一个新类(称为子类),来扩展一个已有的类(称为超类)。子类会自动获得超类中定义的所有字段(成员变量)和方法(成员函数),从而实现代码的复用和功能的扩展。 通过继承,我们可以在类之间建立“层次结构”或“家族关系”,例如:动物类是所有动物的通用模板,而狗类和猫类可以继承动物类,分别添加属于自己的特有属性和行为。继承不仅让代码更加简洁和易于维护,还使得程序结构更加清晰,便于理解和扩展。
在现实世界中,你可以找到许多对象,它们是其他更一般对象的特化版本。例如,“昆虫”这个词描述了一种具有众多特征的非常一般的生物类型。 因为“蚱蜢”和“熊蜂”是昆虫,它们具有昆虫的所有一般特征。此外,它们还有自己的特殊特征。例如,蚱蜢有跳跃能力,熊蜂有螫针。蚱蜢和熊蜂是昆虫的特化版本。
当一个对象是另一个对象的特化版本时,它们之间存在“是一个”关系。例如,蚱蜢是一个昆虫。这里有一些“是一个”关系的其他例子:
当“是一个”关系存在于对象之间时,这意味着特化对象具有一般对象的所有特征,再加上使其特殊的附加特征。在面向对象编程中,继承用于在类之间创建“是一个”关系。这允许你通过创建另一个类来扩展类的功能,该类是它的特化版本。
继承涉及超类和子类。超类是一般类,子类是特化类。你可以将子类视为超类的扩展版本。子类从超类继承字段和方法,而不必重写它们中的任何一个。此外,可以向子类添加新字段和方法,这就是使其成为超类特化版本的原因。
注意:为了避免术语混淆,应该提到超类也称为基类,子类也称为派生类。任一组术语都是正确的。为了保持一致性,我们使用超类和子类这两个术语。
让我们看一个如何使用继承的例子。在自然界中,动物是一个广泛的概念,包含了各种各样的生物。所有的动物都有一些共同的特征,比如需要进食、需要休息、能够发出声音等。同时,不同种类的动物又有各自独特的特征和行为。
让我们首先创建一个Animal类,它设计用于表示所有动物的共同特征:
|public class Animal { private String name; // 动物名称 private int age; // 动物年龄 /** * setName方法设置name字段 * @param n 要存储在name中的值 */ public void setName(String n) { name = n; } /** * getName方法返回动物名称 * @return 存储在name字段中的值 */ public String getName() { return name; } /** * setAge方法设置age字段 * @param a 要存储在age中的值 */ public void setAge(int a) { age = a; } /** * getAge方法返回动物年龄 * @return 存储在age字段中的值 */ public int getAge() { return age; } /** * eat方法表示动物进食的行为 */ public void eat() { System.out.println(name + "正在吃东西"); } /** * sleep方法表示动物睡觉的行为 */ public void sleep() { System.out.println(name + "正在睡觉"); } /** * makeSound方法表示动物发出声音的行为 */ public void makeSound() { System.out.println(name + "发出声音"); } }
让我们看一个演示这个类的程序:
|public class AnimalDemo { public static void main(String[] args) { // 创建一个Animal对象 Animal animal = new Animal(); // 设置动物的名称和年龄 animal.setName("小黄"); animal.setAge(2); // 调用动物的行为方法 animal.eat(); animal.sleep
运行这个程序会输出动物的基本行为信息。
Animal类表示所有动物的一般特征。但是,存在许多不同类型的动物,如狗、猫、鸟、鱼等。因为每种动物都有自己独特的特征和行为,我们可以创建子类来处理每一种。
例如,我们可以创建一个Dog类,它将是Animal类的子类。这个类有品种(breed)、是否训练过(isTrained)等字段。
|public class Dog extends Animal { private String breed; // 狗的品种 private boolean isTrained; // 是否训练过 /** * 构造函数设置狗的品种和训练状态 * @param b 狗的品种 * @param trained 是否训练过 */ public Dog(String b, boolean trained) { // 设置breed和isTrained字段 breed = b;
这里注意看Dog类的头,头使用extends关键字,这表示这个类扩展另一个类(超类)。超类的名称列在extends单词之后。所以,这一行表示Dog是被声明的类的名称,Animal是它扩展的超类的名称。
因为Dog类扩展了Animal类,它继承了Animal类的所有公共成员。以下是Dog类成员的列表:
字段:
方法:
注意,Animal类的name和age字段没有列在Dog类的成员中。这是因为这些字段是私有的。 超类的私有成员不能被子类访问,所以从技术上讲,它们不被继承。当创建子类对象时,超类的私有成员存在于内存中,但只有超类中的方法可以访问它们。它们对超类来说确实是私有的。
super关键字引用对象的超类。你可以使用super关键字调用超类构造函数。
在之前你看到了说明超类的默认构造函数或无参构造函数如何在子类构造函数执行之前自动调用的例子。但是,如果超类没有默认构造函数或无参构造函数怎么办?或者,如果超类有多个重载构造函数,你想要确保调用特定的一个怎么办?在
这两种情况下,你都可以使用super关键字显式调用超类构造函数。
让我们看一个例子。Animal类有一个无参构造函数和一个接受String和int参数的构造函数:
|public class Animal { private String name; // 动物名称 private int age; // 动物年龄 /** * 构造函数#1:无参构造函数 */ public Animal() { System.out.println("这是Animal类的无参构造函数。"); name = "未知"; age = 0; } /** * 构造函数#2:接受名称和年龄的构造函数 */
Dog类扩展Animal。这个类的构造函数使用super关键字调用超类构造函数并向其传递参数:
|public class Dog extends Animal { private String breed; // 狗的品种 /** * 构造函数 */ public Dog() { super("旺财", 1); // 调用超类构造函数,传递名称和年龄 System.out.println("这是Dog类的构造函数。"); breed = "未知品种"; } }
Dog构造函数中的super("旺财", 1)语句调用超类构造函数并向其传递参数"旺财"和1。
关于调用超类构造函数,你应该记住以下三个指导原则:
在 Java 中,方法重写(Overriding)是指子类中定义了一个与其超类(父类)中具有相同方法名、参数列表和返回类型的方法。此时,子类的方法会覆盖(重写)超类中对应的方法。当通过子类对象调用该方法时,实际执行的是子类中重写后的版本,而不是超类中的原始实现。
方法重写通常发生在以下场景:子类继承了超类的方法,但该方法的实现对于子类来说并不完全适用。由于子类通常比超类更具体、更专门化,因此子类可能需要根据自身的需求,提供一个更合适的实现来替换超类的方法。通过重写,子类可以扩展或修改继承自超类的行为,从而实现多态性(polymorphism)。

需要注意的是,重写方法时,子类方法的访问修饰符不能比超类方法更严格,且方法签名(包括方法名、参数类型和顺序)必须完全一致。此外,重写方法时可以使用 @Override 注解,这样编译器会检查是否正确地进行了重写,避免因方法签名不一致而导致的错误。
举个例子,如果超类 Animal 有一个 makeSound() 方法,Dog 类继承自 Animal 并重写了 makeSound() 方法,那么通过 Dog 对象调用 makeSound() 时,实际执行的是 Dog 类中的实现,而不是 Animal 类中的实现。这就是方法重写的作用和意义。
前面介绍的Animal类。这个类有一个makeSound方法,它表示动物发出声音的一般行为。但是,不同种类的动物发出的声音是不同的。例如,狗会汪汪叫,猫会喵喵叫,鸟会啾啾叫。
为了满足这个需求,我们可以设计一个新类Dog,它扩展Animal类并有其自己的makeSound方法的特化版本,让狗发出独特的汪汪叫声。
|public class Dog extends Animal { private String breed; // 狗的品种 private String soundType; // 声音类型 /** * 构造函数设置狗的品种和声音类型 * @param b 狗的品种 * @param sound 声音类型 */ public Dog(String b, String sound) { breed = b; soundType = sound; }
注意@Override注解出现在makeSound方法定义之前。这个注解告诉Java编译器makeSound方法旨在重写超类中的方法。
重载方法和重写方法之间有区别。重载是当一个方法与一个或多个其他方法具有相同名称但参数列表不同时。虽然重载方法具有相同名称,但它们具有不同签名。当方法重写另一个方法时,它们都具有相同签名。
重载和重写都可以在继承关系中发生。你已经学习过,重载方法可以出现在同一类中。此外,子类中的方法可以重载超类中的方法。 如果类A是超类,类B是子类,类B中的方法可以重载类A中的方法,或类B中的另一个方法。另一方面,重写只能在继承关系中发生。如果类A是超类,类B是子类,类B中的方法可以重写类A中的方法。但是,方法不能重写同一类中的另一个方法。
当方法用final修饰符声明时,它不能在子类中被重写。以下方法头是使用final修饰符的例子:
|public final void message()
如果子类尝试重写final方法,编译器会生成错误。这种技术可用于确保子类使用特定的超类方法,而不是它的修改版本。
类的受保护成员可以被子类中的方法和与类在同一包中的方法访问。
到目前为止,你在类中使用了两个访问说明符:private和public。Java提供了第三个访问说明符protected。类的受保护成员可以被同一类的方法或子类的方法直接访问。
此外,受保护成员可以被与受保护成员类在同一包中的任何类的方法访问。受保护成员不是完全私有的,因为它可以被类外部的某些方法访问。
受保护成员也不是完全公共的,因为对它们的访问仅限于同一类、子类和与成员类在同一包中的类中的方法。受保护成员的访问介于private和public之间。
让我们看一个具有受保护成员的类。Animal2类是前面介绍的Animal类的修改版本。在这个类中,name和age字段被设为protected而不是private:
|public class Animal2 { protected String name; // 动物名称 protected int age; // 动物年龄 /** * setName方法设置name字段 * @param n 要存储在name中的值 */ public void setName(String n) { name = n; } /** * getName方法返回动物名称 * @return 存储在name字段中的值 */
因为在第3行和第4行name和age字段被声明为protected,任何继承自这个类的类都可以直接访问它们。Dog2类是一个例子。这个类是本章前面介绍的Dog类的修改版本。这个类有一个新方法updateInfo,它直接访问超类的name和age字段。
如果你不为类成员提供访问说明符,类成员默认获得包访问。这意味着同一包中的任何方法都可以访问该成员。这里是一个例子:
|public class Animal { String name; // 没有访问说明符,默认包访问 int age; // 没有访问说明符,默认包访问 String species; // 没有访问说明符,默认包访问 // 方法定义跟随... }
在这个类中,name、age和species字段没有给出访问说明符,所以编译器授予它们包访问。与Animal类在同一包中的任何方法都可以直接访问这些成员。
受保护访问和包访问之间有细微差别。受保护成员可以被同一包中的方法或子类中的方法访问。即使子类在不同的包中,这也是正确的。但是,具有包访问的成员不能被不同包中的子类访问。
在面向对象编程中,一个类不仅可以作为其他类的子类,还可以作为超类被进一步继承。这意味着继承关系可以形成一条“链”,即所谓的继承链。 具体来说,A类继承自B类,B类又继承自C类,这样A类就间接地继承了C类的所有非私有成员。继承链可以有多层,理论上可以一直延伸下去。 通过继承链,最底层的子类能够访问和复用所有祖先类(即继承链上所有超类)中被允许访问的属性和方法。这种机制有助于代码的复用和结构的层次化设计,但过长的继承链也可能导致代码难以维护和理解,因此在设计时需要权衡。
让我们看一个这样的继承链的例子。考虑Dog类,它从Animal类继承。该类表示狗这种动物的特征,狗是动物的一种特化。
|public class Dog extends Animal { private String breed; // 狗的品种 /** * 构造函数设置狗的品种 * @param b 狗的品种 */ public Dog(String b) { breed = b; } /** * makeSound方法根据狗的品种发出不同的声音 * 这个方法重写超类方法 */ @Override public void makeSound
Dog构造函数接受一个String参数,这是狗的品种。这个值存储在breed字段中。makeSound方法重写同名超类方法,让狗发出汪汪叫声。
假设我们希望用另一个更特化的类来扩展这个类。例如,Puppy类表示小狗,它是狗的一种特化。小狗有年龄(ageInMonths)、是否断奶(isWeaned)等字段。
|public class Puppy extends Dog { private int ageInMonths; // 年龄(以月为单位) private boolean isWeaned; // 是否断奶 /** * 构造函数设置品种、年龄和断奶状态 * @param breed 狗的品种 * @param age 年龄(月) * @param weaned 是否断奶 */ public Puppy(String breed, int age, boolean
Puppy类继承Dog类的成员,包括Dog从Animal继承的成员。
让我们看一个演示这个类的程序:
|public class PuppyDemo { public static void main(String[] args) { // 创建Puppy对象 Puppy puppy = new Puppy("金毛", 3, true); // 设置小狗的名称 puppy.setName("小黄"); // 调用继承自Animal类的方法 puppy.eat(); puppy.sleep
运行这个程序会输出:
|小黄正在吃东西 小黄正在睡觉 小黄正在汪汪叫 小黄正在玩耍 品种:金毛 年龄:3个月 是否断奶:是
Java API有一个名为Object的类,所有其他类都直接或间接地从它继承。
Java中的每个类,包括API中的类和你创建的类,都直接或间接地从名为Object的类继承,该类是java.lang包的一部分。
当类不使用extends关键字从另一个类继承时,Java会自动从Object类扩展它。例如,看以下类声明:
|public class MyClass { // 成员声明... }
这个类没有显式扩展任何其他类,所以Java将其视为按以下方式编写:
|public class MyClass extends Object { // 成员声明... }
最终,每个类都扩展Object类。
因为每个类都直接或间接扩展Object类,每个类都继承Object类的成员。其中最有用的两个是toString和equals方法。在第8章中,你学习了每个类都有toString和equals方法,现在你知道为什么了!这是因为这些方法是从Object类继承的。
在Object类中,toString方法返回对包含对象类名的String的引用,后跟@符号,后跟对象的哈希码,这是一个十六进制数字。equals方法接受对对象作为其参数的引用。如果参数引用调用对象,它返回true。
让我们看一个演示这些方法的程序:
|public class ObjectMethods { public static void main(String[] args) { // 创建两个对象 Dog dog1 = new Dog("金毛", true); Dog dog2 = new Dog("金毛", true); // 设置名称 dog1.setName("旺财"); dog2.
运行这个程序会输出:
|Dog@16f0472 Dog@18d107f 它们不相同。
如果你希望为给定类更改这些方法中任一个的行为,你必须在类中重写它们。
在Java中,父类(超类)的引用变量不仅可以引用自身类型的对象,还可以引用其所有子类(派生类)的对象。这是因为子类对象本质上也是父类对象的一种,因此可以赋值给父类类型的变量。 例如,如果有一个Animal类和一个继承自它的Dog类,那么Animal类型的变量既可以指向Animal对象,也可以指向Dog对象。这种机制为多态提供了基础,使得同一个父类引用变量在不同情况下可以代表不同的子类对象,从而实现灵活的程序设计。

看以下声明名为animal的引用变量的语句:
|Animal animal;
这个语句告诉我们animal变量的数据类型是Animal。因此,我们可以使用animal变量来引用Animal对象,如下面的语句所示:
|animal = new Animal();
Animal类也用作Dog类的超类。由于超类和子类之间的"是一个"关系,Dog类的对象不仅仅是Dog对象。它也是Animal对象。(狗是一种动物。)由于这种关系,我们可以使用Animal变量来引用Dog对象。例如,看以下语句:
|Animal animal = new Dog("金毛", true);
这个语句将animal声明为Animal变量。它创建一个Dog对象并将对象的地址存储在animal变量中。这个语句完全合法,不会导致错误消息,因为Dog对象也是Animal对象。
这是多态的一个例子。术语多态意味着采取多种形式的能力。在Java中,引用变量是多态的,因为它可以引用与其自身类型不同的对象,只要这些类型是其类型的子类。以下所有声明都是合法的,因为Dog、Cat和Bird类都从Animal继承:
|Animal animal1 = new Dog("金毛", true); Animal animal2 = new Cat("波斯猫", false); Animal animal3 = new Bird("鹦鹉", true);
虽然Animal变量可以引用扩展Animal的任何类的对象,但变量可以用这些对象做什么是有限制的。 回忆Animal类有几个方法:setName、getName、setAge、getAge、eat、sleep和makeSound。所以,Animal变量只能用于调用这些方法,无论变量引用的对象类型如何。
例如,看以下代码:
|Animal animal = new Dog("金毛", true); animal.setName("旺财"); // 这有效 animal.eat(); // 这有效 animal.sleep(); // 这有效 animal.bark(); // 错误!不会工作
在这段代码中,animal被声明为Animal变量并被分配Dog对象的地址。Animal类只有setName、getName、eat、sleep和makeSound等方法,所以这些是animal变量知道如何执行的唯一方法。 这段代码中的最后一条语句是对bark方法的调用,该方法在Dog类中定义。因为animal变量只知道Animal类中的方法,它不能执行这个方法。
当我们使用超类(父类)类型的变量去引用一个子类(子类型)对象时,会出现一个值得注意的问题:如果子类对超类的方法进行了重写(Override),那么当我们通过这个超类变量去调用该方法时,究竟会执行超类中的版本,还是子类中重写后的版本呢?也就是说,方法调用时到底是根据变量的声明类型(超类),还是根据实际引用的对象类型(子类)来决定调用哪个方法?这个问题涉及到Java的多态机制和方法的动态绑定。
回忆我们之前将方法调用与正确方法定义匹配的过程称为绑定。当变量包含多态引用时,Java执行动态绑定或后期绑定。这意味着Java虚拟机在运行时确定调用哪个方法,这取决于变量引用的对象类型。所以,是对象的类型决定调用哪个方法,而不是变量的类型。
让我们看一个演示多态行为的程序:
|public class Polymorphic { public static void main(String[] args) { // 创建Animal引用数组 Animal[] animals = new Animal[3]; // 第一个是普通动物 animals[0] = new Animal(); animals[0].setName("小动物"); // 第二个是狗
运行这个程序会输出:
|动物1:小动物 小动物正在吃东西 小动物正在睡觉 小动物发出声音 动物2:旺财 旺财正在吃东西 旺财正在睡觉 旺财正在汪汪叫 动物3:小黄 小黄正在吃东西 小黄正在睡觉 小黄正在汪汪叫
Java中有一个名为instanceof的操作符,你可以用它来确定对象是否是特定类的实例。以下是使用instanceof操作符的表达式的一般形式:
|refVar instanceof ClassName
在一般形式中,refVar是引用变量,ClassName是类名。这是布尔表达式的形式,如果refVar引用的对象是ClassName的实例,它将返回true。否则,表达式返回false。
instanceof操作符理解类从另一个类继承时存在的"是一个"关系。例如,看以下代码:
|Dog dog = new Dog("金毛", true); if (dog instanceof Animal) System.out.println("是的,dog是一个Animal。"); else System.out.println("不,dog不是一个Animal。");
即使dog引用的对象是Dog对象,这段代码也会显示"是的,dog是一个Animal。"instanceof操作符返回true,因为Dog是Animal的子类。
抽象方法是指在超类(通常是抽象类)中声明但没有具体实现的方法。它们只包含方法签名(即方法名、参数列表和返回类型),没有方法体。抽象方法的声明以分号结尾,并且必须使用abstract关键字进行修饰。例如:
以下是抽象方法头的一般格式:
|AccessSpecifier abstract ReturnType MethodName(ParameterList);
注意abstract关键字出现在头中,头以分号结束。方法没有体。以下是抽象方法头的例子:
|public abstract void setValue(int value);
当抽象方法出现在类中时,该方法必须在子类中被重写。如果子类未能重写该方法,将导致错误。抽象方法用于确保子类实现该方法。
当一个类中包含抽象方法时,这个类必须被声明为抽象类。抽象类不能被直接实例化,也就是说,你不能通过new关键字来创建抽象类的对象。抽象类的主要作用是作为其他类的父类(超类),为子类提供一个通用的模板或基础。抽象类中可以包含抽象方法(没有方法体、需要子类实现的方法)和具体方法(有方法体、子类可以直接继承的方法)。通过定义抽象类,可以将所有子类共有的属性和行为集中在一起,而将需要各个子类自行实现的行为声明为抽象方法,从而强制子类必须实现这些方法。这种设计有助于代码的复用和规范化,确保所有继承自抽象类的子类都具备某些特定的功能或特性。
让我们看一个抽象类的例子。Animal类保存所有动物的通用数据,但不保存特定种类动物所需的所有数据:
|public abstract class Animal { private String name; // 动物名称 private int age; // 动物年龄 /** * 构造函数设置动物的名称和年龄 * @param n 动物名称 * @param a 动物年龄 */ public Animal(String n, int a) { name = n; age = a;
Animal类包含用于存储动物名称和年龄的字段。它还有一个构造函数、eat方法、sleep方法和一个名为getHabitat的抽象方法。
这个抽象方法必须在继承自Animal类的类中被重写。这个方法背后的想法是它返回动物生活的栖息地。它被设为抽象的,因为不同种类的动物生活在不同的栖息地,这个类旨在成为表示特定种类动物的其他类的基类。
让我们看Dog类的例子,它扩展Animal类:
|public class Dog extends Animal { // 狗的特定属性 private String breed; // 狗的品种 private boolean isTrained; // 是否训练过 /** * 构造函数设置狗的姓名、年龄、品种和训练状态 * @param n 狗的名称 * @param a 狗的年龄 * @param b 狗的品种 * @param trained 是否训练过 */ public Dog(String
让我们看一个演示这个类的程序:
|public class DogDemo { public static void main(String[] args) { // 创建一个Dog对象 Dog dog = new Dog("旺财", 3, "金毛", true); // 显示狗的数据 System.out.println(dog); // 调用狗的行为方法 dog.eat(); dog.sleep
运行这个程序会输出:
|名称:旺财 年龄:3 品种:金毛 是否训练过:是 栖息地:家庭和人类居住区 旺财正在吃东西 旺财正在睡觉 旺财正在汪汪叫 栖息地:家庭和人类居住区
接口(Interface)是Java中一种特殊的引用类型,用于为类规定一组必须实现的方法,也就是为类指定行为规范。可以把接口理解为一种“协议”或“契约”,它定义了类应该具备哪些功能,但不关心这些功能是如何实现的。
在最简单的形式中,接口就像只包含抽象方法(没有方法体的方法)的类。接口中的方法默认都是public abstract,即公开且抽象的。接口不能被实例化,不能直接创建接口类型的对象。相反,接口需要被其他类“实现”(implement)。当一个类实现某个接口时,必须提供接口中所有方法的具体实现,否则该类也必须声明为抽象类。
接口的语法和类很相似,但有以下不同点:
interface关键字,而不是class关键字。public static final)和抽象方法(默认public abstract),但不能包含普通的成员变量和带有方法体的普通方法(Java 8及以后可以有默认方法和静态方法,后面会介绍)。让我们看一个名为Runnable的接口的例子:
|public interface Runnable { void run(); }
注意第3行run方法的头没有访问说明符。这是因为接口中的所有方法都是隐式public的。你可以选择在方法头中写public,但大多数程序员不写,因为所有接口方法都必须是public的。
实现Runnable接口的任何类都必须提供run方法的实现(具有接口指定的确切签名和相同返回类型)。Dog类是例子:
|public class Dog extends Animal implements Runnable { private String breed; // 狗的品种 // 构造函数 public Dog(String n, int a, String b) { super(n, a); breed = b; } // run方法实现Runnable接口 public void run() { System.out.println(
当你希望类实现接口时,你在类头中使用implements关键字。注意Dog类头以implements Runnable子句结尾。因为Dog类实现Runnable接口,它必须提供接口run方法的实现。
当一个类实现接口时,实际上是在与编译器“签订契约”:类必须提供接口中声明的所有方法的具体实现。接口规定了类应该具备哪些功能,但不关心这些功能是如何实现的。也就是说,接口只负责定义“做什么”,而具体“怎么做”由实现类决定。如果类没有实现接口中的所有方法,编译器会报错,除非该类被声明为抽象类。因此,接口在Java中起到了行为规范的作用,确保所有实现该接口的类都遵循相同的方法签名和功能要求。这种机制有助于实现代码的解耦和多态,使得不同的类可以通过相同的接口进行交互,提升了程序的灵活性和可扩展性。
你可能想知道为什么我们需要抽象类和接口,因为它们彼此如此相似。原因是一个类只能扩展一个超类,但Java允许类实现多个接口。当类实现多个接口时,它必须提供所有接口指定的方法。
要在类定义中指定多个接口,只需在implements关键字后列出接口名称,用逗号分隔。以下是实现多个接口的类第一行的例子:
|public class MyClass implements Interface1, Interface2, Interface3
这个类实现三个接口:Interface1、Interface2和Interface3。
从Java 8开始,接口可能有默认方法。默认方法是有体的接口方法。让我们看Runnable接口的另一个版本,其中run方法是默认方法:
|public interface Runnable { default void run() { System.out.println("这是默认的run方法,表示正在移动。"); } }
注意第3行方法头以default关键字开始。这对于有体的接口方法是必需的。当类实现具有默认方法的接口时,类可以重写默认方法,但不是必需的。
9. 继承基础练习
创建一个Animal类作为父类,然后创建Dog类作为子类,演示继承关系。
|// 父类 public class Animal { protected String name; protected int age; public Animal(String name, int age) { this.name = name; this.age = age; } public void eat() { System.out.println(name + "正在吃东西"
10. 方法重写练习
为Animal类创建一个Dog子类,重写父类的eat方法。
|// 父类 public class Animal { protected String name; public Animal(String name) { this.name = name; } public void eat() { System.out.println(name + "正在吃东西"); } public void makeSound() { System.out.println
11. super关键字练习
创建一个Animal父类和Dog子类,演示super关键字的使用。
|// 父类 public class Animal { protected String name; protected int age; public Animal(String name, int age) { this.name = name; this.age = age; System.out.println("Animal构造函数被调用"); } public void eat() {
输出结果:
|旺财正在吃东西 旺财正在睡觉 旺财正在汪汪叫 姓名: 旺财, 年龄: 3 品种: 金毛
说明:
Dog类通过extends Animal继承父类super关键字调用父类的构造函数和方法输出结果:
|Animal对象: 动物正在吃东西 动物发出声音 Dog对象: 旺财正在吃狗粮 旺财正在汪汪叫
说明:
@Override注解表示重写父类方法输出结果:
|Animal构造函数被调用 Dog构造函数被调用 调用方法: 旺财正在吃东西 名称: 旺财, 年龄: 3 品种: 金毛 旺财正在汪汪叫
说明:
super(name, age)调用父类的构造函数,必须是子类构造函数的第一条语句super.displayInfo()调用父类的方法