1.extends

前面我们学习的继承是从interface继承,使用implements关键字来定义与接口的层次关系,现在我们希望可以继承class,使用extends关键字定义类的层次关系。
比如现在有一个class RotatingSLList,是继承class SLList
image.png
我们可以在class声明中设置这种继承关系,使用extends关键字如下:

public class RotatingSLList<Item> extends SLList<Item>

即定义了"is-a"的关系

该RotatingSLList除了能使用SLList的全部method之外,其额外作用是将元素右旋(元素之间顺序不变):
比如[5 10 15 20],以20为枢轴,右旋其左边所有元素后变成[20 5 10 15]实现方法:

public void rotateRight() {
    Item x = removeLast();
    addFirst(x);
}

通过使用extends关键字,子类继承父类的所有成员。“成员”包括:

  • 所有实例和静态变量
  • 所有方法method
  • 所有嵌套类

注意构造函数不是继承的,子类不能直接访问父类的私有成员(private)。
除此之外,子类还可以自定义一些其他的method,变量等等,也可以Override属于父类的method

super关键字

通过super关键字,可以让子类使用父类中的方法,使用 super. 访问,比如某子类使用并Override SLList中的removeLast()方法:

    @Override
    public Item removeLast() {
        Item x = super.removeLast();
        deletedItems.addLast(x);
        return x;
    }

构造函数非继承

正如我们前面提到的,子类继承父类,其中包括实例和静态变量,方法和嵌套类的所有成员,但不包括构造函数。当我们给子类写构造函数初始化时,需要先考虑父类的构造函数,一个形象的例子是,假设我们有两个类:

public class Human {...}
public class TA extends Human {...}

如果我们运行下面的构造函数:

TA(){
somebody = new TA();
}

那么首先必须创造一个人类。然后该人类才可以被赋予 TA 的品质。如果不先创建人类,就构建 TA 则毫无意义。
因此,子类在构造函数初始化之前需要先调用父类的构造函数,使用super():

TA() {
    super();
    somebody = new TA();
}

假设你在子类构造函数初始化的时候并没有加super(),Java会隐式地帮你自动先调用父类的构造函数,但是它只会调用无参数的版本,比如:

public VengefulSLList(Item x) {
    deletedItems = new SLList<Item>();
}

尽管子类的构造函数是含参数版本,由于没有调用super(x),Java仍会自动调用父类的无参数版的构造函数,要想正确调用父类含参的构造函数:

public VengefulSLList(Item x) {
    super(x);
    deletedItems = new SLList<Item>();
}

请注意
Java不允许使用 super.super,请查看this link

Java 中的每个class都是 Object class或extends Object class的后代。也就是说每个类都继承了Object class:
Class Object is the root of the class hierarchy. Every class has Object as a superclass. All objects, including arrays, implement the methods of this class.

public class A extends Object extends B(){......}

因此在任何类中都可以调用Object class中存在的一些method:
.equals(Object obj), .hashCode(), and toString()
see more detaile
但是interface 并没有 extends Object class
see more detail


2.封装

(本段是精校Google翻译)
封装是面向对象编程的基本原则之一,也是程序员用来抵御最大敌人--复杂性 的方法之一。管理复杂性是我们在编写大型程序时必须面临的主要挑战之一。

对抗复杂性的一些工具主要包括层次抽象(抽象障碍!)和一个被称为“设计变革”的概念。围绕这个想法,程序应该构建成模块化的、可交互的部分,即在不破坏系统的情况下与外界进行交换。此外,隐藏其他人不需要的信息是管理大型系统时的另一种基本方法。

封装的根源在于“向外部隐藏信息”的概念。以细胞来解释封装,细胞的内部结构可能极其复杂,由染色体、线粒体、核糖体等组成,但它却完全封装在一个模块中——抽象掉了内部的复杂性。
image.png
在计算机科学术语中,一个模块可以定义为一系列方法,它们作为一个整体协同工作以执行一个任务或一组相关任务。这可能类似于表示List的class。现在,如果模块的实现细节在内部被隐藏,并且与其交互的唯一方法是通过接口文档,那么该模块则被称为封装。


