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
类的子类 subclass,Animal
类是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");
}
}
扩展可以使的子类继承父类的所有成员,包括:
- 所有实例和静态变量
- 所有方法
- 所有嵌套类
构造函数不能被继承!
构造函数
构造函数不可继承。但是,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
方法的内容。