LOADING

加载过慢请开启缓存 浏览器默认开启

软件工程与计算I笔记

let 关键字在 Lisp/Scheme(图中所示为类似 Scheme 的语法)中用于创建局部变量绑定


在图中代码的作用:

(define (f x y)
  (let ((a (+ 1 (* x y)))
        (b (- 1 y)))
    (+ (* x (square a))
       (* y b)
       (* a b))))

这段代码的意思是:

  1. 定义一个函数 f,带两个参数 xy
  2. 在函数体中,使用 let 定义两个局部变量
    • a 被绑定为 (+ 1 (* x y))
    • b 被绑定为 (- 1 y)

(define (make-rat n d) (cons n d))

  • 作用:创建一个有理数,由分子 n 和分母 d 构成。

  • 解释:调用 make-rat 时,会返回一个由 cons 构造的二元对(pair)。

    scheme
    
    
    复制编辑
    (make-rat 3 4)  ; 返回 (3 . 4)
    
  • cons 是用来构造 pair 的函数。


2. (define (numer x) (car x))

  • 作用:从有理数 x 中提取分子

  • 解释

    • car 返回 pair 的第一个元素(即 n)。

    • 举例:

      scheme
      
      
      复制编辑
      (numer (make-rat 3 4)) ; 返回 3
      

3. (define (denom x) (cdr x))

  • 作用:从有理数 x 中提取分母

  • 解释

    • cdr 返回 pair 的第二个元素(即 d)。

    • 举例:

      scheme
      
      
      复制编辑
      (denom (make-rat 3 4)) ; 返回 4
      

原始表达式:

LENGTH (CONS a NIL)

我们想要计算列表 CONS a NIL 的长度,即一个只有一个元素的列表。


第一步:

= Y (\g.\c.\x. NULL x c (g (SUCC c) (CDR x))) ZERO (CONS a NIL)

解释:

  • YY combinator,用于支持 λ-演算中的递归定义。
  • 这里定义了一个递归函数 g,其主体是 (\c.\x. NULL x c (g (SUCC c) (CDR x)))
    • 参数 c 是当前累加器(用于计数)。
    • 参数 x 是当前列表。
    • 如果 x 是空(NULL x),就返回 c(即当前计数),否则递归处理尾部 CDR x,并将 c 增加(SUCC c)。
  • 整体是:从 c = ZERO 开始,作用于 CONS a NIL

第二步:

= (\g.\c.\x. NULL x c (g (SUCC c) (CDR x))) (Y (\g.\c.\x. NULL x c (g (SUCC c) (CDR x)))) ZERO (CONS a NIL)

解释:

  • Y f 展开为 f (Y f)
  • 所以 g 被替换为 Y (\g...\x...) 本体。

第三步:

= NULL (CONS a NIL) ZERO ((Y (\g.\c.\x. NULL x c (g (SUCC c) (CDR x)))) ONE NIL)

解释:

  • (\c.\x. NULL x c (g (SUCC c) (CDR x))) 被应用于 ZEROCONS a NIL
  • 然后进行应用化简:
    • 判断 NULL (CONS a NIL),由于 CONS a NIL 是非空,NULL 返回 false
    • 因此返回的是递归调用:(Y ...) (SUCC ZERO) (CDR (CONS a NIL))
      • SUCC ZERO = ONE
      • CDR (CONS a NIL) = NIL

第四步:

= Y (\g.\c.\x. NULL x c (g (SUCC c) (CDR x))) ONE NIL
  • 正在进行下一轮递归,此时参数变成 c = ONEx = NIL

第五步:

= ONE

解释:

  • x = NIL,所以 NULL x 为 true,返回 c,即 ONE。

image-20250531152504252image-20250531152511152

重载:在⼀个类里面,不同的方法,恰好名字相同

JVM Run-Time Data Areas

• The pc Register

• Java Virtual Machine Stacks

• Heap

• Method Area

• Run-Time Constant Pool

• Native Method Stacks

引用类型变量对Java对象访问的实现

访问数据

• 实例数据值(对象中各个实例字段的数据)

• 对象类型数据(对象类型、⽗类、实现的接⼝、⽅法等)

访问⽅式

• 句柄访问⽅式

• 直接指针访问⽅式

字节码

JVM方法调用时栈帧变化示例代码