3.Implementation Inheritance如何打破封装

假设我们有一个封装好的Dog interface,包含:

  • bark()
  • barkMany()

其函数实现如下图所示,现在有一个子类VerboseDog,通过implementation继承Dog,并Override barkMany()函数:
image.png
现在调用VerboseDog的barkMany(),步骤是:

  1. VerboseDog d = new VerboseDog(); complier类型(静态类型)与runtime类型(动态类型)均是VerboseDog;
  2. d.BarkMany(3) 考虑Dynamic selection(出现Override均考虑Dynamic Selection),由于此时的runtime类型是VerboseDog,调用子类Override的BarkMany()
  3. -->调用bark(),此时的runtime类型是VerboseDog,准备调用子类的Override bark(),由于子类中没有bark()方法,使用继承父类的bark()方法
  4. 打印三次 bark

假设管理者改变了Dog类内部的函数实现,但是在外部看来,其函数作用与原来仍相同:

image.png

在这种情况下,仍然按照之前的步骤调用子类的barkMany():

  1. VerboseDog d = new VerboseDog(); complier类型(静态类型)与runtime类型(动态类型)均是VerboseDog;
  2. d.BarkMany(3) 考虑Dynamic selection,由于此时的runtime类型是VerboseDog,调用子类Override的BarkMany()
  3. -->调用bark(),此时的runtime类型是VerboseDog,准备调用子类的Override bark(),由于子类中没有bark()方法,使用继承父类的bark()方法
  4. 问题的关键来了,此时父类的bark()是
public void bark() {
barkMany(1);
}

也就是调用父类的bark()--->调用barkMany(1),由于此时的runtime type是VerboseDog,barkMany(1)实际是要调用子类的Override barkMany(),其中1作为参数传入子类的barkMany()

@Override
public void barkMany(int N) {
    System.out.println("As a dog, I say: ");
    for (int i = 0; i < N; i += 1) {
           bark();
    }
}

然后子类的barkMany()调用父类的bark(),父类的bark()又再次调用子类的barkMany()......
从而造成无限循环!


4.类型检查与强制类型转换(Casting)

回忆一下dynamic selection,dynamic selection即依据对象的动态类型,确定在运行过程中该对象所执行的方法。例如 ,某个对象的runtime type是VengefulSLList,并且VengefulSLList class是SLList class的子类,如果SLList的某个方法被VengefulSLList Override,则调用子类的该方法。
小练习
image.png

  • Does that line cause a compilation error?
  • Which method uses dynamic selection?

让我们逐行检测这段代码

VengefulSLList<Integer> vsl = new VengefulSLList<Integer>(9);
SLList<Integer> sl = vsl;

这两行没什么问题,因为VengefulSLList是SLList的子类,与SLList是"is-a"关系,即VengefulSLList一定是属于SLList,因此sl的SLList类型的内存盒可以容纳VengefulSLList类型

sl.addLast(50);
sl.removeLast();

这两行也没问题,当sl调用addLast(50)时,runtime type是VengefulSLList,但是VengefulSLList并没有Override addLast(),因此调用SLList自己的addLast()
对于sl.removeLast(),由于已经被子类Override(上文有提到),因此调用的是VengefulSLList的removeLast()

sl.printLostItems();

这行代码会编译错误,请注意 编译器在运行之前(也就是runtime之前)决定该语句是否有效是依据静态类型(compile-time types 等同于 declared type)来判断的,由于sl的静态类型是SLList,而SLList中并没有printLostItems()方法,因此直接报错,不会进入到dynamic selection

VengefulSLList<Integer> vsl2 = sl;

同时这行代码也会编译错误,总的来说,编译器在检查方法调用与赋值操作时会参考对象的静态类型( the compiler only allows method calls and assignments based on compile-time types )

