所谓向前引用,即在定义某个变量前使用它
在c中向前引用一定是错误的,而在Java中,向前引用在某些时候是可行的
为什么java可以实现向前引用(这里只是讨论可行性的原理,实际上java对向前引用做了许多限制,所以这里许多看似无错的案例实际上是不被允许的))
为一个变量分配好空间,填入了默认值,之后无论是读取它还是赋值都是可行的
当在java程序中创建一个对象实例时:
1.先查看类有没有被加载,没有的话首先完成加载类
类的加载会做如下的事情:
加载class文件,给所有static属性分配空间,将这片存储空间清零,这等同于设置了默认值(0,null)
再按照源文件中书写顺序进行指定初始化
2.当类的加载完成后
给所有非static属性分配空间,设置默认值
按照书写顺序进行指定初始化
进行构造器初始化
创建过程结束
对于静态属性
static int b = a;
static Integer a = 1;
在执行这两条语句的赋值动作之前,a、b被加载进内存,拥有了空间和默认值
所以按理来说,b在a的定义语句之前使用它是没有问题的
对于非静态属性,它的加载在静态属性之后,所以不能用非静态属性去给静态属性赋值(即使甚至没有使用向前引用):
int b = 1;
static int a = b;
而非静态属性的执行初始化就宽松多了,可以使用非静态的其他属性,也可以使用静态属性,因为这时所有属性都已经被加载了:
int a = b;
int b = 1;
或
int a = b;
static int b = 1;
按照原理,这两种都不会有什么问题
注:静态属性和静态代码块的地位是一样的,非静态属性和非静态代码块的地位是一样的
*java对向前引用的限制
在指定初始化时,若使用了别的属性值,如语句int a = b;
因为书写顺序的原因,存在两种可能
(1)b未完成自己的指定初始化,这时b的值为默认值
(2)b已经完成了自己的指定初始化,这时b的值为你设定的值
在定义语句前使用一个属性,往往并非程序员特意设计,而是因为疏漏
如上述的例子
int a = b;//a == 0
int b = 1;//b == 1;
乍一看这个代码的目的是让a和b的值都为1
但实际上,执行到a的指定初始化时,b的指定初始化尚未执行,所以此时的b仍为默认值(0)
为了避免类似的问题,java对向前引用做了一些限制
允许在定义一个字段前为它赋值,不允许在定义一个字段前读取它或者修改它
这里有一个小陷阱
{
a = 1;//right
int b = a;//error
}
int a;
即使对a提前完成了指定初始化,也不可以在定义它的语句之前使用它
不过有一些方法可以绕过上面的限制,在属性的定义语句之前进行不止赋值的操作
实际上,当同时满足下面4个条件,编译器就会判断当前的向前引用是非法的(也就是说打破任意一项就可以做到向前引用):
1. 该属性若是静态的,出现在了静态属性的初始化语句或者静态的初始化块中
该属性若是非静态的,出现在了非静态的初始化语句或者非静态的初始化块中
2. 该属性不在赋值语句的左边
3. 直接使用变量名来访问属性
4.以上所有发生在直接包含该属性的类或者接口中
实例
class UseBeforeDeclaration {
static {
x = 100;
// ok - assignment
int y = x + 1;
// error - read before declaration
int v = x = 3;
// ok - x at left hand side of assignment
int z = UseBeforeDeclaration.x * 2;
// ok - not accessed via simple name
Object o = new Object() {
void foo() { x++; }
// ok - occurs in a different class
{ x++; }
// ok - occurs in a different class
};
}
{
j = 200;
// ok - assignment
j = j + 1;
// error - right hand side reads before declaration
int k = j = j + 1;
// error - illegal forward reference to j
int n = j = 300;
// ok - j at left hand side of assignment
int h = j++;
// error - read before declaration
int l = this.j * 3;
// ok - not accessed via simple name
Object o = new Object() {
void foo(){ j++; }
// ok - occurs in a different class
{ j = j + 1; }
// ok - occurs in a different class
};
}
int w = x = 3;
// ok - x at left hand side of assignment
int p = x;
// ok - instance initializers may access static fields
//注:规则1目的在于:
// 限制静态属性初始化使用未定义的静态属性
// ,限制非静态属性初始化使用未定义的非静态属性
//但正如前面分析的,非静态属性可以任意使用静态属性,所以这里没问题
//另外,想要破坏规则1,还可以把赋值语句放进构造器或者函数里,满足了不在初始化语句或者不在初始化代码块的条件,如:
int b;
int a = -1;
UseBeforeDeclaration() {
b = a;//在构造器中初始化
}
static int u =
(new Object() { int bar() { return x; } }).bar();
// ok - occurs in a different class
static int x;
int m = j = 4;
// ok - j at left hand side of assignment
int o =
(new Object() { int bar() { return j; } }).bar();
// ok - occurs in a different class
int j;
}
public class Test {
int b = this.a;//不是通过变量名直接访问,通过方法访问也可以
// int b = getA();
int a = -1;
}
* 以上所有讨论都是限于类的成员变量,并不适用在方法中的本地变量,本地变量无法做到向前引用
对于局部变量,既不能在定义它前使用它,也不能在定义它前给它赋值
本地变量并不会有默认值,当你使用一个未经指定初始化的本地变量,永远都会直接报错
为什么Java不像对待成员属性那样对待本地变量呢,给它赋一个默认值呢?
对于一个成员属性,它可能会在声明处完成指定初始化,也可能会在某个代码块或者函数内部完成,也可能在构造器里完成
所以在使用一个成员属性的时候,编译器很难确定它是否已经完成了指定初始化,这时候一个适当的默认值就很有必要了
对于生存期和作用域局限于方法块中的普通本地变量,它的初始化语句显然就在这个块里,这时候很容易判断出这个变量是否完成了初始化,不赋给默认值,而是强制程序员在完成初始化后再使用变量,可以避免很多错误