public class TestDemo { 
	public static int minus(int x){ 
		return -x; 
	} 

	public static void main(String[] args) { 
		int x = 5; 
		int y = minus(x); 
	} 
}

⾸先检查main的访问标志、描述符描述的返回类型和参数列表,确定可以访问后进⼊Code属性表执⾏命令,读⼊栈深度建⽴符合要求的操作数栈,读⼊局部变量⼤⼩建⽴符合要求的局部变量表,根据参数数向局部变量表中依序加⼊参数(第⼀个参数是引⽤当前对象的this,所以空参数列表的参数数也是1),然后开始根据命令正式执⾏:

• 0: iconst_5

• 将整数5压⼊栈顶

• 1: istore_1

• 将栈顶整数值存⼊局部变量表的slot1(slot0是参数String[] args指向的对象的地址)

• 2: iload_1

• 将slot1压⼊栈顶

• 3: invokestatic #2 // Method minus:(I)I

• ⼆进制invokestatic⽅法⽤于调⽤静态⽅法,参数是根据常量池中已经转换为直接引⽤的常量,意即minus函数在⽅法区中的地址,找到这个地址调⽤函数,向其中加⼊的参数为栈顶的值

• 6: istore_2

• 将栈顶整数存⼊局部变量的slot2

• 7: return

• 将返回地址中存储的PC地址返到PC,栈帧恢复到调⽤前

现在我们分析调⽤minus函数的时候进⼊minus函数的过程:

• 同样的⾸先检查minus函数的访问标志、描述符描述的返回类型和参数列表,确定可以访问后进⼊Code属性表执⾏命令,读⼊栈

深度建⽴符合要求的操作数栈,读⼊局部变量⼤⼩建⽴符合要求的局部变量表,根据参数数向局部变量表中依序加⼊参数,然后

开始根据命令正式执⾏:

• 0: iload_0

• 将slot0压⼊栈顶,也就是传⼊的参数

• 1: ineg

• 将栈顶的值弹出取负后压回栈顶

• 2: ireturn

• 将返回地址中存储的PC地址返到PC,栈帧恢复到调⽤前

结构化编程的缺点

Not easy to read

Not easy to maintain

面向对象

对象是面向对象中的术语,既表示客观世界问题空间(Namespace)中的某个具体的事物,又表示软件系统解空间中的基本元素。

在软件系统中,对象具有唯—的标识符,对象包括属性(Properties)和⽅法(Methods),属性就是需要记忆的信息,方法就是对象能够提供的服务。

是—个描述或蓝图(被表示成—段代码),⽤于定义组成某类特定对象的所有特性。

编程中使⽤类的思想与现实世界中把东⻄进⾏分类的思想相—致,这是—种⽅便⽽明确的事物组织⽅式。

**类是对某个对象的定义。**它包含有关对象动作⽅式的信息,包括它的名称、⽅法、属性和事件。

当引⽤类的代码运⾏时,类的—个新的实例,即对象,就在内存中创建了。虽然只有—个类,但能从这个类在内存中创建多个相同类型的对象。

创建类的原因

• 对现实世界中的对象建模

• 对抽象对象建模

• 限制变化所影响的范围

• 降低复杂度

• 创建中⼼控制点

• 隔离复杂度

• 隐藏实现细节

JVM

类加载器:JVM 需要将编译后的字节码文件加载到其内部的运行时数据区域中进行执行。这个过程涉及到了 Java的类加载机制(双亲委派机制)。

运行时数据区:JVM 定义了 Java 程序运行期间需要使用到的内存区域,简单来说,这块内存区域存放了字节码信息以及程序执行过程的数据,垃圾收集器也会针对运行时数据区进行对象回收的工作

执行引擎:将字节码指令解释/编译为对应平台上的本地机器指令以。简单来说,JVM 中的执行引擎充当了将高级语言翻译为机器语言的译者。

类加载

加载、验证、准备、解析、初始化、使用和卸载。

1)Loading(载入):将字节码从不同的数据源转化为二进制字节流加载到内存中,并生成一个代表该类的 java.lang.Class 对象。

2)Verification(验证):对二进制字节流进行校验

3)Preparation(准备):对类变量(也称为静态变量,static 关键字修饰的)分配内存并初始化。

