Home
11/18/2024

Java 继承机制的笔记_1


笔记的来源:CS 61B-2024 春季的课程 课程主要内容:数据结构与算法分析 课程运用语言:Java

这个课有6 个 Homework,10 个 Lab,9 个 Project。其中第一个 project 是一个完整的 2024 游戏的实现,很有意思。此文章对应的是课程 8-9 节的内容。 由于内容较多,还有 10-11 节的内容写在楼下一篇文章中。

此笔记对应资源:CS 61B 课本资源

上位词,下位词

在语言学中,上下位词的概念用于形容词的关系。比如红色的上位词可以是颜色。在 java 中,这种关系被用于形容类之间的继承关系。

比如说我定义了一个类Animal,它有一些共同的属性和方法,比如说叫做“吃”,“睡觉”,“跑”。然后我定义了一个类Dog,它继承了Animal的属性和方法,并添加了一些狗独有的属性和方法,比如说“抱”,“拉”,“摇”。Dog类可以认为是Animal类的子类。

这里称Dog类是Animal类的子类 subclassAnimal类是Dog类的超类 superclass

在这里我们先介绍接口继承的概念。

接口继承

public interface Animal {
    public void eat();
    public void sleep();
    public void run();
}

在上面的例子当中,可以称Animal类为接口,它本质上是一个契约,指定动物类能有什么行为。接下来我们用定义关系的关键词implements来建立一个Dog类,继承Animal接口。

public class Dog implements Animal {
    @Override
    public void eat() {
        System.out.println("Dog is eating");
    }
    ...
}

默认方法 Default Method

如果在接口中定义了一个方法,如:

public interface Animal {
    public void eat(){
        System.out.println("Animal is eating");
    };
}

那么系统会报错Interface methods cannot have body,因为接口方法不能有方法体。

为了解决这个问题,Java 8 引入了默认方法的概念。默认方法可以有方法体,可以被子类继承,也可以被实现类实现。需要加上default关键字。

public interface Animal {
    public default void eat() {
        System.out.println("Animal is eating");
    }
}

覆盖

在子类中实现所需函数时,@Override在方法签名的前面,用来表示覆盖父类中的默认方法。

其实不加这个标签,依然可以实现对父类方法的覆盖。这个标签的一大作用,便是对拼写错误的检查,如果你添加了@Override,但是对应的方法名称在父类的接口中不存在,编译器会报错。

静态类型以及动态类型

在 Java 中,每一个变量都有一个静态类型和一个动态类型。静态类型是在编译时确定的,而动态类型是在运行时确定的。

比如说:

Animal animal ;

在上面的代码中,animal的静态类型是Animal,而它的动态类型是null,因为还没有给它赋值。

Animal animal = new Dog();
animal = new Cat();

在上面的代码中,animal的静态类型是Animal,而它的动态类型是Dog。而且我们可以改变animal的动态类型,但是不能改变它的静态类型。

Extends关键词

当我们继承一个接口的时候我们使用的关键词是implements,但是当我们继承一个类而不是继承接口的时候我们使用的关键词是extends--扩展。

public class Puppy extends Dog{
    public void play(){
        System.out.println("Puppy is playing");
    }
}

扩展可以使的子类继承父类的所有成员,包括:

  1. 所有实例和静态变量
  2. 所有方法
  3. 所有嵌套类

构造函数不能被继承!

构造函数

构造函数不可继承。但是,Java 规则规定,所有构造函数都必须从调用超类的构造函数之一开始。可以使用关键字super明确调用构造函数。如果您没有明确调用构造函数,Java 将自动为您执行该操作。

下面的代码等价:

public class Puppy extends Dog{
    public Puppy(){
        super();
        puppy_a = new Dog();
    }
}
public class Puppy extends Dog{
    public Puppy(){
        puppy_a = new Dog();
    }
}

但是,如果父类构造函数有参数,则子类构造函数必须调用父类构造函数,并传入相应的参数。

下面两段代码就不一样了:

public class Puppy extends Dog{
    public Puppy(String name){
        super(name);
        puppy_a = new Dog();
    }
}
public class Puppy extends Dog{
    public Puppy(){
        super();
        puppy_a = new Dog();
    }
}

Object

所有类的祖先都是Object类,它是所有类的父类。Object类中定义了一些方法,如:equals()hashCode()toString()等。具体文档查看Object 类Object类声明了这些方法:

String toString()//返回对象的字符串表示
boolean equals(Object obj)//判断两个对象是否相等
int hashCode()//返回对象的哈希码
Class<?> getClass()//返回对象的类
protected Object clone()//创建并返回对象的浅拷贝
protected void finalize()///在垃圾回收器将对象从内存中清除之前调用
void notify()//唤醒一个正在等待对象的线程
void notifyAll()//唤醒所有正在等待对象的线程
void wait()//等待对象的通知
void wait(long timeout)//等待对象的通知,最长时间为timeout毫秒
void wait(long timeout, int nanos)//等待对象的通知,最长时间为timeout毫秒和nanos纳秒

IS-A 关系和 HAS-A 关系

这两种关系用来描述的是类和对象之间彼此的两种基本关系。

IS-A 关系

  • 一个类是另一个类的子类,或者说,它是另一个类的一种。
  • 例如,Dog类是Animal类的子类。

HAS-A 关系

  • 一个类包含另一个类的实例变量,或者说,它是一个类的组成部分。
  • 例如,Dog类包含一个name变量,表示狗的名字。

在这里extends方法只运用于 IS-A 关系。

类型检查和类型转换

Animal animal = new Dog();

在上面的代码中,animal的静态类型是Animal,而它的动态类型是Dog。我们称含有 new 的类型声明为运行时类型(动态类型),而不含有 new 的类型声明为编译时类型(静态类型)

一个重要的性质就是,animal可以使用Dog类的任何方法,因为Dog类是Animal类的子类。但是如果使用Dog类中新添加而不是Animal类中定义的方法,则会出现编译错误

如果将上面等号两边反过来,则会发生类型检查错误:

Dog dog = new Animal();

在上面的代码中,dog的静态类型是Dog,而它的动态类型是Animal。这时编译器会报错,因为Animal不是Dog的子类。


假如我们有一个方法 oldestAnimal(),用来比较两个动物的年龄,他的类型是Animal

public Animal oldestAnimal(Animal a1, Animal a2){...}

那么我们就不能这么写:

Dog dog1 = new Dog();
Dog dog2 = new Dog();
Dog oldestDog = oldestAnimal(dog1, dog2);

因为oldestAnimal()方法传出的参数类型是Animal,而 oldestDog 的静态类型是Dog,所以编译器会报错。这个时候我们可以利用类型转换

Dog oldestDog = (Dog) oldestAnimal(dog1, dog2);

高阶函数

下面展示如何用复杂的 java 实现此简洁的 python 代码 : )

def tenX(x):
   return 10*x

def do_twice(f, x):
   return f(f(x))

print(do_twice(tenX, 2))

java 实现:

public interface IntUnaryFunction {
	int apply(int x);
}

public class TenX implements IntUnaryFunction {
	public int apply(int x) {
   		return 10 * x;
	}
}

public class HoFDemo {
	public static int do_twice(IntUnaryFunction f, int x) {
   		return f.apply(f.apply(x));
	}

	public static void main(String[] args) {
   		System.out.println(do_twice(new TenX(), 2));
	}
}

可以看出来,这里运用了一个apply方法作为中间过渡,看起来是把函数当成变量,实则是改变了apply方法的内容。