本文编写于 235 天前,最后修改于 235 天前,其中某些信息可能已经过时。

类的继承

继承的概念

在Java中,类的继承是指在一个现有类的基础上去构建一个新的类,构建出来的新类被称作子类,现有类被称作父类,子类会自动拥有父类所有可继承的属性和方法。在程序中,如果想声明一个类继承另一个类,需要使用extends关键字,接下来通过一个案例来学习子类是如何继承父类的。

// 定义动物类
class Animal {
    String name;
    void shout() {
        System.out.println("动物发出叫声");
    }
}
// 定义dog类继承动物类
class Dog extends Animal {
    public void printName() {
        System.out.println("name = " + name);
    }
}
public class Example {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.name = "极品二哈";
        dog.printName();
        dog.shout();
    }
}

运行结果:

name = 极品二哈
动物发出叫声

例子中,Dog类通过extends关键字继承了Animal类,这样Dog类便是Animal类的子类。从运行结果不难看出,子类虽然没有定义name属性和shout()方法,但是却能访问这两个成员。这就说明,子类在继承父类的时候,会自动拥有父类所有的成员。在类的继承中,需要注意一些问题,具体如下:

  • 在Java中,类只支持单继承,不允许多重继承,也就是说一个类只能有一个直接父类,例如下面这种情况是不合法的。
class A{}
class B{}
class C extends A,B{} //C 类不可以同时继承A 类和B 类
  • 多个类可以继承一个父类,例如下面这种情况是允许的。
class A{}
class B extends A{}
class C extends A{} //类B 和类C 都可以继承类A
  • 在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 的子类
  • 在Java中,子类和父类是一种相对概念,也就是说一个类是某个类父类的同时,也可以是另一个类的子类。例如上面的示例中,B类是A 类的子类,同时又是C 类的父类。

重写父类方法

在继承关系中,子类会自动继承父类中定义的方法,但有时在子类中需要对继承的方法进行一些修改,即对父类的方法进行重写。需要注意的是,在子类中重写的方法需要和父类被重写的方法具有相同的方法名、参数列表以及返回值类型。

例中,Dog类从Animal类继承了shout()方法,该方法在被调用时会打印“动物发出叫声”,这明显不能描述一种具体动物的叫声,Dog类对象表示犬类,发出的叫声应该是“汪汪”。为了解决这个问题,可以在Dog类中重写父类Animal中的shout()方法。

// 定义动物类
class Animal {
    String name;
    void shout() {
        System.out.println("动物发出叫声");
    }
}
// 定义dog类继承动物类
class Dog extends Animal {
    void shout() {
        System.out.println("汪汪汪汪");
    }
}
public class Example {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.shout();
    }
}

运行结果:

汪汪汪汪

案例中定义了Dog类并且继承自Animal类。在子类Dog中定义了一个shout()方法对父类的方法进行重写。从运行结果可以看出,在调用Dog类对象的shout()方法时,只会调用子类重写的该方法,并不会调用父类的shout()方法。

注意:子类重写父类方法时,不能使用比父类中被重写的方法更严格的访问权限,如父类中的方法是public的,子类的方法就不能是private的,关于访问权限中更多的知识,我们将在本章结尾进行详细讲解,在这里大家只要有个印象就行了。

super关键字

从上一个例子中的运行结果可以看出,当子类重写父类的方法后,子类对象将无法访问父类被重写的方法,为了解决这个问题,在Java中专门提供了一个super关键字用于访问父类的成员。例如访问父类的成员变量、成员方法和构造方法。接下来分两种情况来学习一下super关键字的具体用法。

  • 使用super关键字调用父类的成员变量和成员方法。具体格式如下:
super.成员变量
super.成员方法([参数1,参数2…])
// 定义动物类
class Animal {
    String name = "动物";
    void shout() {
        System.out.println("动物发出叫声");
    }
}
// 定义dog类继承动物类
class Dog extends Animal {
    String name = "犬类";
    // 重写父类的shout方法
    void shout() {
        super.shout();
    }
    // 定义打印方法
    void printName() {
        System.out.println("name = " + super.name);
    }
}
public class Example {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.shout();
        dog.printName();
    }
}