4)Resolution(解析):该阶段将常量池中的符号引用转化为直接引用。

5)Initialization(初始化):类变量将被赋值为代码期望赋的值。换句话说,执行类构造器方法的过程。

运行时数据区

方法区、堆、虚拟机栈、本地方法栈以及程序计数器(PC寄存器)

• PC寄存器:当前运行方法的字节码指令地址。

• 本地方法栈:运行本地方法(java代码调用其它语言)。Native方法参数、局部变量。

• 虚拟机栈(栈):存储方法调用和局部变量。栈帧:包括方法参数、返回地址、操作数栈等。

• 堆:存储Java运行时所有的对象实例。对象实例、数组

• 方法区:存储类信息、常量、静态变量等。

执行引擎

解释器:读取字节码,然后执行指令。因为它是一行一行地解释和执行指令,所以它可以很快地解释字节码,但是执行起来会比较慢(毕竟要一行执行完再执行下一行)。

即时编译器:执行引擎首先按照解释执行的方式来执行,随着时间推移,即时编译器会选择性的把一些热点代码编译成本地代码。执行本地代码比一条一条进行解释执行的速度快很多,因为本地代码是保存在缓存里的。

垃圾回收器,用来回收堆内存中的垃圾对象。

JVM运行java代码

源码编译生成字节码,启动JVM运行字节码文件,

访问标记

常量池之后的区域就是访问标记(Access flags),这个标记用于识别类或接口的访问信息,比如说:

•到底是 class 类 还是 interface 接口?

•是 public 吗?

•是 abstract 抽象类吗?

•是 final 类吗?

类索引、父类索引和接口索引

这三部分用来确定类的继承关系,this_class 为当前类的索引,super_class 为父类的索引,interfaces 为接口。

字段表

一个类中定义的字段会被存储在字段表

方法表

方法表和字段表类似,区别是用来存储方法的信息,包括方法名,方法的参数,方法的签名。

小结

•class 文件是一串连续的二进制,由 0 和 1 组成,但我们仍然可以借助一些工具来看清楚它的真面目。

•class 文件的内容通常可以分为下面这几部分,魔数、版本号、常量池、访问标记、类索引、父类索引、接口索引、字段表、方法表、属性表。

•常量池包含了类、接口、字段和方法的符号引用,以及字符串字面量和数值常量。

•访问标记用于识别类或接口的访问信息,比如说是不是 public | private | protected,是不是 static,是不是 final 等。

•类索引、父类索引和接口索引用来确定类的继承关系。

•字段表用来存储字段的信息,包括字段名,字段的参数,字段的签名。

•方法表用来存储方法的信息,包括方法名,方法的参数,方法的签名。

•属性表用来存储属性的信息,包括字段的初始值,方法的字节码指令等。

运行时数据区

虚拟机栈操作的基本元素就是栈帧,栈帧主要包含了局部变量表、操作数栈、动态连接以及方法返回地址。栈帧是一个先进后出的数据结构,每个方法从调用到执行完成都会对应一个栈帧在虚拟机栈中入栈和出栈。

JVM将内存分为年轻代和老年代,其中年轻代又分为Eden区和两个Survivor区

•年轻代

• 用于存放新生的对象,其中Eden区是新对象的分配区域

• 当Eden区满时,会触发Minor GC,将存活的对象移动到Survivor区,同时清空Eden区

• Survivor区则用于存放经过一次Minor GC后存活的对象

•老年代

• 用于存放长时间存活的对象,当年轻代中的对象经过多次Minor GC后仍然存活,就会被移动到老年代

• 当老年代满时,会触发Full GC

字节码指令集

指令分类

• 指令可以基本分为以下几类:

• 存储指令 (例如:aload_0, istore)

• 算术与逻辑指令 (例如: ladd, fcmpl)

• 类型转换指令 (例如:i2b, d2i)

• 对象创建与操作指令 (例如:new, putfield)

• 堆栈操作指令 (例如:swap, dup2)

• 控制转移指令 (例如:ifeq, goto)

• 方法调用与返回指令 (例如:invokespecial, areturn)

前/后缀 操作数类型

• i整数

• l长整数

• s 短整数

• b 字节

• c 字符

• f单精度浮点数

• d 双精度浮点数

• z 布尔值

• a 引用

加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传输。