由于vsl2的静态类型是VengefulSLList,而sl的静态类型是SLList,根据"is-a"的关系,
VengefulSLList是SLList,但SLList不一定是VengefulSLList,因此,编译器不允许VengefulSLList的内存盒存放SLList类型的对象。

强制类型转换

方法调用的complier time type(static type)与其声明的类型相同。假设我们有这个方法

public static Dog maxDog(Dog d1, Dog d2) { ... }

因为maxDog的返回类型是Dog,因此任何对maxDog()的调用均拥有静态类型Dog

Poodle frank = new Poodle("Frank", 5);
Poodle frankJr = new Poodle("Frank Jr.", 15);

Dog largerDog = maxDog(frank, frankJr);
Poodle largerPoodle = maxDog(frank, frankJr); //does not compile! RHS has compile-time type Dog

上述代码的想法是返回两只贵宾犬中较大的一只,但是第四行,Poodle与Dog不是"is-a"关系,也就是并非所有Dog都是Poodle,因此会编译错误,事实上,我们知道,无论是frank还是frankJr谁更大,最终返回结果一定是Poodle,那么有没有什么办法可以解决呢?
答案是使用强制类型转换
强制类型转换是一个强有力同时危险的工具,本质上,强制类型转换即绕开编译器本身的类型检查,让编译器相信我们的判断是正确的,just trust me.
危险之处在于:

Poodle frank = new Poodle("Frank", 5);
Malamute frankSr = new Malamute("Frank Sr.", 100);

Poodle largerPoodle = (Poodle) maxDog(frank, frankSr); // runtime exception!

假设我们对贵宾犬frank和雪橇犬frankSr做大小比较,并将最终的结果强制类型转换为Poodle(贵宾犬),但事实上却是Malamute > Poodle,当maxDog在运行时返回 Malamute 并且我们尝试将 Malamute 转换为 Poodle 时,我们就会遇到 ClassCastException 异常


5.高阶函数

Higher Order Function: 将其他函数当作参数和数据的函数
例如,在Python中:

def tenX(x):
    return 10*x
 
def do_twice(f, x):
    return f(f(x))
 
print(do_twice(tenX, 2))

do_twice(f,x)将tenX作为参数,执行了tenX(tenX(2)),返回200
如何在Java中实现高阶函数?
在老版本中(Java 7或更早),内存盒并不能容纳指向函数的指针,这也就意味着函数本身不能作为参数传入进而被高阶函数内的内存盒接收
解决方法是考虑使用接口继承来替代:
定义一个IntUnaryFunction接口,声明函数apply()

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

定义子类class TenX,继承该接口:

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

以上两个java文件即实现了python中的:

def tenX(x):
    return 10*x

我们知道Java中一个.java文件可以使用其他.java文件,那些文件相当于封装好的一个个对象,在本.java文件中使用,有点像C语言的typedef struct结构体,封装好的类相当于一种新的"数据类型",我们可以在本.java文件中使用其他类的非private成员,method等等
因此,想要实现函数套函数的功能,需要将包含apply()实现的TenX class封装,进而作为内存盒可容纳的参数(Object当然可以被内存盒容纳啦)在函数之间传递。
继续创建一个新的.java文件,HoFDemo class:

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));
    }
}

将IntUnaryFunction实例化为f,即可作为参数在函数(method)中传递,解决了函数不能作为参数在Java7中传递的问题,进而就可以使用 f.apply(f.apply(x))完成python中的高阶函数的功能

Java 8

在Java 8之后,一种新的类型可以容纳对method的引用

public class Java8HoFDemo {
    public static int tenX(int x) {
           return 10*x;
    }
    public static int doTwice(Function<Integer, Integer> f, int x) {
         return f.apply(f.apply(x));
    }
    public static void main(String[] args) {
        int result = doTwice(Java8HoFDemo::tenX, 2);
         System.out.println(result);
    }
}

Fallenpetals
4 声望9 粉丝