运行结果:

动物发出叫声
name = 动物

案例中,定义了一个Dog类继承Animal类,并重写了Animal类的shout()方法。在子类Dog 的shout()方法中使用“super.shout()”调用了父类被重写的方法,在printName()方法中使用“super.name”访问父类的成员变量。从运行结果可以看出,子类通过super关键字可以成功地访问父类成员变量和成员方法。

  • 使用super关键字调用父类的构造方法。具体格式如下:
super([参数1,参数2…])
// 定义动物类
class Animal {
    public Animal(String name) {
        System.out.println("我是一只 " + name);
    }
}
// 定义dog类继承动物类
class Dog extends Animal {
    public Dog() {
        super("极品二哈");
    }
}
public class Example {
    public static void main(String[] args) {
        Dog dog = new Dog();
    }
}

运行结果:

我是一只 极品二哈

根据前面所学的知识,在实例化Dog对象时一定会调用Dog类的构造方法。从运行结果可以看出,Dog类的构造方法被调用时父类的构造方法也被调用了。需要注意的是,通过super调用父类构造方法的代码必须位于子类构造方法的第一行,并且只能出现一次。

在定义一个类时,如果没有特殊需求,尽量在类中定义一个无参的构造方法,避免被继承时出现错误。

final关键字

final关键字修饰类

Java中的类被final关键字修饰后,该类将不可以被继承,也就是不能够派生子类。

// 定义动物类
final class Animal {
}
// 定义dog类继承动物类
class Dog extends Animal {
}
public class Example {
    public static void main(String[] args) {
        Dog dog = new Dog();
    }
}

编译程序报错

由于Animal类被final关键字所修饰,因此,当Dog类继承Animal类时,编译出现了“无法从最终Animal进行继承”的错误。由此可见,被final关键字修饰的类为最终类,不能被其他类继承。

final关键字修饰方法

当一个类的方法被final关键字修饰后,这个类的子类将不能重写该方法。

// 定义动物类
class Animal {    
    public final void shout() {
    }
}
// 定义dog类继承动物类
class Dog extends Animal {
    public final void shout() {
    }
}
public class Example {
    public static void main(String[] args) {
        Dog dog = new Dog();
    }
}

编译程序报错

Dog 类重写父类Animal中的shout()方法后,编译报错。这是因为Animal类的shout()方法被final所修饰。由此可见,被final关键字修饰的方法为最终方法,子类不能对该方法进行重写。正是由于final的这种特性,当在父类中定义某个方法时,如果不希望被子类重写,就可以使用final关键字修饰该方法。

final关键字修饰变量

Java中被final修饰的变量为常量,它只能被赋值一次,也就是说final修饰的变量一旦被赋值,其值不能改变。如果再次对该变量进行赋值,则程序会在编译时报错。

public class Example {
    public static void main(String[] args) {
        final int num = 100;
        num = 4;
    }
}

编译程序报错

当第4行对num 赋值时,编译报错。原因在于变量num 被final修饰。由此可见,被final修饰的变量为常量,它只能被赋值一次,其值不可改变。

被final关键字修饰的变量为局部变量。

class Student {
    final String name;
    public void introduce() {
        System.out.println("我是一名学生,我的名字叫" + name);
    }
}
public class Example {
    public static void main(String[] args) {
        Student student = new Student();
        student.introduce();
    }
}

出现了编译错误,提示变量name没有初始化。这是因为使用final关键字修饰成员变量时,虚拟机不会对其进行初始化。因此使用final修饰成员变量时,需要在定义变量的同时赋予一个初始值,下面将第2行代码修改为:

final String name="小海绵"; //为final 关键字修饰的name 属性赋值

运行结果:

我是一名学生,我的名字叫小海绵

抽象类和接口

抽象类

当定义一个类时,常常需要定义一些方法来描述该类的行为特征,但有时这些方法的实现方式是无法确定的。例如前面在定义Animal类时,shout()方法用于表示动物的叫声,但是针对不同的动物,叫声也是不同的,因此在shout()方法中无法准确描述动物的叫声。