• 将一个局部变量加载到操作数栈的指令包括:iload,iload_n,lload、lload_n、fload、 fload_n、

dload、dload_n,aload、aload_n。

• 将一个数值从操作数栈存储到局部变量表的指令:

istore,istore_n,lstore,lstore_n,fstore,fstore_n,dstore,dstore_n,astore,astore_n

• 将常量加载到操作数栈的指令:

bipush,sipush,ldc,ldc_w,ldc2_w,aconst_null,iconst_ml,iconst_i,lconst_l,fconst_f,dconst_d

• 局部变量表的访问索引指令:wide

JVM直接就支持宽化类型转换(小范围类型向大范围类型转换):

• 1)int类型到long,float,double类型

• 2)long类型到float,double类型

• 3)float到double类型

• 但在处理窄化类型转换时,必须显式使用转换指令来完成,包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和 d2f。

面向对象:封装

封装

●封装规则1:

将数据和对该数据进行操作的行为放在同一个类中。

●封装规则2:

使用职责驱动设计来确定数据和行为在类中的组合方式。

●封装规则3:

职责应当是完整的。

关键字static允许一个方法在没有类实例的情况下运行。

静态变量初始化

●静态变量在类被加载时初始化。

●类之所以被加载,是因为JVM决定现在是加载它的时候。

●关于静态变量初始化,有两个规则

  1. 在创建该类的任何对象之前,类中的静态变量会被初始化,

  2. 在运行该类的任何静态方法之前,类中的静态变量也会被初始化。

被标记为final的变量在类中必须被初始化,被标记为final的方法不能被override,被标记为final的类不能被继承或产生子类
使用final方法的原因有两个。

第一个原因是把方法锁定,以预防任何继承类修改它的意义。这是出于设计的考虑,你想要确保在继承中方法行为保持不变,并且不会被重写。

使用final方法的第二个原因是效率。如果你将一个方法指明为final,就是同意编译器将针对该方法的所有调用都转为内嵌(inline)调用。

对象初始化初步
●变量会在任何方法被调用之前初始化,甚至在构造方法之前;

●静态数据先初始化,然后是非静态数据,

●静态数据的初始化和静态代码块的执行按照代码中的书写顺序进行。

字符串

以及str3 = str1 + str2str3 = "test" + "test"的区别

面向对象:协作

基本问题求解的原则:分解与抽象

面向对象方法的原则:职责与协作

面向对象方法的三要素:封装、继承、多态

类之间的关系

General Relationship

• 依赖

Instance Level Relationship

• 连接

• 关联

Class Level Relationship

• 继承

• 实现

在图书管理系统中

• 图书馆和图书

• 借阅记录和学生

• 图书和图书中的一页

• 图书馆和学生助手(拥有整理图书的行为)

  1. 图书馆和图书

    • 关联关系(Association)
      图书馆和图书之间是典型的关联关系,表示图书馆拥有(管理)多个图书。这种关系通常用成员变量(如List<Book>)实现,体现为”has-a”的关系。
      • 可能是单向关联(仅图书馆知道图书的存在)或双向关联(图书也可能持有图书馆的引用,但实践中较少见)。
      • 如果图书的生命周期独立于图书馆(如图书可以被转移到其他图书馆),则是普通关联;如果图书严格属于某个图书馆且不能独立存在,则可能是组合关系(Composition),但通常前者更常见。
  2. 借阅记录和学生

    • 关联关系(Association)
      借阅记录与学生之间是关联关系,表示一条借阅记录关联到一个学生。
      • 借阅记录类中可能包含一个Student类型的成员变量,体现”谁借了书”。
      • 如果借阅记录是学生的组成部分(如学生对象内部管理自己的借阅记录),则可能是组合关系(Composition),但通常借阅记录由系统统一管理,因此普通关联更合适。
  3. 图书和图书中的一页

    • 组合关系(Composition)
      图书和页之间是严格的组合关系,表示页是图书的一部分,生命周期完全由图书控制(”部分”不能脱离”整体”存在)。
      • 图书类可能包含一个List<Page>成员变量,且当图书被销毁时,所有页也应被销毁。
      • 体现为强”拥有”关系,符合组合的定义。
  4. 图书馆和学生助手(拥有整理图书的行为)

    • 依赖关系(Dependency)关联关系(Association)

      • 如果学生助手只是临时参与整理图书(如通过方法参数传递),则是依赖关系(临时性使用)。
        示例:

        public class Library {
            public void organizeBooks(StudentAssistant assistant) { ... }
        }
        
      • 如果学生助手是图书馆的固定成员(如作为图书馆的属性),则是关联关系
        示例:

        public class Library {
            private StudentAssistant assistant;
        }
        

