文章目录
1 实例变量和类变量
java 变量大体可分为:
成员变量
局部变量
局部变量分为以下三种:
- 形参:申明方法时定义的变量,随着方法结束而结束。
- 方法内定义的局部变量:必须在方法内显示的初始化,随着方法结束而结束。
- 代码块的局部变量:也需要在代码块中显示初始化,随着代码块结束而结束
类中定义的变量为成员变量,有两种:
- 定义时,使用了static,则称为静态变量或者类变量,生命周期随着类的消亡而消亡。
- 定时时,没有使用static,则称为非静态变量或者实例变量,没有引用则会被回收。
在以下两种情况,需要严格的遵循变量向前引用的原则。
- 两个变量都为实例变量
如:
int a = 2;
int b = a + 2;
- 两个变量都为类变量
如:
static int a = 2;
static int b = a + 2;
这很好理解,也复合大众逻辑,但是有一种情况就是例外:
int a = b + 2;
static int b = 1;
这是合法的,虽然平时我们都不会这么写,这是因为__类变量的初始化实际总是处于实例变量初始化之前__。换句话说,在a 初始化之前,b已经在类初始化时,赋值完成。
2 实例变量和类变量的内存属性
static修饰的类变量属于类本身,而一个JVM中,每个类只有一个Class对象,但是该类的对象实例可以创建对个,因此在内存中只对应一块内存空间。这就意味着有如下特性:
- 通过static修饰的类变量,在内存中只对应一个内存空间,当发生改变时,该类的所有对象实例读取该类变量时,都会发生变化。
- 不通过static修饰的实例变量,在内存中的空间会随着对象实例的增加而增加。换句话说,new了多少个对象,则会创建多少个内存空间。不同空间的值也不会相互影响。
- 但是对于实例变量而言,该类每创建一次实例,就需要为实例变量分配一块内存。有几个实例,就分配了几块内存空间。
这两者对应的初始化时间和赋值时间:
在初始化实例变量之前,会先为所有实例变量分配内存空间,并赋予初值,无论这个实例是在代码块中申明还是直接申明,但是最后的赋值语句,都是在构造函数中执行,并且保持在源码中副赋值顺序。
在初始化类变量之前,会先为所有类变量分配内存空间,并赋予显示的初始值,再按照源码中的排序顺序执行静态初始化块和类变量所指定初始值。
而类变量初始化和实例变量初始化的时间不同,类变量初始化是在类加载的时候做的,而实例变量初始化是在创建对象的时候做的。
3 父类构造器
在创建任何java对象时,总会依次初始化,:
- 父类静态变量
- 父类静态代码块
- 子类静态变量
- 子类静态代码块
- 父类普通变量
- 父类普通代码块
- 父类构造函数
- 子类普通变量
- 子类普通代码块
- 子类构造函数
注意,实例变量在初始化时,是先赋予初值,再在构造函数中,赋值真实值
需要注意的是,在初始化子类之前,先初始化的是父类的哪个构造函数。
- 子类构造器第一行使用super显示调用,则根据调用传入的实参列表来确定调用父类的哪个构造器
- 子类构造器执行体的第一行代码使用this显式调用本类中重载的构造器,系统将根据调用里传入的实参列表来确定本类的另一个构造器。
- 子类构造器执行提中既没有 super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器。
3.1 实例变量初始化
对于java对象而言,每次程序创建java对象时,都需要为实例对象分配内存空间,并执行初始化。
对实例变量初始化的三种方式:
- 定义是指定初始值
- 在非静态代码块中指定初始值
- 在构造器中指定初始值
1,2两种方式比第3种(也就是构造器中)更早执行,执行顺序和在源程序中相同。
但是在JVM中,对于实例变量赋予初始值的操作都是在构造函数里执行的,看到这里可能奇怪了,这是否和我们之前说的矛盾了呢?
其实并不是,代码块中的赋初值和定义实例变量时赋值,都是将定义和赋值的两部分操作分开,在构造函数外定义,在构造函数内赋值,只不过赋值语句在构造函数代码的前面而已,合并后,前两者还是保持原来的顺序。
比如:
public class test{
int count = 20;
{
count = 12;
}
public test(){
}
}
等同于:
public class test{
int count ;
public test(){
count = 20;
count =12;
}
}
3.2 类初始化
类变量属于java类本身,只有当程序员初始化该java类时,才会为该类变量分配内存空间,并执行初始化。
每个JVM对于一个java类只初始化一次,也只会为类变量分配一次内存空间。
对类变量执行初始化的两种方式:
- 定义类变量时指定初始化值。
- 在静态代码块中对类变量指定初始值。
这两种方式的执行顺序与它们在源程序的执行顺序相同。
JVM在对类执行初始化:
- 会先为所有类变量分配内存空间。
- 再按源代码的排列顺序执行静态初始化块中所指定的初始值和定义类变量时的初始值。
4 父子实例变量的内存
当子类继承父类时,子类可以获取父类的成员变量和方法,在public和protected关键字申明的情况下,可以直接访问父类的变量和方法。但是!!java中对成员变量和方法的处理是不同的。
先说结论吧:
子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法。
子类定义了与父类完全同名的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量。
继承成员变量和继承方法之间存在这样的差别,所以对于一个引用类型的变量而言:
当通过该变量访问它所引用的对象的实例变量时,该实例变量的值取决于声明该变量时类型(等号左边)
当通过该变量来调用它所引用的对象的方法时,该方法行为取决于它所实际引用的对象的类型(等号右边)。
为什么会这样???
我想说说个人理解:
首先根据类中实例变量和类变量的在内存中的属性,类变量跟着类走,存储在方法区中,所有引用永远只指向一块内存空间。但是实例变量存储在内存堆中,同一个类在创建不同对象时,会指向不同的堆内存空间,这就意味着,所以在父类和子类两个类,在创建实例时,所指向的内存空间也不同,因此不会覆盖。
其次,对于子类和父类的重名方法,在执行方法时,是将方法中的语句压入方法栈中,在一个栈中,如果压入两个相同名称和参数的方法则会造成结果紊乱,不知道哪个返回值是正确的。因此java不允许压入完全重复的方法。
(以上都是根据结合JVM虚拟机内存结构的个人理解,如果觉得不正确的同学欢迎指正,你的指正就是我的进步,我谢谢你!!!)
看个例子:
class Base{
int count = 2;
}
class Mid extends Base{
int count = 20;
}
public class Sub extends Mid {
int count = 200;
public static void main(String[] args) {
Sub s = new Sub();
Mid s2m = s;
Base s2b = s;
System.out.println(s.count);
System.out.println(s2m.count);
System.out.println(s2b.count);
}
}
结果依次是200,20,2;
根据上述的分析和原则,实例变量的值,都是根据申明的类决定的。
5 父子类变量的内存
类变量跟着类走,存储在方法区中,所有引用永远只指向一块内存空间。一个类变量,存储在本类的方法区内存中,不会被子类覆盖。
6 final 申明
在申明final变量时,有几个注意点:
- 必须显示指定初始值。
- 无论是在申明时就赋予初始值,还是在静态代码块中被赋予初始值,内存的本质上初始值都是在代码块中被赋值的。
- 申明后不可对final修饰的变量,再次赋值。
- final方法不可被重写。
7 final宏替换
对于一个final变量,无论是类变量、实例变量、局部变量,只要在定义该变量时用final修饰,这个值是在编译时期确定的。也就意味着,在引用的该变量的地方,直接用常量代替。这既是所谓的宏替换。
这种宏替换除了发生在final修饰符中以外,还会发生在变量在被赋值时,赋值语句只是简单的算术表达式或者字符串连接运算。
来个例子:
public class FinalTest{
public static void main(String[] args){
//下面定义了4个fina1“宏变量”
final int a=5+21
final double b 1.2/3
fina1 string str="疯狂”+"Java";
fina1 string book="疯狂Java讲义:”+99.0;
//下面的book2变量的值因为调用了方法,所以无法在编译时被确定下来
final String book2="疯狂Java讲义:"+ String, valueof(99.0);
System.out, print1n(book="疯狂Java讲义:99.0");
System.out, print1n(book2=“疯狂Java讲义:99.0”);
}
}
结果为 true false.
8 内部类的局部变量的final修饰
在内部类中的使用局部变量必须使用final,因为当不用final修饰时,会有闭包脱离所在方法继续运行的场景。
比如:
public static void main(String[] args) {
final String str = "java";
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 100 ; i++){
System.out.println(str + " " + i );
try {
Thread.sleep(100);
}catch (Exception ex){
ex.printStackTrace();
}
}
}
}).start();
}
正常情况下,在执行完start后,str会结束生命周期,但是,只要线程没有结束,str就会存在,并且会有被修改的可能,这将会引起很大的混乱。