1.实例变量和类变量
成员变量 VS 局部变量
- 局部变量(存储在方法的栈内存中)
- 形参:方法签名中定义,由方法调用者赋值,随方法结束而消亡
- 方法内局部变量:方法内定义,必须在方法内进行显示初始化,初始化完成后开始生效,随方法结束而消亡
- 代码块内局部变量:代码块内定义,必须代码块内进行显示初始化,初始化完成后开始生效,随代码块结束而消亡
- 成员变量(类体内定义的)
- 无static:非静态变量/实例变量;有static:静态变量/类变量
- static
- 从程序角度看,static的作用:将实例成员变为类成员。无static修饰的类的成员,成员属于类的实例;有static修饰,成员属于类本身。因此,static只能修饰类的成员
- 定义成员变量时 要 合法前向引用
- 类变量的初始化时机总是处于实例变量的初始化时机之前
-
1 public class RightDef{ 2 // 下面代码完全没有问题 3 int num1 = num2 + 20; 4 static int num2 = 10; 5 }
public class RightDef{ 2 // 非法前向引用 3 int num1 = num2 + 20; 4 int num2 = 10; 5 }
public class RightDef{ 2 // 非法前向引用 3 static int num1 = num2 + 20; 4 static int num2 = 10; 5 }
- 同一个JVM内,每个类只有一个Class对象(可以通过反射获取该Class对象),但是可以创建多个对象
- Java允许通过类的实例对象访问类变量,尽管类变量是属于类Class对象的(实例对象并不具有类变量);底层是:转换为通过类Class对象来访问类变量
- 实例变量的初始化时机
- 类中定义实例变量指定初始值;类中的非静态代码块中指定初始值;构造器中指定初始值
-
class Cat{ String name; int age; // 构造器中指定 public Cat(String name, int age){ this.name = name; this.age = age; } // 非静态代码块指定 { weight = 2.0; } // 定义变量时指定 double weight = 2.3; }
- 三者的执行顺序:经过编译器处理之后,三者的赋值语句都被合并到构造器中。合并过程中,定义变量语句转换得到的赋值语句、初始化块里的语句转换得到的赋值语句,总是位于构造器的所有语句之前;而前两种语句编译合并后的顺序与它们在源代码中的顺序相同。父类构造器方法最先调用,然后再合并。javap 命令查看
- 因此,赋值语句的执行时机为:定义变量时指定,非静态代码块中指定(在创建对象实例时,非静态代码块会执行) 先于 构造器中的赋值语句,至于前两者的顺序与它们的源码顺序相同,二者地位平等。
- 上例中Cat的weight属性值为2.3
- 类变量的初始化时机。只有两种:定义类变量时指定;静态初始化块中。两者的执行顺序为按源码顺序执行。执行完之后再执行另一个
- 类变量初始化分为两个阶段;
- 第一阶段;分配内存,并且此时有默认初始化值(在赋初始值前,实例变量是在构造方法之前,类变量在静态代码块前)
- 第二阶段:赋初始值(本质上是在静态代码块中)
- 类变量初始化分为两个阶段;
2.父类构造器
- 隐式调用和显式调用
- 只要在程序创建java对象时,系统总是先调用最顶层父类的初始化操作(包括初始化块和</先于> 构造器),然后依次向下调用所有父类的初始化操作,最终执行本类的初始化操作返回本类的对象实例。
- 至于先调用父类的哪个构造器?
- ①super显示调用父类构造器
- ②this调用本类重载构造器,然后转到情形①
- ③既没有显示super,也没有显示this。系统在执行子类构造器前, 隐式调用父类的无参构造器。
- 注意:super/this,用于调用构造器(mine:构造只需一次,并且应该先构造出来,才有其他)。只能用于构造器中,必须作为第一行代码,只能有一个(不能同时出现),只能调用一次
- 访问子类对象的实例变量
- 在执行构造器代码之前,对象所占的内存已经被分配下来,此时内存里的值默认是空值。构造器只是赋初始值。
- 当变量的编译类型和运行类型不同时,通过该变量访问引用对象的实例变量时,实例变量的值由声明该变量的类型决定(编译类型);但是当访问方法时,由实际所引用的对象来决定(运行类型)
- 访问被子类重写的方法
- 应该避免在父类的构造器中调用被子类重写过的方法。否则实际运行过程中,构造器调用的将是子类重写的方法而不是父类的方法
- 因为:如果调用了,那么通过子类的构造器创建子类对象实例时,若调用到父类的这个构造器,那么会导致子类的重写方法在子类的构造器的所有代码执行前执行,从而导致子类的重写方法访问不到子类的实例变量的值得情形
3.父子实例的内存控制
- 继承成员变量与继承方法的区别
- 对于一个引用类型的变量而言,当通过该变量访问引用对象的实例变量时,该实例变量的值取决于声明该变量时的类型;当通过该变量调用所引用对象的方法时,该方法行为取决于他所实际引用的对象的类型
- why:编译器会将继承的方法转移到子类中,但是并不会将继承的成员变量转移到子类中;因此,重写方法将完全覆盖父类的方法,实例变量却不可能覆盖父类的成员变量,子类中允许与父类中同名的实例变量。
- 内存中子类实例
- 系统中并没有父类对象实例,只有子类对象实例,但是子类对象实例中保存了它的所有父类所定义的全部实例变量,因此可以重名。
- 当程序创建一个子类对象时,系统不仅仅会为该类中定义的实例变量分配内存,也会为其父类中定义的所有实例变量分配内存,即使父子类中有重名出现(子类会隐藏父类中的同名实例变量,但是不会完全覆盖)
- super关键字本身并没有引用任何对象,甚至不能被当成一个真正的引用变量来使用(限定作用)
-
- 子类方法不能直接直接使用return super;却可以直接使用return this放回调用对象
- 程序不允许直接把super当成变量使用,例如判断 super == a;这条语句将引起编译错误
- super语句的作用:为了访问父类中定义的、被隐藏的实例变量/调用父类中定义的、被覆盖的方法,可以通过super.作为限定来修饰这些实例变量和方法
- 父、子类的类变量
- super.作为限定来访问父类中定义的类变量(也可直接 父类名.属性值<推荐,可读性佳>)
4.final修饰符
- final修饰的变量
- 普通实例变亮,有默认初始值,但是final修饰的必须显示的赋初始化值
- 本质上final实例变量只能在构造器中显示赋值
- 本质上final类变量只能在静态初始化块中进行赋值
- final修饰的局部变量,需要显示赋值,不能修改
- 执行“宏替换”的变量
- 对于一个final修饰的变量而言(不管是类变量、实例变量、还是局部变量),如果定义该final变量时就指定初始值,而且这个初始值可以再编译时就确定下来(直接量,基本的算术表达式,字符串拼接<包括隐士类型转换,但是显式转换则不可以><没有访问变量,没有调用方法><编译器很笨,不能有调用(变量/方法/...)>),那么这个final变量本质上将不再是变量,而是相当于一个直接量。(可以通过 “==” 方法验证)
- 即“宏变量”,编译器会 把程序中 用到该变量的方法直接替换为改变量的值。
- final变量只有在定义变量时指定初始值才会有“宏变量”效果,在其他地方指定则不会有(与编译器 比较 笨 脱不了干系)
- final方法不能被重写
- 如果子类中并不能访问到父类中的某方法(比如限定符),那么子类中定义的同名方法并不属于对父类中方法的重写(@override验证),只是一个普通的方法,那么就自然没有final一说了
- 内部类中的局部变量
- 局部内部类<方法体中/代码块中>(包括匿名内部类)访问方法体内的局部变量(其他内部类<静态/非静态内部类>也访问不到方法体内的局部变量),必须用final修饰
- why:局部内部类产生的隐式“闭包”将使局部变量脱离它所在的方法而继续存在(扩大作用域)
- 内部类可能扩大局部变量的作用域(eg. 内部类中新建线程,在新线程中调用局部变量),为了避免局部变量所在方法执行完毕仍然能够随意更改局部变量值引起的极大混乱,编译器要求所有被内部类访问的局部变量都必须使用final修饰
- 吼吼吼