. 与“基本类型”对比
• 基本类型(8 个):byte, short, int, long, float, double, char, boolean
• 引用类型:除了上述 8 个之外的所有类型
◦ 类:String, Object, 你自己写的 Person…
◦ 接口:Runnable, List…
◦ 数组:int[], String[], Person[][]…
◦ 枚举:enum Color { RED, GREEN }
◦ 注解:@Override, @Deprecated …
2. 在内存里的表现
• 基本类型变量里存的是实际数值。
• 引用类型变量里存的是对象地址(一个“引用”),真正的对象在堆上。
引用类型又分静态类型,动态类型
“静态类型 / 动态类型” 并不是“引用类型的两个子类”,而是编译期视角 vs 运行期视角对同一个变量或表达式的两种“称呼”。
只有“引用类型”才可能同时具有这两个视角;基本类型没有继承体系,因此不存在这种区分。
静态类型(Static Type)
• 也叫“编译期类型”、“声明类型”。
• 写在源码里,编译器用它来做名字解析和类型检查。
• 一旦声明,在源码层面不再改变。
List<String> list = new ArrayList<>();
// list 的静态类型是 List<String>
Parent p = new Child();
// 静态类型 Parent ≠ 运行时类型 Child
基本类型:
int a = 5;
• 变量 a 的类型就是 int,编译期和运行期都一样,没有“父类/子类”层次,也就没有“静态类型”与“运行时类型”的差别。
“静态”指“编译期(static time)”,与“运行期(runtime)”相对。编译器只要一看到 Parent p,就知道以后所有对 p 的名字检查只能按 Parent 来做,无论右边真正 new 出来的是 GrandChild、Child 还是别的什么。
编译器之所以只看静态类型(Parent)因为 Java 的语言规范——在编译期阶段,所有名字解析(name resolution)都必须依据静态类型来做。
词法-语法阶段
只是把源码变成 AST,变量 p 只是一个名字,还谈不上类型。
2. 语义分析(attribute resolution / name resolution)
这是关键阶段:
• 根据变量声明 Parent p = … 给 AST 节点 p 打上静态类型 Parent。
• 以后凡是看见 p.m()、p.f 之类,解析器会到 Parent 对应的作用域(scope)里去找 m 或 f,找不到就报编译错误。
这一步严格遵守 JLS §6.5 “Determining the Meaning of a Name” 和 §15.12 “Method Invocation Expressions” 的规定:
“选择方法时,以编译时类型为准,运行时再根据实际对象做动态分派。”
3. 生成字节码
语义分析已经保证“能过编译的名字一定在 Parent(或其祖先)里”。
因此字节码里出现的常量池/符号引用自然只指向 Parent 的成员,不会也不需要指向 GrandChild 的任何私有扩展。
换句话说,“符号表里只有 Parent 的成员”是结果,而不是原因。
在 Java 里,作用域和代码块经常重合,但概念上要先分清:
1. 作用域 ≠ 代码块
• 作用域是“名字可见范围”的抽象规则。
• 代码块({ … })只是最常见的作用域载体之一;除此之外还有
– 类作用域(类里声明的成员)
– 方法/构造器作用域
– 包作用域
– import 作用域
– 循环语句、try-with-resources、catch 等也各自形成作用域。
2. 符号表(symbol table)的构建过程
1. 编译器在语义分析阶段遍历 AST,每进入一个作用域就压栈一张局部符号表;退出作用域时出栈。
2. 对类文件,常量池里也会有一张“类级别”的符号表(字段、方法、父类、接口等)。
3. 因此“Parent p = new GrandChild();”中的 p,在编译器眼里属于当前局部作用域;解释 p.m() 时,编译器会沿着作用域栈 + 类继承链一路查找,而不仅仅是“当前代码块”。
3. 回到你最早的问题
• 之所以“只能看到 Parent 的成员”,是因为名字解析规则规定:
– 对实例字段/方法,用静态类型的类及其父类链做查找。
• 这个规则在符号表层面体现为:
– 只把 Parent 及其祖先的成员放进“可解析集合”;
– GrandChild 中新加的成员在 Parent 的查找链里找不到,于是编译错误。