📋 个人简介
💖 作者简介:大家好,我是W_chuanqi,一个编程爱好者
📙 个人主页:W_chaunqi
😀 支持我:点赞👍+收藏⭐️+留言📝
💬 愿你我共勉:“没有什么比勇气更温文尔雅,没有什么比怯懦更冷酷无情!”✨✨✨

在现实生活中,继承一般指的是子女继承父辈的财产。在程序中,继承描述的是事物之间的所属关系,通过继承可以使多种事物之间形成一种关系体系。例如,猫和狗都属于动物,程序中便可以描述为猫和狗继承自动物,同理,波斯猫和巴厘猫继承猫科,而沙皮狗和斑点狗继承自犬科。

在Java中,类的继承是指在一个现有类的基础上去构建一个新的类,构建出来的新类被称作子类,现有类被称作父类。子类继承父类的属性和方法,使得子类对象(实例)具有父类的特征和行为。
在程序中,如果想声明一个类继承另一个类,需要使用extends关键字,语法格式如下所示。
class 父类{
……
}
class 子类 extends 父类{
……
}
接下来通过一个案例学习子类是如何继承父类。
// 定义Animal类
class Animal {
private String name; // 定义name属性
private int age; // 定义name属性
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
// 定义Dog类继承Animal类
class Dog extends Animal {
// 此处不写任何代码
}
// 定义测试类
public class Example01 {
public static void main(String[] args) {
Dog dog = new Dog(); // 创建一个Dog类的实例对象
dog.setName("牧羊犬"); // 此时访问的方法时父类中的,子类中并没有定义
dog.setAge(3); // 此时访问的方法时父类中的,子类中并没有定义
System.out.println("名称:" + dog.getName() + ",年龄:" + dog.getAge());
}
}
程序运行结果如下图。

上述代码中,第2~22行代码定义了一个Animal类,第25~27行代码定义了一个Dog类,Dog类中并没有定义任何操作,而是通过extends关键字继承了Animal类,成为了Animal类的子类。从运行结果可以看出,子类虽然没有定义任何属性和方法,但是却能调用父类的方法。这就说明,子类在继承父类的时候,会自动继承父类的成员。
除了继承父类的属性和方法,子类也可以定义自己的属性和方法。
// 定义Animal类
class Animal {
private String name; // 定义name属性
private int age; // 定义name属性
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
// 定义Dog类继承Animal类
class Dog extends Animal {
private String color; // 定义name属性
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
// 定义测试类
public class Example02 {
public static void main(String[] args) {
Dog dog = new Dog(); // 创建一个Dog类的实例对象
dog.setName("牧羊犬"); // 此时访问的方法时父类中的,子类中并没有定义
dog.setAge(3); // 此时访问的方法时父类中的,子类中并没有定义
dog.setColor("黑色");
System.out.println("名称:" + dog.getName() + ",年龄:" + dog.getAge() + ",颜色:" + dog.getColor());
}
}
程序运行结果如下图。

在上述代码中,Dog类扩充了Animal类,增加了color属性及getColor()和setColor()方法。此时的Dog类已经存在了3个属性和6个方法。在main()方法中,第41行代码创建并实例化dog对象;第42~44行代码通过dog对象调用Animal类和Dog类的setter方法,设置名称、年龄和颜色;第45行代码通过dog对象调用Animal类和Dog类的getter方法获取名称、年龄和颜色。由运行结果可知,程序成功设置并获取了dog对象的名称、年龄和和颜色。
继承中需要注意的问题。
(1)在Java中,类只支持单继承,不允许多重继承。也就是说一个类只能有一个直接父类,例如下面这种情况是不合法的。
class A{}
class B{}
class C extends A,B{} // C类不可以同时继承A类和B类
(2)多个类可以继承一个父类,例如下面这种情况是允许的。
class A{}
class B extends A{}
class C extends A{} // 类B和类C都可以继承类A
(3)在Java中,多层继承也是可以的,即一个类的父类可以再继承另外的父类。例如,C类继承自B类,而B类又可以继承自A类,这时,C类也可称作A类的子类。例如下面这种情况是允许的。
class A{}
class B extends A{} // 类B继承类A,类B是类A的子类
class C extends B{} // 类C继承类B,类C是类B的子类,同时也是类A的子类
(4)在Java中,子类和父类是一种相对概念,一个类可以是某个类的父类,也可以是另一个类的子类。例如,在第(3)种情况中,B类是A类的子类,同时又是C类的父类。
在继承中,子类不能直接访问父类中的私有成员,子类可以调用父类的非私有方法,但是不能调用父类的私有成员。
在继承关系中,子类会自动继承父类中定义的方法,但有时在子类中需要对继承的方法进行一些修改,即对父类的方法进行重写。在子类中重写的方法需要和父类被重写的方法具有相同的方法名、参数列表以及返回值类型,且在子类重写的方法不能拥有比父类方法更加严格的访问权限。
下面通过一个案例讲解方法的重写。
// 定义Animal类
class Animal {
// 定义动物叫的方法
void shout() {
System.out.println("动物发出叫声");
}
}
// 定义Dog类继承动物类
class Dog extends Animal {
// 重写父类Animal中的shout()方法
void shout() {
System.out.println("汪汪汪……");
}
}
// 定义测试类
public class Example03 {
public static void main(String[] args) {
Dog dog = new Dog(); // 创建Dog类的实例对象
dog.shout(); // 调用dog重写的shout()方法
}
}
程序运行结果如下图。

上述代码中,第2~7行代码定义了一个Animal类,并在Animal类中定义了一个shout()方法。第10~15行代码定义了Dog类继承Animal类,并在类中重写了父类Aniaml的shout()方法。第20~21行代码创建并实例化Dog类对象dog,并通过dog对象调用shout()方法。从运行结果可以看出,dog对象调用的是子类重写的shout()方法,而不是父类的shout()方法。
💣脚下留心:子类重写父类方法时的访问权限
子类重写父类方法时,不能使用比父类中被重写的方法更严格的访问权限。例如,父类中的方法是public权限,子类的方法就不能是private权限。如果子类在重写父类方法时定义的权限缩小,则在编译时将出现错误提示。
// 定义Animal类 class Animal { // 定义动物叫的方法 public void shout() { System.out.println("动物发出叫声"); } } // 定义Dog类继承动物类 class Dog extends Animal { // 重写父类Animal中的shout()方法 private void shout() { System.out.println("汪汪汪……"); } } // 定义测试类 public class Example04 { public static void main(String[] args) { Dog dog = new Dog(); // 创建Dog类的实例对象 dog.shout(); // 调用dog重写的shout()方法 } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
在上述代码中,第4行代码在Animal类中定义了一个shout()方法并将访问权限定义为public,第10~15行代码定义了一个Dog类并继承Animal类,第11行代码在声明shout()方法时,将shout()方法的访问权限定义为private。由运行结果可知,编译文件会报错,这是因为子类重写父类方法时,不能使用比父类中被重写的方法更严格的访问权限。
当子类重写父类的方法后,子类对象将无法访问父类被重写的方法,为了解决这个问题,Java提供了super关键字,super关键字可以在子类中调用父类的普通属性、方法以及构造方法。
接下来详细讲解super关键字的具体用法。
(1)使用super关键字访问父类的成员变量和成员方法,具体格式如下:
super.成员变量
super.成员方法(参数1,参数2…)
接下来通过一个案例学习super关键字访问父类的成员变量和成员方法。
// 定义Animal类
class Animal {
String name = "牧羊犬";
// 定义动物叫的方法
void shout() {
System.out.println("动物发出叫声");
}
}
// 定义Dog类继承动物类
class Dog extends Animal {
// 重写父类Animal中的shout()方法,扩大了访问权限
public void shout() {
super.shout(); // 调用父类中的shout()方法
System.out.println("汪汪汪……");
}
public void printName() {
System.out.println("名字:" + super.name); // 调用父类中的name属性
}
}
// 定义测试类
public class Example05 {
public static void main(String[] args) {
Dog dog = new Dog(); // 创建Dog类的实例对象
dog.shout(); // 调用dog重写的shout()方法
dog.printName(); // 调用Dog类中的printName()方法
}
}
程序运行结果如下图。

上述代码中,第2~9行代码定义了一个Animal类,并在Animal类中定义了name属性和shout()方法。第12~22行代码定义了Dog类并继承了Animal类。在Dog类的shout()方法中使用“super.shout()”调用了父类被重写的shout()方法。在printName()方法中使用“super.name”访问父类的成员变量name。从运行结果中可以看出,子类通过super关键字可以成功地访问父类成员变量和成员方法。
(2)使用super关键字访问父类中指定的构造方法,具体格式如下:
super(参数1,参数2…)
接下来就通过一个案例学习如何使用super关键字调用父类的构造方法。
// 定义Animal类
class Animal {
private String name;
private int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String info() {
return "名称:" + this.getName() + ",年龄: " + this.getAge();
}
}
// 定义Dog类继承动物类
class Dog extends Animal {
private String color;
public Dog(String name, int age, String color) {
super(name, age);
this.setColor(color);
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
} // 重写父类的info()方法
public String info() {
return super.info() + ",颜色:" + this.getColor(); // 扩充父类中的方法
}
}
// 定义测试类
public class Example06 {
public static void main(String[] args) {
Dog dog = new Dog("牧羊犬", 3, "黑色"); // 创建Dog类的实例对象
System.out.println(dog.info());
}
}
程序运行结果如下图。

上述代码中,第37行代码使用super()调用了父类中有两个参数的构造方法;49~51行代码是在子类Dog中重写了父类Animal中的info()方法;第57~58行代码是实例化了一个Dog对象并调用了info()方法。由运行结果可知,程序输出的内容是在子类中定义的内容。这说明,如果在子类中重写了父类的info()方法,使用子类的实例化对象调用info()方法时,会优先调用子类中的info()方法。
注意
通过super()调用父类构造方法的代码必须位于子类构造方法的第一行,并且只能出现一次。
super与this关键字的作用非常相似,都可以调用构造方法、普通方法和属性,但是两者之间还是有区别的,super与this的区别如下表。
| 区别点 | this | super |
|---|---|---|
| 属性访问 | 访问本类中的属性,如果本类中没有该属性,则从父类中查找。 | 访问父类中的属性 |
| 方法 | 访问本类中的方法,如果本类中没有该方法,则从父类中继续查找 | 直接访问父类中的方法 |
| 调用构造 | 调用本类构造,必须放在构造方法的首行 | 调用父类构造,必须放在子类构造方法的首行 |
注意
this和super两者不可以同时出现,因为this和super在调用构造方法时都要求必须放在构造方法的首行。
final的英文意思是“最终”。在Java中,可以使用final关键字声明类、属性、方法,在声明时需要注意以下几点:
Java中的类被final关键字修饰后,该类将不可以被继承,即不能够派生子类。
// 使用final关键字修饰Animal类
final class Animal {
// 方法体为空
}
// Dog类继承Animal类
class Dog extends Animal {
// 方法体为空
}
// 定义测试类
public class Example07 {
public static void main(String[] args) {
Dog dog = new Dog(); // 创建Dog类的实例对象
}
}
程序编译时报错,如下图。

当一个类的方法被final关键字修饰后,这个类的子类将不能重写该方法。接下来通过一个案例验证。
// 定义Animal类
class Animal {
// 使用final关键字修饰shout()方法
public final void shout() {
}
}
// 定义Dog类继承Animal类
class Dog extends Animal {
// 重写Animal类的shout()方法
public void shout() {
}
}
// 定义测试类
public class Example08 {
public static void main(String[] args) {
Dog dog = new Dog(); // 创建Dog类的实例对象
}
}
程序运行结果如下图。

在上述代码中,第11行代码在Dog类中重写了父类Animal中的shout()方法,编译报错。这是因为Animal类的shout()方法被final修饰,而被final关键字修饰的方法为最终方法,子类不能对该方法进行重写。因此,当在父类中定义某个方法时,如果不希望被子类重写,就可以使用final关键字修饰该方法。
Java中被final修饰的变量是为常量,常量只能在声明时被赋值一次,在后面的程序中,其值不能被改变。如果再次对该常量赋值,则程序会在编译时报错。接下来通过一个案例进行验证。
public class Example09 {
public static void main(String[] args) {
final int AGE = 18; // 第一次可以赋值
AGE = 20; // 再次赋值会报错
}
}
编译上述程序,程序报错,如下图。

在使用final声明变量时,要求全部的字母大写。如果一个程序中的变量使用public static final声明,则此变量将成为全局变量,如下面代码所示。
public static final String NAME = "哈士奇";
当定义一个类时,常常需要定义一些成员方法描述类的行为特征,但有时这些方法的实现方式是无法确定的。例如,前面在定义Animal类时,shout()方法用于描述动物的叫声,但是针对不同的动物,叫声也是不同的,因此在shout()方法中无法准确描述动物的叫声。
针对上面描述的情况,Java提供了抽象方法来满足这种需求。抽象方法是使用abstract关键字修饰的成员方法,抽象方法在定义时不需要实现方法体。抽象方法的定义格式如下:
abstract void 方法名称(参数);
当一个类包含了抽象方法,该类必须是抽象类。抽象类和抽象方法一样,必须使用abstract关键字进行修饰。
抽象类的定义格式如下:
abstract class 抽象类名称{
访问权限 返回值类型 方法名称(参数){
return [返回值];
}
访问权限 abstract 返回值类型 抽象方法名称(参数); //抽象方法,无方法体
}
抽象类的定义规则如下:
接下来通过一个案例学习抽象类的使用。
// 定义抽象类Animal
abstract class Animal {
// 定义抽象方法shout()
abstract void shout();
}
// 定义Dog类继承抽象类Animal
class Dog extends Animal {
// 实现抽象方法shout()
void shout() {
System.out.println("汪汪...");
}
}
// 定义测试类
public class Example10 {
public static void main(String[] args) {
Dog dog = new Dog(); // 创建Dog类的实例对象
dog.shout(); // 调用dog对象的shout()方法
}
}
程序运行结果如下图。

上述代码中,第2~5行代码是声明了一个抽象类Animal,并在Animal类中声明了一个抽象方法shout();第10~12行代码在子类Dog中实现父类Animal的抽象方法shout();第18行代码通过子类的实例化对象调用shout()方法。
注意
使用abstract关键字修饰的抽象方法不能使用private修饰,因为抽象方法必须被子类实现,如果使用了private声明,则子类无法实现该方法。
如果一个抽象类的所有方法都是抽象的,则可以将这个类定义接口。接口是Java中最重要的概念之一,接口是一种特殊的类,由全局常量和公共的抽象方法组成,不能包含普通方法。
在JDK8之前,接口是由全局常量和抽象方法组成的,且接口中的抽象方法不允许有方法体。JDK 8对接口进行了重新定义,接口中除了抽象方法外,还可以有默认方法和静态方法(也叫类方法),默认方法使用default修饰,静态方法使用static修饰,且这两种方法都允许有方法体。
接口使用interface关键字声明,语法格式如下:
public interface 接口名 extends 接口1,接口2... {
public static final 数据类型 常量名 = 常量值;
public default 返回值类型 抽象方法名(参数列表);
public abstract 返回值类型 方法名(参数列表){
//默认方法的方法体
}
public abstract 返回值类型 方法名(参数列表){
//类方法的方法体
}
}
在上述语法中,“extends 接口1,接口2…”表示一个接口可以有多个父接口,父接口之间使用逗号分隔。Java使用接口的目的是为了克服单继承的限制,因为一个类只能有一个父类,而一个接口可以同时继承多个父接口。接口中的变量默认使用“public static final”进行修饰,即全局常量。接口中定义的方法默认使用“public abstract”进行修饰,即抽象方法。如果接口声明为public,则接口中的变量和方法全部为public。
在很多的Java程序中,经常看到编写接口中的方法时省略了public ,有很多读者认为它的访问权限是default,这实际上是错误的。不管写不写访问权限,接口中的方法访问权限永远是public。与此类似,在接口中定义常量时,可以省略前面的“public static final”,此时,接口会默认为常量添加“public static final”。
与抽象类一样,接口的使用必须通过子类,子类通过implements关键字实现接口,并且子类必须实现接口中的所有抽象方法。需要注意的是,一个类可以同时实现多个接口,多个接口之间需要使用英文逗号(,)分隔。
定义接口的实现类,语法格式如下:
修饰符 class 类名 implements 接口1,接口2,...{
...
}
下面通过一个案例学习接口的使用。
// 定义抽象类Animal
interface Animal {
int ID = 1; // 定义全局常量
String NAME = "牧羊犬";
void shout(); // 定义抽象方法shout()
static int getID() {
return Animal.ID;
}
public void info(); // 定义静态方法info()
}
interface Action {
public void eat(); // 定义抽象方法eat()
}
// 定义Dog类实现Animal接口和Action接口
class Dog implements Animal, Action {
// 重写Action接口中的抽象方法eat()
public void eat() {
System.out.println("喜欢吃骨头");
}
// 重写Animal接口中的抽象方法shout()
public void shout() {
System.out.println("汪汪...");
}
// 重写Animal接口中的抽象方法info()
public void info() {
System.out.println("名称:" + NAME);
}
}
// 定义测试类
class Example11 {
public static void main(String[] args) {
System.out.println("编号" + Animal.getID());
Dog dog = new Dog(); // 创建Dog类的实例对象
dog.info();
dog.shout(); // 调用Dog类中重写的shout()方法
dog.eat(); // 调用Dog类中重写的eat()方法
}
}
程序运行结果如下图。

上述代码中,第2~13行代码定义了一个Animal接口,在Animal接口中定义了全局常量id和NAME、抽象方法shout()、info()和静态方法getID()。第15~17行代码定义了一个Action接口,在Action接口中定义了一个抽象方法eat()。第20~35行代码定义了一个Dog类,Dog类通过implements实现了Animal接口和Action接口,并实现了这两个接口中的方法。第40行代码使用Animal接口名直接访问了Animal接口中的静态方法getID()。第41~44行代码创建并实例化了Dog类对象dog,通过dog对象访问了Animal接口和Action接口中的常量、抽象方法 。
从运行结果可以看出,Dog类的实例化对象可以访问接口中的常量、实现的接口方法以及本类内部的方法,而接口中的静态方法则可以直接使用接口名调用。需要注意的是,接口的实现类,必须实现接口中的所有方法,否则程序编译报错。
如果在开发中一个子类既要实现接口又要继承抽象类,则可以按照以下格式定义子类。
修饰符 class 类名 extends 父类名 implements 接口1,接口2,... {
...
}
下面通过一个案例演示子类既实现接口又继承抽象类的情况。
// 定义抽象类Animal
interface Animal {
public String NAME = "牧羊犬";
public void shout(); // 定义抽象方法shout()
public void info(); // 定义抽象方法info()
}
abstract class Action {
public abstract void eat(); // 定义抽象方法eat()
}
// 定义Dog类实现Animal接口和Action接口
class Dog extends Animal implements Action {
// 重写Action接口中的抽象方法eat()
public void eat() {
System.out.println("喜欢吃骨头");
}
// 重写Animal接口中的抽象方法shout()
public void shout() {
System.out.println("汪汪...");
}
// 重写Animal接口中的抽象方法info()
public void info() {
System.out.println("名称:" + NAME);
}
}
// 定义测试类
class Example12 {
public static void main(String[] args) {
Dog dog = new Dog(); // 创建Dog类的实例对象
dog.info(); // 调用Dog类中重写的info()方法
dog.shout(); // 调用Dog类中重写的shout()方法
dog.eat(); // 调用Dog类中重写的eat()方法
}
}
上述代码中,Dog类通过extends实现了Animal接口,同时通过implements实现了抽象类Action。因为Animal接口和抽象类Action本身都有抽象方法,所以子类中必须实现。
在Java中,接口是不允许继承抽象类的,但是允许一个接口继承多个接口。接下来通过一个案例讲解接口的继承。
// 定义抽象类Animal
interface Animal {
public String NAME = "牧羊犬";
public void info(); // 定义抽象方法info()
}
interface Color {
public void black(); // 定义抽象方法black()
}
interface Action extends Animal, Color {
public void shout(); // 定义抽象方法shout()
}
// 定义Dog类实现Action接口
class Dog implements Action {
// 重写Animal接口中的抽象方法info()
public void info() {
System.out.println("名称:" + NAME);
}
// 重写Color接口中的抽象方法black()
public void black() {
System.out.println("黑色");
}
// 重写Action接口中的抽象方法shout()
public void shout() {
System.out.println("汪汪...");
}
}
// 定义测试类
class Example13 {
public static void main(String[] args) {
Dog dog = new Dog(); // 创建Dog类的实例对象
dog.info(); // 调用Dog类中重写的info()方法
dog.shout(); // 调用Dog类中重写的shout()方法
dog.black(); // 调用Dog类中重写的eat()方法
}
}
程序运行结果如下图。

在上述代码中,第12~14行代码定义了接口Action并继承接口Animal和Color,这样接口Action中就同时拥有Animal和Color接口中info()和black()方法以及本类中shout()方法。在第17~32行代码定义了一个Dog类并实现了Action接口,这样Dog类就必须同时实现这3个抽象方法。
多态性是面向对象思想中的一个非常重要的概念,在Java中,多态是指不同对象在调用同一个方法时表现出的多种不同行为。例如,要实现一个动物叫的方法,由于每种动物的叫声是不同的,因此可以在方法中接收一个动物类型的参数,当传入猫类对象时就发出猫类的叫声,传入犬类对象时就发出犬类的叫声。在同一个方法中,这种由于参数类型不同而导致执行效果不同的现象就是多态。
Java中多态主要有以下两种形式:
(1)方法的重载。
(2)对象的多态性(方法重写)。
接下来通过一个案例演示Java程序中的多态。
// 定义抽象类Anmal
abstract class Animal {
abstract void shout(); // 定义抽象shout()方法
}
// 定义Cat类继承Animal抽象类
class Cat extends Animal {
// 实现shout()方法
public void shout() {
System.out.println("喵喵……");
}
}
// 定义Dog类继承Animal抽象类
class Dog extends Animal {
// 实现shout()方法
public void shout() {
System.out.println("汪汪……");
}
}
// 定义测试类
public class Example14 {
public static void main(String[] args) {
Cat an1 = new Cat(); // 创建Cat对象,使用Animal类型的变量an1引用
Dog an2 = new Dog(); // 创建Dog对象,使用Animal类型的变量an2引用
an1.shout();
an2.shout();
}
}
程序运行结果如下图。

上述代码中,第2~4行代码定义了一个抽象类Animal,在抽象类Animal中定义了抽象方法shout()。第7~20行代码定义了两个继承Animal的类Cat和Dog,并在Cat类和Dog类中重写了Animal类中的shout()方法。第25~28行代码是创建了Cat类对象和Dog类对象,并将Cat类对象和Dog类对象向上转型成了Animal类型的对象,然后通过Animal类型的对象an1和an2调用shout()方法。从运行结果可以看出,对象an1和an2调用的分别是Cat类和Dog类中的shout()方法。
对象类型转换主要分为以下两种情况:
(1)向上转型:子类对象→父类对象。
(2)向下转型:父类对象→子类对象。
对于向上转型,程序会自动完成,而向下转型时,必须指明要转型的子类类型。
对象类型的转换格式如下所示。
//对象向上转型:
父类类型 父类对象 = 子类实例;
//对象向下转型:
父类类型 父类对象 = 子类实例;
子类类型 子类对象 = (子类)父类对象;
下面通过一个案例介绍如何进行对象的向上转型操作。
// 定义类Anmal
class Animal {
public void shout() {
System.out.println("喵喵……");
}
}
// Dog类
class Dog extends Animal {
// 重写shout()方法
public void shout() {
System.out.println("汪汪……");
}
public void eat() {
System.out.println("吃骨头……");
}
}
// 定义测试类
public class Example15 {
public static void main(String[] args) {
Dog dog = new Dog(); // 创建Dog对象
Animal an = dog;
an.shout();
}
}
程序运行结果如下图。

上述代码中,第23~25行代码是创建了一个dog对象,并将dog对象向上转型成Animal类型的对象an,然后使用对象an调用shout()方法。从程序的运行结果中可以发现,虽然是使用父类对象an调用了shout()方法,但实际上调用的是被子类重写过的shout()方法。也就是说,如果对象发生了向上转型关系后,所调用的方法一定是被子类重写过的方法。
注意
父类Animal的对象an是无法调用Dog类中的eat()方法的,因为eat()方法只在子类中定义,而没有在父类中定义。
在进行对象的向下转型前,必须发生对象向上转型,否则将出现对象转换异常。接下来通过一个案例演示对象进行向下转型。
// 定义类Anmal
class Animal {
public void shout() {
System.out.println("喵喵……");
}
}
// Dog类
class Dog extends Animal {
// 重写shout()方法
public void shout() {
System.out.println("汪汪……");
}
public void eat() {
System.out.println("吃骨头……");
}
}
// 定义测试类
public class Example16 {
public static void main(String[] args) {
Animal an = new Dog(); // 此时发生了向上转型,子类→父类
Dog dog = (Dog) an; // 此时发生了向下转型
dog.shout();
dog.eat();
}
}
程序运行结果如下图。

在上述程序中,第23行代码发生了向上转型,将Dog类的实例转换成了Animal类的实例an,第24行代码是将Animal类的实例转换为Dog类的实例。第25行代码使用dog对象调用shout()方法,由于Animal类的shout()方法已被子类Dog类重写,因此dog对象调用的方法是被子类重写过的方法。
注意
在向下转型时,不能直接将父类实例强制转换为子类实例,否则程序会报错。例如,将上面代码中的第23~24行代码修改为下面一行代码,则程序报错。
Dog dog = (Dog)new Animal(); //编译错误
- 1
Java中可以使用instanceof关键字判断一个对象是否是某个类(或接口)的实例,语法格式如下所示。
对象 instanceof 类(或接口)
在上述格式中,如果对象是指定的类的实例对象,则返回true,否则返回false。
接下来通过一个案例演示instanceof关键字的用法。
// 定义类Anmal
class Animal {
public void shout() {
System.out.println("动物叫……");
}
}
// Dog类
class Dog extends Animal {
// 重写shout()方法
public void shout() {
System.out.println("汪汪……");
}
public void eat() {
System.out.println("吃骨头……");
}
}
// 定义测试类
public class Example17 {
public static void main(String[] args) {
Animal a1 = new Dog(); // 通过向上转型实例化Animal对象
System.out.println("Animal a1 = new Dog():" + (a1 instanceof Animal));
System.out.println("Animal a1 = new Dog():" + (a1 instanceof Dog));
Animal a2 = new Animal(); // 实例化Animal对象
System.out.println("Animal a1 = new Animal():" + (a2 instanceof Animal));
System.out.println("Animal a1 = new Animal():" + (a2 instanceof Dog));
}
}
程序运行结果如下图。

上述代码中,第2~6行代码定义了Animal类;第9~18行代码定义了Dog类继承Animal类;第23行代码实例化Dog类对象,并将Dog类实例向上转型为Animal类对象a1。第24行代码是通过instanceof关键字判断对象a1是否是Animal类的实例,第25行代码是通过instanceof关键字判断对象a1是否是Dog类的实例;第26行代码是实例化了一个Animal类对象a2,第27行代码是通过instanceof关键字判断对象a2是否是Animal类的实例,第28行代码通过instanceof关键字判断对象a2是否是Dog类的实例。
Java提供了一个Object类,它是所有类的父类,每个类都直接或间接继承Object类,因此Object类通常被称之为超类。当定义一个类时,如果没有使用extends关键字为这个类显式地指定父类,那么该类会默认继承Object类。
Object类常用方法如下表。
| 方法名称 | 方法说明 |
|---|---|
| boolean equals() | 判断两个对象是否“相等” |
| int hashCode() | 返回对象的哈希码值 |
| String toString() | 返回对象的字符串表示形式 |
下面通过一个例子演示Object类中toString()方法的使用。
// 定义Animal类
class Animal {
// 定义动物叫的方法
void shout() {
System.out.println("动物叫!");
}
}
// 定义测试类
public class Example18 {
public static void main(String[] args) {
Animal animal = new Animal(); // 创建Animal类对象
System.out.println(animal.toString()); // 调用toString()方法并打印
}
}
程序运行结果如下图。

在实际开发中,通常希望对象的toString()方法返回的不仅仅是基本信息,而是对象特有的信息,这时可以重写Object类的toString()方法。
// 定义Animal类
class Animal {
// 重写Object类的toString()方法
public String toString() {
return "这是一个动物。";
}
}
// 定义测试类
public class Example19 {
public static void main(String[] args) {
Animal animal = new Animal(); // 创建Animal类对象
System.out.println(animal.toString()); // 调用toString()方法并打印
}
}
程序运行结果如下图。

在Java中,允许在一个类的内部定义类,这样的类称作内部类,内部类所在的类称作外部类。在实际开发中,根据内部类的位置、修饰符和定义方式的不同,内部类可分为4种,分别是成员内部类、局部内部类、静态内部类、匿名内部类。
在一个类中除了可以定义成员变量、成员方法,还可以定义类,这样的类被称作成员内部类。成员内部类可以访问外部类的所有成员。
接下来通过一个案例学习如何定义成员内部类。
class Outer {
int m = 0; // 定义类的成员变量
// 下面的代码定义了一个成员方法,方法中访问内部类
void test1() {
System.out.println("外部类成员方法");
}
// 下面的代码定义了一个成员内部类
class Inner {
int n = 1;
void show1() {
// 在成员内部类的方法中访问外部类的成员变量
System.out.println("外部成员变量m = " + m);
}
void show2() {
// 在成员内部类的方法中访问外部类的成员变量
System.out.println("内部成员方法");
}
}
void test2() {
Inner inner = new Inner();
System.out.println("内部成员变量n = " + inner.n);
inner.show2();
}
}
public class Example20 {
public static void main(String[] args) {
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.show1();
outer.test2();
}
}
程序运行结果如下图。

上述代码中,第1~29行代码定义了一个Outer类,Outer类就是一个外部类。第10~22行代码是在Outer类内部定义了Inner类,Inner类就是Outer类的成员内部类。Outer类中定义了test1()和test2()两个方法。Inner类定义了一个show()方法,在show()方法中访问外部类的成员变量num;test2()方法中创建了内部类Inner的实例对象,并通过该对象调用show()方法,将num值输出。从运行结果可以看出,内部类可以在外部类中使用,并能访问外部类的成员。
如果想通过外部类访问内部类,则需要通过外部类创建内部类对象,创建内部类对象的具体语法格式如下:
外部类名.内部类名 变量名 = new 外部类名().new 内部类名();
局部内部类,也叫作方法内部类,是指定义在某个局部范围中的类,它和局部变量一样,都是在方法中定义的,有效范围只限于方法内部。
在局部内部类中,局部内部类可以访问外部类的所有成员变量和方法,而局部内部类中变量和方法却只能在所属方法中访问。
接下来通过一个案例学习局部内部类的定义和使用。
class Outer {
int m = 0; // 定义类的成员变量
// 下面的代码定义了一个成员方法,方法中访问内部类
void test1() {
System.out.println("外部类成员方法");
}
void test2() {
// 下面的代码定义了一个成员内部类
class Inner {
int n = 1;
void show() {
// 在成员内部类的方法中访问外部类的成员变量
System.out.println("外部成员变量m = " + m);
test1();
}
}
Inner inner = new Inner();
System.out.println("局部内部类变量n = " + inner.n);
inner.show();
}
}
public class Example21 {
public static void main(String[] args) {
Outer outer = new Outer();
outer.test2();
}
}
程序运行结果如下图。

在上述代码中,第1~24行代码定义了一个外部类Outer,并在该类中定义了成员变量m、成员方法test1()和test2()。第11~19行代码是在外部类的成员方法test2()中定义了一个局部内部类Inner;然后在局部内部类Inner中,编写了show()方法。第16~17行代码是对外部类变量和方法的调用;第20~22行代码是在test2()方法中创建了局部内部类Inner对象,并调用局部内部类的方法和变量。
所谓静态内部类,就是使用static关键字修饰的成员内部类。与成员内部类相比,在形式上,静态内部类只是在内部类前增加了static关键字,但在功能上,静态内部类只能访问外部类的静态成员,通过外部类访问静态内部类成员时,可以跳过外部类直接访问静态内部类。
创建静态内部类对象的基本语法格式如下:
外部类名.静态内部类名 变量名 = new 外部类名().静态内部类名();
接下来通过一个案例学习静态内部类的定义和使用。
class Outer {
static int m = 0; // 定义类的成员变量
// 下面的代码定义了一个静态内部类
static class Inner {
int n = 1;
void show() {
// 在静态内部类的方法中访问外部类的成员变量
System.out.println("外部静态变量m = " + m);
}
}
}
public class Example22 {
public static void main(String[] args) {
Outer.Inner inner = new Outer.Inner();
inner.show();
}
}
程序运行结果如下图。

上述代码中,第1~14行代码定义了一个外部类Outer,其中第3~13行代码是在Outer类中定义了静态成员变量和静态内部类Inner。然后在静态内部类Inner中,编写了一个show()方法,在show()方法中打印了外部静态变量m,第18~19行代码是声明了一个内部类对象inner,并使用inner对象调用show()方法测试对外部类静态变量m的调用。
匿名内部类是没有名称的内部类。在Java中调用某个方法时,如果该方法的参数是接口类型,除了可以传入一个接口实现类,还可以使用实现接口的匿名内部类作为参数,在匿名内部类中直接完成方法的实现。
创建匿名内部类的基本语法格式如下:
new 父接口(){
//匿名内部类实现部分
}
interface Animal {
void shout();
}
public class Example23 {
public static void main(String[] args) {
String name = "小花";
animalShout(new Animal() {
@Override
public void shout() {
System.out.println(name + "喵喵...");
}
});
}
public static void animalShout(Animal an) {
an.shout();
}
}
程序运行结果如下图。

第1~3行代码创建了Animal接口;第8~13行代码是调用animalShout()方法,将实现Animal接口的匿名内部类作为animalShout()方法的参数,并在匿名内部类中重写了Animal接口的show()方法。
在上述代码中的匿名内部类中访问了局部变量name,而局部变量name并没有使用final修饰符修饰,程序也没有报错。这是JDK 8的新增特性,允许在局部内部类、匿名内部类中访问非final修饰的局部变量,而在JDK 8之前,局部变量前必须加final修饰符,否则程序编译时报错。
匿名类的编写步骤:
(1)在调用animalShout()方法时,在方法的参数位置写上new Animal(){},这相当于创建了一个实例对象,并将对象作为参数传给animalShout()方法。在new Animal()后面有一对大括号,表示创建的对象为Animal的子类实例,该子类是匿名的。具体代码如下所示:
animalShout(new Animal(){});
(2)在大括号中编写匿名子类的实现代码,具体如下所示:
animalShout(new Animal() {
public void shout() {
System.out.println("喵喵……");
}
});
尽管人人希望自己身体健康,处理的事情都能顺利进行,但在实际生活中总会遇到各种状况,比如感冒发烧,工作时电脑蓝屏、死机等。同样,在程序运行的过程中,也会发生各种非正常状况,例如,程序运行时磁盘空间不足、网络连接中断、被装载的类不存在等。针对这种情况, Java语言引入了异常,以异常类的形式对这些非正常情况进行封装,通过异常处理机制对程序运行时发生的各种问题进行处理。
接下来通过一个案例认识一下什么是异常。
public class Example24 {
public static void main(String[] args) {
int result = divide(4, 0); // 调用divide()方法
System.out.println(result);
}
// 下面的方法实现了两个整数相除
public static int divide(int x, int y) {
int result = x / y; // 定义一个变量result记录两个数相除的结果
return result; // 将结果返回
}
}
程序运行结果如下图。

从运行结果可以看出,程序发生了算术异常(ArithmeticException),该异常是由于代码中的第3行代码调用divide()方法时传入了参数0,运算时出现了被0除的情况。异常发生后,程序会立即结束,无法继续向下执行。
上述程序产生的ArithmeticException异常只是Java异常类中的一种,Java提供了大量的异常类,这些类都继承自java.lang.Throwable类。
接下来通过一张图展示Throwable类的继承体系。

Throwable有两个直接子类Error和Exception,其中,Error代表程序中产生的错误,Exception代表程序中产生的异常。
● Error类称为错误类,它表示Java程序运行时产生的系统内部错误或资源耗尽的错误,这类错误比较严重,仅靠修改程序本身是不能恢复执行的。举一个生活中的例子,在盖楼的过程中因偷工减料,导致大楼坍塌,这就相当于一个Error。例如,使用java命令去运行一个不存在的类就会出现Error错误。
● Exception类称为异常类,它表示程序本身可以处理的错误,在Java程序中进行的异常处理,都是针对Exception类及其子类的。在Exception类的众多子类中有一个特殊的子类—RuntimeException类,RuntimeException类及其子类用于表示运行时异常。 Exception类的其他子类都用于表示编译时异常。
Throwable类中的常用方法如下表。
| 方法声明 | 功能描述 |
|---|---|
| String getMessage() | 返回异常的消息字符串 |
| String toString() | 返回异常的简单信息描述 |
| void printStackTrace() | 获取异常类名和异常信息,以及异常出现在程序中的位置,把信息输出在控制台。 |
出现异常后,程序会立即终止。为了解决异常,Java提供了对异常进行处理的方式一一异常捕获。异常捕获使用try…catch语句实现,try…catch具体语法格式如下:
try{
//程序代码块
}catch(ExceptionType(Exception类及其子类) e){
//对ExceptionType的处理
}
上述语法格式中,在try代码块中编写可能发生异常的Java语句,catch代码块中编写针对异常进行处理的代码。当try代码块中的程序发生了异常,系统会将异常的信息封装成一个异常对象,并将这个对象传递给catch代码块进行处理。catch代码块需要一个参数指明它所能够接收的异常类型,这个参数的类型必须是Exception类或其子类。
接下来通过一个案例演示用try…catch语句对异常进行捕获。
public class Example25 {
public static void main(String[] args) {
// 下面的代码定义了一个try…catch语句用于捕获异常
try {
int result = divide(4, 0); // 调用divide()方法
System.out.println(result);
} catch (Exception e) { // 对异常进行处理
System.out.println("捕获的异常信息为:" + e.getMessage());
}
System.out.println("程序继续向下执行...");
}
// 下面的方法实现了两个整数相除
public static int divide(int x, int y) {
int result = x / y; // 定义一个变量result记录两个数相除的结果
return result; // 将结果返回
}
}
程序运行结果如下图。

上述代码中,第4~9行代码是对可能发生异常的代码用try…catch语句进行了处理。在try代码块中发生除0异常时,程序会通过catch语句捕获异常,第8行代码在catch语句中通过调用Exception对象的getMessage()方法,返回异常信息“/ by zero”。catch代码块对异常处理完毕后,程序仍会向下执行,而不会终止程序。
需要注意的是,在try代码块中,发生异常语句后面的代码是不会被执行的,如上述代码中第6行代码的打印语句就没有执行。
在程序中,有时候会希望有些语句无论程序是否发生异常都要执行,这时就可以在try…catch语句后,加一个finally代码块。
public class Example26 {
public static void main(String[] args) {
// 下面的代码定义了一个try…catch…finally语句用于捕获异常
try {
int result = divide(4, 0); // 调用divide()方法
System.out.println(result);
} catch (Exception e) { // 对捕获到的异常进行处理
System.out.println("捕获的异常信息为:" + e.getMessage());
return; // 用于结束当前语句
} finally {
System.out.println("进入finally代码块");
}
System.out.println("程序继续向下执行…");
}
// 下面的方法实现了两个整数相除
public static int divide(int x, int y) {
int result = x / y; // 定义一个变量result记录两个数相除的结果
return result; // 将结果返回
}
}
程序运行结果如下图。

上述代码中,第9行代码是在catch代码块中增加了一个return语句,用于结束当前方法,程序第13行代码就不会执行了,而finally代码块中的代码仍会执行,不受return语句影响。也就是说不论程序是发生异常还是使用return语句结束,finally中的语句都会执行。因此,在程序设计时,通常会使用finally代码块处理完成必须做的事情,如释放系统资源。
注意
finally中的代码块在一种情况下是不会执行的,那就是在try…catch中执行了System.exit(0)语句。System.exit(0)表示退出当前的Java虚拟机,Java虚拟机停止了,任何代码都不能再执行了。
在实际开发中,大部分情况下我们会调用别人编写方法,并不知道别人编写的方法是否会发生异常。针对这种情况,Java允许在方法的后面使用throws关键字对外声明该方法有可能发生的异常,这样调用者在调用方法时,就明确地知道该方法有异常,并且必须在程序中对异常进行处理,否则编译无法通过。
throws关键字声明抛出异常的语法格式如下:
修饰符 返回值类型 方法名(参数1,参数2.....) throws 异常类1, 异常类2.....{
//方法体.....
}
从上述语法格式中可以看出,throws关键字需要写在方法声明的后面,throws后面需要声明方法中发生异常的类型。
接下来通过一个案例演示throws关键字的用法。
public class Example27 {
public static void main(String[] args) {
int result = divide(4, 2); // 调用divide()方法
System.out.println(result);
}
// 下面的方法实现了两个整数相除,并使用throws关键字声明抛出异常
public static int divide(int x, int y) throws Exception {
int result = x / y; // 定义一个变量result记录两个数相除的结果
return result; // 将结果返回
}
}
编译程序,编译器报错,如下图。

在上述代码中,第3行代码调用divide()方法时传入的第二个参数为2,程序在运行时不会发生被0除的异常,但是由于定义divide()方法时声明了抛出异常,调用者在调用divide()方法时就必须进行处理,否则就会发生编译错误。
接下来修改上述程序,在try…catch处理divide()方法抛出的异常。
public class Example28 {
public static void main(String[] args) {
// 下面的代码定义了一个try…catch语句用于捕获异常
try {
int result = divide(4, 2); // 调用divide()方法
System.out.println(result);
} catch (Exception e) { // 对捕获到的异常进行处理
e.printStackTrace(); // 打印捕获的异常信息
}
}
// 下面的方法实现了两个整数相除,并使用throws关键字声明抛出异常
public static int divide(int x, int y) throws Exception {
int result = x / y; // 定义一个变量result记录两个数相除的结果
return result; // 将结果返回
}
}
上述代码由于使用了try…catch对divide()方法进行了异常处理,因此程序可以编译通过,运行后正确的打印出了运行结果2。程序运行结果如下图。

由于使用了try…catch对divide()方法进行了异常处理,因此程序可以编译通过,运行后正确的打印出了运行结果2。下面修改上述程序,将divide()方法抛出的异常继续抛出。
public class Example29 {
public static void main(String[] args) throws Exception {
int result = divide(4, 0); // 调用divide()方法
System.out.println(result);
}
// 下面的方法实现了两个整数相除,并使用throws关键字声明抛出异常
public static int divide(int x, int y) throws Exception {
int result = x / y; // 定义一个变量result记录两个数相除的结果
return result; // 将结果返回
}
}
程序运行结果如下图。

解析:上述代码,在main()方法继续使用throws关键字将Exception抛出,程序虽然可以通过编译,但从运行结果可以看出,在运行时期由于没有对“/by zero”的异常进行处理,最终导致程序终止运行。
在实际开发中,经常会在程序编译时产生一些异常,这些异常必须要进行处理,这种异常被称为编译时异常,也称为checked异常。另外还有一种异常是在程序运行时产生的,这种异常即使不编写异常处理代码,依然可以通过编译,因此被称为运行时异常,也称为unchecked异常。
1.编译时异常
在Exception类中,除了RuntimeException类及其子类,Exception的其他子类都是编译时异常。编译时异常的特点是Java编译器会对异常进行检查,如果出现异常就必须对异常进行处理,否则程序无法通过编译。
处理编译时期的异常有两种方式,具体如下:
(1)使用try…catch语句对异常进行捕获处理。
(2)使用throws关键字声明抛出异常,调用者对异常进行处理。
2.运行时异常
RuntimeException类及其子类都是运行时异常。运行时异常的特点是Java编译器不会对异常进行检查。也就是说,当程序中出现这类异常时,即使没有使用try…catch语句捕获或使用throws关键字声明抛出,程序也能编译通过。运行时异常一般是由程序中的逻辑错误引起的,在程序运行时无法恢复。
例如,通过数组的角标访问数组的元素时,如果角标超过了数组范围,就会发生运行时异常,代码如下所示:
int[] arr=new int[5];
System.out.println(arr[6]);
在上面的代码中,由于数组arr的length为5,最大角标应为4,当使用arr[6]访问数组中的元素就会发生数组角标越界的异常。
JDK中定义了大量的异常类,虽然这些异常类可以描述编程时出现的大部分异常情况,但是在程序开发中有时可能需要描述程序中特有的异常情况,例如,前面讲解的程序中的divide()方法,不允许被除数为负数。为了解决这个问题,Java允许用户自定义异常,但自定义的异常类必须继承自Exception或其子类。
自定义异常的具体代码如下所示。
// 下面的代码是自定义一个异常类继承自Exception
public class DivideByMinusException extends Exception{
public DivideByMinusException (){
super(); // 调用Exception无参的构造方法
}
public DivideByMinusException (String message){
super(message); // 调用Exception有参的构造方法
}
}
在实际开发中,如果没有特殊的要求,自定义的异常类只需继承Exception类,在构造方法中使用super()语句调用Exception的构造方法即可。
自定义异常类中使用throw关键字在方法中声明异常的实例对象,格式如下:
throw Exception异常对象
下面通过一个案例演示自定义异常类。
public class Example30 {
public static void main(String[] args) {
int result = divide(4, -2);
System.out.println(result);
}
// 下面的方法实现了两个整数相除
public static int divide(int x, int y) {
if (y < 0) {
throw new DivideByMinusException("除数是负数");
}
int result = x / y; // 定义一个变量result记录两个数相除的结果
return result; // 将结果返回
}
}
编译程序,编译器报错,如下图。

从运行结果可以看出,程序在编译时就发生了异常。因为在一个方法内使用throw关键字抛出异常对象时,需要使用try…catch语句对抛出的异常进行处理,或者在divide()方法上使用throws关键字声明抛出异常,由该方法的调用者负责处理。但是程序没有这样做。
为了解决上面的问题,对程序进行修改,在divide()方法上,使用throws关键字声明抛出DivideByMinusException异常,并在调用divide()方法时使用try…catch语句对异常进行处理。
public class Example31 {
public static void main(String[] args) {
// 下面的代码定义了一个try…catch语句用于捕获异常
try {
int result = divide(4, -2);
System.out.println(result);
} catch (DivideByMinusException e) { // 对捕获到的异常进行处理
System.out.println(e.getMessage()); // 打印捕获的异常信息
}
}
// 下面的方法实现了两个整数相除,并使用throws关键字声明抛出自定义异常
public static int divide(int x, int y) throws DivideByMinusException {
if (y < 0) {
throw new DivideByMinusException("除数是负数");
}
int result = x / y; // 定义一个变量result记录两个数相除的结果
return result; // 将结果返回
}
}
程序运行结果如下图。

上述代码中的main()方法中,第4~9行代码使用try…catch语句捕获处理divide()方法抛出的异常。在调用divide()方法时,如果传入的被除数不能为负数,程序会抛出一个自定义的DivideByMinusException异常,该异常最终被catch代码块捕获处理,并打印出异常信息。