1. 什么是继承?
继承是一个机制,它允许在已有类的基础上创建新类,并“继承”其属性与方法。新类称为子类(或派生类),原有类称为父类(或基类)。
这就好比你在旧配方的基础上做一个新蛋糕:把好的都保留下来,再加上几个新配料——瞧,你已经有了“奶奶秘方蛋糕(加入神秘配料)”。
- 父类——包含通用的属性与行为。
- 子类——继承这些内容,并且可以添加自己的东西或改变行为。
比如,“猫是动物”:所有的猫都是动物,但不是所有动物都是猫。Animal 可以是基类,而 Cat 是它的子类;Dog 也同理。
再比如,“圆是图形”:每个圆都是图形,但不是每个图形都是圆。Figure 可以是父类,而 Circle 和 Triangle 是它的子类。在 Java 中,这些关系通过关键字 extends 来实现。
为什么需要继承?
价值百万的问题:为什么不能直接把代码从一个类复制到另一个类?为什么还要发明继承?
- 代码复用:把所有共性提取到基类。不必重复字段和方法——它们会自动出现在子类中。比如,在一个面向大学的程序里,基类可以是“人”,至少有姓名和生日,而“学生”和“老师”是具有各自属性的子类。
- 扩展与特化:可以只在需要的地方添加新属性/方法,或修改已有行为。所有动物都会发出声音,但猫会喵,狗会汪。同时你还可以给猫添加“爬树”的能力(而基类中没有)。
- 结构化的层次:代码更有逻辑也更易懂。比如有类 Animal、Dog、Cat,一眼就能看出 Dog 和 Cat 是 Animal 的特例。
示例。如果 Java 没有继承,你就得在每个动物类里复制诸如 eat()、sleep() 的方法以及字段 name。如果明天需要再加一个 breathe() 方法,你就得改动几十个类!有了继承——只需把“会呼吸”的能力加到基类里即可。
2. 继承的语法:extends
如何在 Java 中声明继承?很简单:使用关键字 extends。
class ParentClass {
// 字段和方法
}
class Subclass extends ParentClass {
// 额外的字段和方法
}
示例 1:动物
// 父类
class Animal {
String name;
void eat() {
System.out.println(name + " 在吃。");
}
}
// 子类
class Cat extends Animal {
void meow() {
System.out.println(name + " 说:喵!");
}
}
这里发生了什么?
- Cat 类继承了 Animal 类的所有字段和方法。
- 这意味着 Cat 已经拥有字段 name 和方法 eat(),尽管我们没有显式编写它们。
- 此外,Cat 还拥有自己独有的方法 meow()。
用法
public class Main {
public static void main(String[] args) {
Cat kitty = new Cat();
kitty.name = "Barsik";
kitty.eat(); // Barsik 在吃。 (继承而来)
kitty.meow(); // Barsik 说:喵!(自有方法)
}
}
大功告成!我们在避免代码重复的同时,用最少的代码获得了一个新类。
3. 子类究竟继承了什么?
在 Java 中,子类会继承:
- 所有公开(public)与受保护(protected)的字段和方法,它们来自基类。
- 所有包可见(package-private)的字段和方法(前提是子类位于同一包中)。
不会继承:
- 构造器(需要在子类中显式声明)。
- 私有(private)字段和方法(它们只在基类内部可访问)。
- 静态初始化块(它们不被继承,但每个类都可以有自己的静态初始化块)。
- 包可见成员,如果子类位于另一个包。
示意
class Animal {
public String name;
private int age;
public void eat() {
System.out.println("吃");
}
private void secret() {
System.out.println("秘密!");
}
}
class Dog extends Animal {
public void bark() {
System.out.println("汪!");
}
}
- Dog 会继承字段 name 和方法 eat()。
- Dog看不到字段 age 和方法 secret()——它们是私有的。
4. Java 中继承的限制
Java 相当严格,它有自己的规矩:
类只能单继承:每个类只能继承自一个父类。不过,一个类可以有任意多个子类。
class Dog extends Animal { ... } // 可以
class Dog extends Animal, Pet { ... } // 错误!这样不行
接口可以多继承——这是另一个话题,我们稍后再谈。
Object 类是 Java 中所有类的祖先。即使你没有显式写出 extends,你的类也会隐式继承 Object。
5. 实践示例
让我们继续构建我们的“动物学”应用。
步骤 1:基类 Animal
class Animal {
String name;
void eat() {
System.out.println(name + " 在吃。");
}
}
步骤 2:子类 Cat
class Cat extends Animal {
void meow() {
System.out.println(name + " 说:喵!");
}
}
步骤 3:子类 Dog
class Dog extends Animal {
void bark() {
System.out.println(name + " 说:汪!");
}
}
步骤 4:使用这些类
public class Main {
public static void main(String[] args) {
Cat cat = new Cat();
cat.name = "Murka";
cat.eat(); // Murka 在吃。
cat.meow(); // Murka 说:喵!
Dog dog = new Dog();
dog.name = "Sharik";
dog.eat(); // Sharik 在吃。
dog.bark(); // Sharik 说:汪!
}
}
我们得到了什么?
- Cat 和 Dog 都使用来自基类的公共字段 name 与方法 eat()。
- 每个类都添加了自己的独特行为:猫的 meow(),狗的 bark()。
- 如果需要改变动物“吃”的方式,只需修改 Animal 类中的 eat() 方法——更改会自动作用到所有子类。
6. 一些有用的小细节
可视化:类层次
可以把层次结构想象成这样:
. Animal
/ \
Cat Dog
- Animal——基类,包含动物的通用内容。
- Cat 和 Dog——子类,添加各自的独特特性。
关于构造器与继承的几点说明
构造器不被继承。如果你没有在子类中定义构造器,而父类有无参构造器,Java 会自动生成一个无参构造器;如果父类只有带参构造器,那么就需要在子类中通过 super(...) 显式调用。一个简短的示例:
class Animal {
String name;
Animal(String name) {
this.name = name;
}
}
class Cat extends Animal {
Cat(String name) {
super(name); // 调用父类构造器
}
}
7. 使用继承时的常见错误
错误 1:试图同时继承多个类。
在 Java 中不能写 class Cat extends Animal, Pet。只能有一个父类!
错误 2:以为私有字段和方法在子类可见。
私有成员不被继承(更准确地说它们存在,但子类无法访问)。如果需要允许子类访问,请使用 protected。
错误 3:忘记调用父类构造器。
如果基类没有无参构造器,而你也没有显式调用 super(...),就会出现编译错误。
错误 4:为“省代码”而使用继承,而不是基于语义。
如果类之间不存在“是一个”(is-a)关系,最好不要使用继承。比如,“人”不应当继承自“汽车”,即便它们都有字段 speed。
GO TO FULL VERSION