课程主页:

https://www.edx.org/course/compilers

课程视频:

https://www.bilibili.com/video/BV1NE411376V?p=20&t=6

备注:

图片均来自于课件。

这次回顾Java。

Java

  • Java:COOL的升级版
    • 同一时期出现
  • 包含
    • 数组
    • 异常(Exceptions)
    • 接口(Interfaces)
    • 强制(Coercions)
    • 线程
    • 动态加载和初始化
    • 总结

历史

  • Java出现在SUN公司
    • 最初针对机顶盒设备
    • 最初的开发花费了数年(91-94)
  • 后来重新定位为互联网语言(94-95)
    • 每种新语言都需要一个“杀手级应用”
    • 竞争对手是TCL,Python

一些语言比较

  • Modula-3
    • 类型
  • Eiffel,ObjectiveC,C ++
    • 面向对象,接口
  • Lisp
    • Java的动态风格(许多功能)

对比COOL

  • 从我们的角度来看,Java是COOL的增强版,增加了如下特性
    • 异常(Exceptions)
    • 接口(Interfaces)
    • 线程
    • 动态加载
    • 其他一些次要的特性
  • Java是一种大语言
    • 有很多功能
    • 许多功能交互

Java数组

问题

B[] b = new B[10];
A[] a = b;

a[0] = new A(); 
b[0].aMethodNotDeclaredInA();

假设B < A,接下来会发生什么?

分析

在Java中,

  • B < A,如果B从A继承
    • 和Cool中一样
  • C < A,如果C < B以及B < A
    • 和Cool中一样
  • B[] < A[],如果B < A
    • 和Cool中不同

回顾之前的代码

B[] b = new B[10];
A[] a = b;

a[0] = new A(); 
b[0].aMethodNotDeclaredInA();

在可更新位置使用不同类型的多个别名是不合理的!

  • 标准解决方法
    • 禁止通过数组进行子类型化
  • B < A,如果B从A继承
  • C < A,如果C < B以及B < A
  • B[] < A[],如果B = A

解决方法

  • Java通过在运行时检查每个数组分配的类型正确性来解决此问题
    • 分配的对象的类型与数组的类型兼容吗?
  • 这增加了数组计算的开销
  • 但请注意:基本类型的数组不受影响
    • 基本类型不是类

Java异常 (Exceptions)

  • 在一段代码的深处,您遇到意外错误,例如

    • 内存不足
    • 假设已排序的列表并没有排序
    • 等等。
  • 你应该做什么?

  • 添加新的异常类型(类)

  • 添加新形式

    try { something } catch(x) { cleanup }
    throw exception

例子

class Foo {
	public static void main(String[] args) {
		try { X(); } catch (Exception e) {
		System.out.println(Error!) }
	}
		
	public void X() throws MyException { 
		throw new MyException();
	}
}
  • $\mathrm{T(v)}=$ 抛出了值为$\mathrm v$的异常
  • $\mathrm{v =\ }$普通值(一个对象)

实现

  • 当我们遇到try
    • 在堆栈中标记当前位置
  • 当我们抛出exception
    • 将堆栈展开到第一个try
    • 执行相应的catch
  • 更复杂的技术降低了try和throw的成本

在对象完成期间引发的未捕获异常会发生什么情况?

异常类型

  • 方法必须声明它们可能引发的异常类型

    public void X() throws MyException
    • 在编译时检查
    • 某些异常不必是方法签名的一部分
      • 例如,取消引用null
  • 其他普通类型规则

    • throw必须应用于Exception类型的对象

Java接口(interface)

Interface指定类之间的关系而不继承。

interface PointInterface { void move(int dx, int dy); }

class Point implements PointInterface { 
	void move(int dx, int dy) { ... }
}

“Java程序可以使用接口,使相关类不必共享一个通用的抽象超类或向Object添加方法。”

换句话说,接口起到C++中多重继承的作用,因为类可以实现多个接口。

class X implements A, B, C { ... }

例子

研究生既可以是大学员工,也可以是学生

class GraduateStudent implements Employee, Student {... }

没有很好的方法使用单一继承将研究生和员工,学生结合起来。

具体实现

实现接口类中的方法不需要固定偏移量。

interface PointInterface { void move(int dx, int dy); }

class Point implements PointInterface {
	void move(int dx, int dy) { ... }
}