针对上面描述的情况,Java允许在定义方法时不写方法体,不包含方法体的方法为抽象方法,抽象方法必须使用abstract关键字来修饰,具体示例如下:abstract void shout(); //定义抽象方法shout()
当一个类中包含了抽象方法,该类必须使用abstract关键字来修饰,使用abstract关键字修饰的类为抽象类,具体示例如下:

abstract void shout(); //定义抽象方法shout()

当一个类中包含了抽象方法,该类必须使用abstract关键字来修饰,使用abstract关键字修饰的类为抽象类,具体示例如下:

//定义抽象类Animal
abstract class Animal {
    //定义抽象方法shout()
    abstract int shout();
}

在定义抽象类时需要注意,包含抽象方法的类必须声明为抽象类,但抽象类可以不包含任何抽象方法,只需使用abstract关键字来修饰即可。另外,抽象类是不可以被实例化的,因为抽象类中有可能包含抽象方法,抽象方法是没有方法体的,不可以被调用。如果想调用抽象类中定义的方法,则需要创建一个子类,在子类中将抽象类中的抽象方法进行实现。接下来通过一个案例来学习如何实现抽象类中的方法。

// 定义抽象类animal
abstract class Animal {
    // 定义抽象方法shout
    abstract void shout();
}
// 定义dog类继承抽象方法Animal
class Dog extends Animal {
    // 实现抽象方法shout
    void shout() {
        System.out.println("汪汪汪汪");
    }
}
public class Example {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.shout();
    }
}

运行结果:

汪汪汪汪

从运行结果可以看出,子类实现了父类的抽象方法后,可以正常进行实例化,并通过实例化对象调用方法。

接口

如果一个抽象类中的所有方法都是抽象的,则可以将这个类用另外一种方式来定义,即接口。在定义接口时,需要使用interface关键字来声明,具体示例如下:

interface Animal {
    int ID = 1; //定义全局常量
    void breathe(); //定义抽象方法
    void run();
}

上面的代码中,Animal即为一个接口。从示例中会发现抽象方法breathe()并没有使用abstract关键字来修饰,这是因为接口中定义的方法和变量都包含一些默认修饰符。接口中定义的方法默认使用“publicabstract”来修饰,即抽象方法。接口中的变量默认使用“publicstaticfinal”来修饰,即全局常量。

由于接口中的方法都是抽象方法,因此不能通过实例化对象的方式来调用接口中的方法。此时需要定义一个类,并使用implements关键字实现接口中所有的方法。接下来通过一个案例来学习。

// 定义接口animal
interface Animal {
    int ID = 1;
    void breathe();
    void run();
}
// 定义dog实现接口Animal
class Dog implements Animal {
    public void breathe() {
        System.out.println("狗在呼吸");
    }
    public void run() {
        System.out.println("狗在跑");
    }
}
public class Example {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.breathe();
        dog.run();
    }
}

运行结果:

狗在呼吸
狗在跑

从运行结果可以看出,类Dog在实现了Animal接口后是可以被实例化的。

上面的例子是类与接口之间的实现关系,在程序中,还可以定义一个接口使用extends关键字去继承另一个接口,接下来对上一个例子稍加修改,演示接口之间的继承关系。

//定义了Animal 接口
interface Animal {
    int ID = 1; // 定义全局常量
    void breathe(); // 定义抽象方法breathe()
    void run(); // 定义抽象方法run()
}
// 定义了LandAnimal 接口,并继承了Animal 接口
interface LandAnimal extends Animal { // 接口继承接口
    void liveOnland(); // 定义抽象方法liveOnLand()
}
// 定义Dog 类实现Animal 接口
class Dog implements LandAnimal {
    // 实现breathe()方法
    public void breathe() {
        System.out.println("狗在呼吸");
    }
    // 实现run()方法
    public void run() {
        System.out.println("狗在跑");
    }
    // 实现liveOnLand()方法
    public void liveOnland() {
        // TODO Auto-generated method stub
        System.out.println("狗生活在陆地上");
    }
}
public class Example {
    public static void main(String[] args) {
        Dog dog = new Dog(); // 创建Dog 类的实例对象
        dog.breathe(); // 调用Dog 类的breathe()方法
        dog.run(); // 调用Dog 类的run()方法
        dog.liveOnland(); // 调用Dog 类的liveOnland()方法
    }
}