面向对象:继承和多态

继承

• ⼦类继承了⽗类所有的成员变量和成员⽅法

• 可以增加成员变量和成员⽅法

• 可以覆盖⽗类的成员⽅法

• 不可以覆盖⽗类的成员变量

如果父类中的成员变量和方法被声明为 private,那么子类就无法直接使用它们。 除此之外,如果父类的成员变量和方法被声明为 protectedpublic,那么子类就可以直接使用它们。

⾮抽象类中不可以有抽象⽅法,抽象类中可以有⾮抽象⽅法

抽象⽅法的存在就是为了多态,具体的⼦类必须实现所有⽗类的抽象⽅法,实现抽象⽅法就像⼦类覆盖⽗类⽅法—样

编译时,编译器决定你是否能调⽤某个⽅法,依据引⽤变量的类型,⽽不是引⽤变量指向的对象的类型

执⾏时,JVM虚拟机决定实际哪个⽅法被调⽤,依据实际引⽤变量指向的对象的类型

Overriding的规则

• 条件:

• 在不同类(⽗类和⼦类)、同—个⽅法、⽅法名字相同

• 参数必须—致,返回值必须兼容

• 例如:⽗类返回Animal,⼦类返回dog

• ⽅法的可达性不能降低。

• 例如:⽗类是缺省的,⼦类是public

不能overriding一个private的方法!!!because a parent class’s private methods are “automatically final, and hidden from the derived class”.

Overloading的规则

• 条件:

同—个类中,不同⽅法,恰好名字相同

• 参数不同(类型、个数、顺序)

• 返回值可以不同,但是不能只是返回值不同

• ⽅法的可达性提⾼降低都⾏。

⽅法的调⽤

Java虚拟机⾥⾯提供四种⽅法调⽤字节码指令

• invokestatic:静态⽅法

• invokespecial:实例构造器方法,私有⽅法和⽗类⽅法

  • 用于调用无需多态的方法,包括:
    • 私有方法(private)。
    • 构造方法(<init>)。
    • 父类的非覆盖方法(通过super.method()调用)。
  • 采用静态绑定(编译时确定),直接调用指定类的方法,忽略子类覆盖。

• invokevirtual:虚⽅法

  • 用于调用实例方法(非私有、非静态、非构造方法),且目标方法是**普通类(非接口)**的方法。
  • 支持动态绑定(运行时多态),即实际调用的方法由对象的运行时类型决定(虚方法分派)。

• invokeinterface:接⼝⽅法,会在运⾏时再确定—个实现此接⼝的对象

  • 专门用于调用接口方法(通过接口引用调用实现类的方法)。
  • 支持动态绑定(类似invokevirtual),但优化方式不同(接口方法搜索更复杂)。

• invokedynamic: 先在运⾏时动态解析出调⽤点限定符所引⽤的⽅法,然后再执⾏该⽅法。把如何查找⽬标⽅法的决定权从虚拟机转嫁到具体⽤户代码之中,让⽤户(包含其他语⾔的设计者)有更⾼的⾃由度。

静态绑定:
在程序执行前方法已经被绑定,针对java简单的可以理解为程序编译期的绑定;
java当中的方法只有final,static,private和构造方法是前期绑定

动态绑定:
后期绑定:在运行时根据具体对象的类型进行绑定。

静态绑定 VS 动态绑定
1、静态绑定是发生在编译阶段;而动态绑定是在运行阶段;
2、private, final and static方法和变量使用静态绑定,而虚函数(virtual methods)则会根据运行时的具体对象进行绑定(注:在Java语言中, 所有的方法默认都是”虚函数”。只有以关键字 final 标记的方法才是非虚函数。)
3、静态绑定使用的是类信息,而动态绑定使用的是对象信息
4、重载方法(overloaded methods)使用的是静态绑定,而重写方法(overridden methods)使用的是动态绑定

overloading匹配优先级