class Point2 implements PointInterface {
	void dummy() { ... }
	void move(int dx, int dy) { ... }
}
  • 分派$\mathrm{e.f(\ldots)}$,其中$\mathrm e$具有接口类型比通常更复杂,因为方法不以固定偏移量存在。
  • 一种方法:
    • 实现接口的每个类都有一个查找表方法名$\mathrm{method\ names\to methods}$
    • 用于更快查找的哈希方法名称
      • 编译时计算哈希

Java强制(Coercions)

  • Java允许在某些情况下强制原始类型。
  • 在1 + 2.0中,将int 1扩展为float 1.0
  • 强制实际上只是编译器为您插入的原始函数
    • 大多数语言在基本数值类型之间具有广泛的强制性
  • Java区分两种强制 & 强制转换:
    • 扩大总是成功的($\mathrm{int\to float}$)
    • 缩小可能会失败,如果无法将数据转换为所需的类型($\mathrm{float\to int}$,向下转换)
  • 缩小强制类型转换必须明确
  • 拓展转换/强制转换可以是隐式的。
  • Java中唯一没有定义强制/强制转换的类型是什么?
    • Bool

例子

  • 强制会导致令人惊讶的行为

    • 考虑PL / I的一个例子

    • 假设A,B,C为3个字符的字符串

    • A是什么

Java线程

  • Java具有通过线程实现的内置并发性

    • 每个线程都有自己的程序计数器和堆栈
  • 线程对象具有Thread类

    • 启动和停止方法
  • 同步在对象上获得锁:

    synchronized (x) { e }
  • 在同步方法中,this被锁定

例1

class Simple {
  int a = 1, b = 2;
  void to() { a = 3; b = 4; }
  void fro() { println("a= " + a + ", b=" + b); }
}

有两个线程调用to()和fro(),打印的结果是什么?

  • “a = 1 b = 2”
  • “a = 3 b = 4”
  • “a = 3 b = 2”

例2

class Simple {
	int a = 1, b = 2;
	void synchronized to() { a = 3; b = 4; }
	void fro() { println("a= " + a + ", b=" + b); }
}

有两个线程调用to()和fro(),打印的结果是什么?

  • “a = 3 b = 2”
  • “a = 3 b = 4”

例3

class Simple {
  int a = 1, b = 2;
  void synchronized to() { a = 3; b = 4; }
  void synchronized fro() { println("a= " + a + ", b=" + b); }
}

有两个线程调用to()和fro(),打印的结果是什么?

  • “a = 3 b= 4”

小结

  • 即使没有同步,变量也应只保存由某个线程写入的值
    • 值的写入是原子的
    • 违反了double
  • Java并发语义很难详细理解,特别是在某些机器上如何实现它们方面

其他主题

  • Java允许在运行时加载类
    • 对源进行类型检查在编译时进行
    • 字节码验证在运行时进行
  • 由类加载器(ClassLoader)处理加载策略
  • 类也可能被卸载
  • 定义中未明确规定

初始化

  • Java中的初始化很复杂
    • 包括COOL中的所有内容以及更多内容
    • 由于并发的原因导致非常复杂
  • 首次使用类中的符号时,类会初始化
    • 当类被加载时不会被初始化
    • 将初始化错误延迟到可预测的点(当引用了类中的某些内容时)
完整流程
  1. 锁定类的类对象
    • 如果另一个线程将其锁定,请等待该锁定
  2. 如果同一线程已经在初始化该类,则释放锁并返回
  3. 如果类已经初始化,则正常返回
  4. 否则,将此线程标记为正在进行初始化,并解锁类
  5. 初始化超类,字段(按文本顺序)
    • 但首先初始化静态的最终字段
    • 在初始化之前为每个字段提供默认值
  6. 任何错误都将导致错误地初始化类,将类标记为错误
  7. 如果没有错误,则锁定类,将标签类初始化,通知正在等待类对象的线程,解锁类

交互

  • 在具有$\mathrm N$个特征的任何系统中,都可能存在$\mathrm N^2$个特征交互。
  • 大型,功能强大的系统很难理解!
    • 包括编程语言

小结

  • Java做得很好

    • 按照生产级语言标准,做得很好
  • Java将许多重要思想带入了主流

    • 强静态分型
    • 垃圾回收
  • 但是Java也

    • 包括无法充分理解的功能

    • 具有太多特征