运行结果:

狗在呼吸
狗在跑
狗生活在陆地上

例中,定义了两个接口,其中LandAnimal接口继承了Animal接口,因此LandAnimal接口包含了三个抽象方法。当Dog类实现LandAnimal接口时,需要实现两个接口中定义的三个方法。从运行结果看出,程序可以针对Dog类实例化对象并调用类中的方法。

为了加深初学者对接口的认识,接下来对接口的特点进行归纳,具体如下:

  • 接口中的方法都是抽象的,不能实例化对象。
  • 当一个类实现接口时,如果这个类是抽象类,则实现接口中的部分方法即可,否则需要实现接口中的所有方法。
  • 一个类通过implements关键字实现接口时,可以实现多个接口,被实现的多个接口之间要用逗号隔开。具体示例如下:
interface Run {
程序代码……
}
interface Fly {
程序代码……
}
class Bird implements Run,Fly {
程序代码……
}
  • 一个接口可以通过extends关键字继承多个接口,接口之间用逗号隔开。具体示例如下:
interface Running {
程序代码……
}
interface Flying {
程序代码……
}
Interface Eating extends Running,Flying {
程序代码……
}
  • 一个类在继承另一个类的同时还可以实现接口,此时,extends关键字必须位于implements关键字之前。具体示例如下:
class Dog extends Canidae implements Animal { //先继承,再实现
程序代码……
}

多态

多态概述

在设计一个方法时,通常希望该方法具备一定的通用性。例如要实现一个动物叫的方法,由于每种动物的叫声是不同的,因此可以在方法中接收一个动物类型的参数,当传入猫类对象时就发出猫类的叫声,传入犬类对象时就发出犬类的叫声。在同一个方法中,这种由于参数类型不同而导致执行效果各异的现象就是多态。

在Java中为了实现多态,允许使用一个父类类型的变量来引用一个子类类型的对象,根据被引用子类对象特征的不同,得到不同的运行结果。接下来通过一个案例来演示。

//定义接口Anmal
interface Animal {
    void shout(); // 定义抽象shout()方法
}
// 定义Cat 类实现Animal 接口
class Cat implements Animal {
    // 实现shout()方法
    public void shout() {
        System.out.println("喵喵……");
    }
}
// 定义Dog 类实现Animal 接口
class Dog implements Animal {
    // 实现shout()方法
    public void shout() {
        System.out.println("汪汪……");
    }
}
// 定义测试类
public class Example {
    public static void main(String[] args) {
        Animal an1 = new Cat(); // 创建Cat 对象,使用Animal 类型的变量an1 引用
        Animal an2 = new Dog(); // 创建Dog 对象,使用Animal 类型的变量an2 引用
        animalShout(an1); // 调用animalShout()方法,将an1 作为参数传入
        animalShout(an2); // 调用animalShout()方法,将an2 作为参数传入
    }
    // 定义静态的animalShout()方法,接收一个Animal 类型的参数
    public static void animalShout(Animal an) {
        an.shout(); // 调用实际参数的shout()方法
    }
}

运行结果:

喵喵……
汪汪……

第25行、第26行代码实现了父类类型变量引用不同的子类对象,当第27行、第28行代码调用animalShout()方法时,将父类引用的两个不同子类对象分别传入,结果打印出了“喵喵”和“汪汪”。由此可见,多态不仅解决了方法同名的问题,而且还使程序变得更加灵活,从而有效地提高程序的可扩展性和可维护性。

对象的类型转换

在多态的学习中,涉及到将子类对象当作父类类型使用的情况,例如下面两行代码:

Animal an1=new Cat(); //将Cat 对象当作Animal 类型来使用
Animal an2=new Dog(); //将Dog 对象当作Animal 类型来使用

将子类对象当作父类使用时不需要任何显式地声明,需要注意的是,此时不能通过父类变量去调用子类中的某些方法,接下来通过一个案例来演示。

//定义Animal 接口
interface Animal {
    void shout(); // 定义抽象方法shout()
}
// 定义Cat 类实现Animal 接口
class Cat implements Animal {
    // 实现抽象方法shout()
    public void shout() {
        System.out.println("喵喵……");
    }
    // 定义sleep()方法
    void sleep() {
        System.out.println("猫睡觉……");
    }
}
// 定义测试类
public class Example {
    public static void main(String[] args) {
        Cat cat = new Cat(); // 创建Cat 类的实例对象
        animalShout(cat); // 调用animalShout()方法,将cat 作为参数传入
    }
    // 定义静态方法animalShout(),接收一个Animal 类型的参数
    public static void animalShout(Animal animal) {
        animal.shout(); // 调用传入参数animal 的shout()方法
        animal.sleep(); // 调用传入参数animal 的sleep()方法
    }
}

编译程序报错

在main方法中,调用animalShout()方法时传入了Cat类型的对象,而方法的参数类型为Animal类型,这便将Cat对象当作父类Animal类型使用。当编译器检查到第29行代码时,发现Animal类中没有定义sleep()方法,从而出现编译程序报错的错误信息,报告找不到sleep()方法。由于传入的对象是Cat类型,在Cat类中定义了sleep()方法,通过Cat类型的对象调用sleep()方法是可行的,因此可以在animalShout()方法中将Animal类型的变量强转为Cat类型。将animalShout()方法进行修改,具体代码如下:

public static void animalShout(Animal animal) {
    Cat cat = (Cat) animal; //将animal 对象强制转换为Cat 类型
    cat.shout(); //调用cat 的shout()方法
    cat.sleep(); //调用cat 的sleep()方法
}

修改后再次编译,程序没有报错,运行结果:

喵喵……
猫睡觉……

Object类

在JDK中提供了一个Object类,它是所有类的父类,即每个类都直接或间接继承自该类。先来看一个例子。

//定义Animal 类
class Animal {
    // 定义动物叫的方法
    void shout() {
        System.out.println("动物叫!");
    }
}
// 定义测试类
public class Example {
    public static void main(String[] args) {
        Animal animal = new Animal(); // 创建Animal 类对象
        System.out.println(animal.toString()); // 调用toString()方法并打印
    }
}

运行结果:

Animal@15db9742

在例中,第13行代码调用了Animal对象的toString()方法,虽然Animal类并没有定义这个方法,但程序并没有报错。这是因为Animal默认继承自Object类,在Object类中定义了toString()方法,在该方法中输出了对象的基本信息。

Object类的toString()方法中的代码具体如下:

getClass().getName()+"@"+Integer.toHexString(hashCode());

为了方便初学者理解上面的代码,接下来分别对其中用到的方法进行解释,具体如下:

  • getClass().getName()代表返回对象所属类的类名,即Animal。
  • hashCode()代表返回该对象的哈希值。
  • Integer.toHexString(hashCode())代表将对象的哈希值用16进制表示。

其中,hashCode()是Object类中定义的一个方法,这个方法将对象的内存地址进行哈希运算,返回一个int类型的哈希值。

在实际开发中,通常希望对象的toString()方法返回的不仅仅是基本信息,而是一些特有的信息,这时重写Object的toString()方法便可以实现。

//定义Animal 类
class Animal {
    // 重写Object 类的toString()方法
    public String toString() {
        return "I am an animal";
    }
}
// 定义测试类
public class Example {
    public static void main(String[] args) {
        Animal animal = new Animal(); // 创建Animal 对象
        System.out.println(animal.toString()); // 打印animal 的toString()方法的返回值
    }
}

运行结果:

I am an animal

在例子的Animal类中重写了Object类的toString()方法,当在main()方法中调用toString()方法时,就打印出了Animal类的描述信息“Iamananimal”。