• char 》int》long》Character》Serializable》Object》char…

对于样例代码:

public class HidingDemo {
    public static void main(String[] args) {
        A x = new B();
        System.out.println("(1) x.i is " + x.i);
        System.out.println("(2) (B)x.i is " + ((B)x).i);
        System.out.println("(3) x.j is " + x.j);
        System.out.println("(4) ((B)x).j is " + ((B)x).j);
        System.out.println("(5) x.m1() is " + x.m1());
        System.out.println("(6) ((B)x).m1() is " + ((B)x).m1());
        System.out.println("(7) x.m2() is " + x.m2());
        System.out.println("(8) x.m3() is " + x.m3());
    }
}

class A {
    public int i = 1;
    public static int j = 11;
    public static String m1() { return "A's static m1"; }
    public String m2() { return "A's instance m2"; }
    public String m3() { return "A's instance m3"; }
}

class B extends A {
    public int i = 2;
    public static int j = 12;
    public static String m1() { return "B's static m1"; }
    public String m2() { return "B's instance m2"; }
}

输出是:

(1) x.i is 1
(2) (B)x.i is 2
(3) x.j is 11
(4) ((B)x).j is 12
(5) x.m1() is A's static m1
(6) ((B)x).m1() is B's static m1
(7) x.m2() is B's instance m2
(8) x.m3() is A's instance m3

A x = new B(); 是 Java 中的 多态(Polymorphism) 的典型用法,它表示:

  1. A x:声明一个 编译时类型(声明类型)为 A 的变量 x
  2. new B():创建一个 运行时类型(实际类型)为 B 的对象。
  3. =:将 B 的实例赋值给 x,但 x访问权限受限于 A 类中定义的成员(除非发生多态)。

1. 编译时类型(A) vs 运行时类型(B)

  • 编译时类型(A):决定了变量能 访问哪些成员(方法、变量)(由 A 类的定义决定)。
  • 运行时类型(B):决定了实际调用 方法时的行为(如果方法被覆盖)。

2. 多态的表现

  • 实例方法(如 m2()):动态绑定,实际调用 B 类的方法(运行时决定)。

    x.m2(); // 输出 "B's instance m2"(因为 B 覆盖了 m2)
    
  • 静态方法/变量(如 m1(), j):静态绑定,按编译时类型 A 访问。

    x.m1(); // 输出 "A's static m1"(静态方法无多态)
    x.j;    // 输出 11(变量隐藏,按 A 的 j 访问)
    
  • 成员变量(如 i):无多态,按编译时类型 A 访问。

    x.i;    // 输出 1(变量隐藏,访问 A 的 i)
    

继承体系下的初始化流程

  1. 入口阶段:从Main()方法开始,先加载基类,递归向上直至最顶层的根基类
  2. 静态初始化:从根基类开始,依次向下执行各派生类的静态代码块(static{})和静态变量赋值
  3. 默认值设置:所有实例变量(基本类型初始化为0/0.0/false,对象引用初始化为null
  4. 构造函数调用:从根基类的构造函数开始逐层向下调用
  5. 实例变量初始化:按代码中的书写顺序初始化成员变量(显式赋值或实例代码块{}
  6. 构造函数执行:最后执行当前构造函数体内的剩余逻辑

关键点说明:

  • 静态优先:静态初始化(步骤2)在类加载时完成,仅执行一次。
  • 父类优先:实例化时始终先完成父类初始化(步骤4→5→6),再处理子类。
  • 顺序规则:实例变量按代码中的出现顺序初始化(步骤5),随后才执行构造函数内其他代码(步骤6)。

例如:

class Root {
    static { System.out.println("Root静态块"); }  // 步骤2
    int a = initA();                            // 步骤5
    Root() { System.out.println("Root构造器"); } // 步骤6
    int initA() { return 1; }
}
class Derived extends Root {
    static { System.out.println("Derived静态块"); } // 步骤2(子类)
    int b = 2;                                    // 步骤5
    Derived() { System.out.println("Derived构造器"); } // 步骤6
}
// 执行 new Derived() 时,输出顺序:
// Root静态块 → Derived静态块 → Root构造器 → Derived构造器

类的初始化

• 事实上,⼀个类的初始化包括3个步骤:

• 加载(Loading),由类加载器执⾏,查找字节码,并创建⼀个Class对象(只是创建);

• 链接(Linking),验证字节码,为静态域分配存储空间(只是分配,并不初始化该存储空间),解析该类创建所需要的对其它类的应⽤;

• 初始化(Initialization),⾸先执⾏静态初始化块static{},初始化静态变量,执⾏静态⽅法(如构造⽅法)。

可能造成类被初始化的操作

• 创建⼀个Java类的实例对象

• 调⽤⼀个Java类的静态⽅法

• 为类或接⼝中的静态域赋值

• 访问类或接⼝中声明的静态域,并且该域的值不是常值变量

• 在⼀个顶层Java类中执⾏assert语句

• 调⽤Class类和反射API中进⾏反射操作

对如下例子:

abstract class Glyph {
    abstract void draw();
    Glyph() {
        System.out.println("Glyph() before draw()");
        draw();
        System.out.println("Glyph() after draw()");
    }
}

class RoundGlyph extends Glyph {
    private int radius = 1;
    RoundGlyph(int r) {
        radius = r;
        System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);
    }
    void draw() {
        System.out.println("RoundGlyph.draw(), radius = " + radius);
    }
}
public class PolyConstructors {
    private static Test monitor = new Test();
    public static void main(String[] args) {
        new RoundGlyph(5);
    }
}

输出结果应为:

Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5

案例代码最终输出为null(因为动态绑定调用的是子类sub的callname,而子类的callname使用的basename是子类的basename,但是子类的basename还没有初始化,为null)

看PPT第13号,55页以后的例子!

例题1输出:

Main method starts
ClassB initialized:1
ClassA initialized
ClassA instance created
Main method ends

例题2输出:

父类 s1成员初始化
父类 s2成员初始化
父类 Father默认构造函数被调用
子类 s1成员初始化
子类 s2成员初始化
子类 Son默认构造函数被调用

例题3输出:

Main class initialized:2
Main method starts
ClassB initialized:1
ClassA initialized
ClassA instance created
Main method ends

错误原因分析

ClassB 中,以下代码会报错:

InterfaceA.super.foo(); // 错误

而以下代码是正确的:

InterfaceB.super.foo();

根本原因

  1. Java 接口默认方法的继承规则

    • 当一个类实现多个接口,且这些接口有相同的默认方法(如 foo()),必须显式覆盖该方法,否则会编译错误(除非其中一个接口是另一个的子接口)。
    • ClassB 中,InterfaceB 已经覆盖了 InterfaceAfoo() 方法,因此 InterfaceAfoo() 默认方法不再直接可见
  2. InterfaceX.super.foo() 的访问限制

    • 使用 InterfaceX.super.foo() 语法时,InterfaceX 必须是当前类直接实现的接口,且该接口的默认方法未被覆盖
    • ClassB 中:
      • InterfaceB 是直接实现的接口,且它覆盖了 foo(),因此可以调用 InterfaceB.super.foo()
      • InterfaceAfoo() 已被 InterfaceB 覆盖,因此不能通过 InterfaceA.super.foo() 直接访问。

为什么 InterfaceB.super.foo() 可以?

  • InterfaceBClassB 直接实现的接口,且它的 foo() 是当前类继承链中的最新覆盖版本,因此允许调用。

当接⼝继承⾏为发⽣冲突时的另⼀个规则是,类的⽅法声明优先于接⼝默认⽅法,⽆论该⽅法是具体的还是抽象的

异常

编译器不会检查RuntimeExceptions

在继承中重写方法时抛出异常的问题

规则1:异常范围一致性原则

子类重写父类方法时:

允许抛出与父类方法完全相同的受检异常(checked exception);

允许抛出父类方法声明异常的子类异常(更具体的异常);

允许不抛出任何受检异常(缩减异常范围);

禁止抛出比父类方法声明更宽泛或无关的受检异常。

非检查型异常(unchecked exceptions)(如 RuntimeException 及其子类)不受此规则限制,子类可自由抛出。

规则2: 多继承场景下的异常交集原则

若子类同时满足以下条件:

• 重写父类中声明异常的方法;

• 实现某接口中同名且声明异常的方法;

则子类重写方法的异常声明必须满足:

• 抛出父类声明异常与接口声明异常的交集;

• 或不抛出任何检查型异常。

实际输出:2