(二)对象与内存控制

二.对象与内存控制

Java内存管理分为两个方面:内存分配和内存回收。这里的内存分配特指创建Java对象是JVM为该对象在堆内存中所分配的内存空间。内存的回收指的是当该Java对象失去引用,变成垃圾时,JVM的垃圾回收机制自动清理该对象,并回收该对象所占用的内存。
由于JVM的垃圾回收机制由一条后台线程完成,本身也是非常消耗性能的,因此如果肆无忌惮的创建对象,让系统分配内存,那这些分配的内存都将由垃圾回收机制进行回收。这样做有两个坏处:
1.不断分配的内存使得系统可分配内存减少,从而降低程序的运行性能
2.大量已分配内存的回收使得垃圾回收的负担加重,降低程序的运行性能

2.1 实例变量和类变量

Java程序的变量大体可分为成员变量和局部变量。其中局部变量可分为如下3类
1.形参:在方法签名中定义的局部变量,由方法调用者负责为其赋值,随方法结束而消亡
2.方法内的局部变量:在方法内定义的局部变量,必须在方法内对其进行显式初始化。这种类型的局部变量从初始化完成后开始生效,随方法结束而消亡
3.代码块内的局部变量:在代码块内定义的局部变量,必须在代码块内对其进行显式初始化。这种类型的局部变量从初始化完成后生效,代码块结束后消亡
局部变量作用时间很短暂,他们都被存储在方法的栈内存中

类体内定义的变量被称为成员变量,如果定义该成员变量时没有使用static修饰,该成员变量又被称为非静态变量或实例变量

注:对于static关键字而言,从词义上来看它是“静态”的意思,但从Java程序的角度,static的作用就是将实例成员变成类成员,static只能修饰在类里定义的成员成分,如果没有使用static修饰这些类里的成员,这里的成员属于该实例

2.1.1 实例变量和类变量的属性

使用static修饰的成员变量时类变量,属于该类本身;没有使用static修饰的成员变量是实例变量,属于该类的实例。在同一个JVM里,每个类只对应一个Class对象,但每个类可以创建多个Java对象
由于同一个JVM内每个类只对应一个Class对象,因此同一个JVM内的一个类的类变量只需要一块内存空间;但是就实例变量而言,该类每创建一次实例,就需要为实例变量分配一块内存空间。也就是说,程序中有几个实例,实例变量就需要几块内存空间

当类初始化完成之后,类变量也随之初始化完成,以后不管程序创建多少个类对象,系统不在为类变量分配内存;但程序每创建一个类对象,系统将再次为实例变量分配内存,并执行初始化
当程序通过实例变量访问该类的实例时在Java底层实际上是将实例变量转换为类变量达成方位所以在实际上类变量并不属于实例变量

2.1.2 实例变量初始化的时机

对于实例变量而言,它属于Java对象本身,每次程序创建Java对象时都需要为实例变量分配内存空间,并执行初始化
从程序运行角度来看,每次创建Java对象都会为实例变量分配内存空间,并对实例变量执行初始化
程序可以在3个地方对实例变量执行初始化
1.定义实例变量是指定初始值
2.非静态初始化块中对实例变量指定初始值
3.构造器中对实例变量指定初值
其中第1,2种方式比第3种方式更早执行,但第1,2种方式的执行顺序与他们在源程序中的排列顺序相同

定义实例变量时指定的初始值,初始化块中为实例变量指定的初始值,构造器中为实例变量指定的初始值,三者作用完全相似,都用于对实例变量指定初始值。经过编译器处理之后,他们对应的赋值语句都被合并到构造器中。在合并过程中,定义变量语句转换得到的赋值语句,初始化块里语句转换得到的赋值语句,总是位于构造器所有语句之前;合并后,两种赋值语句的顺序保持他们在源码中的顺序

2.1.3 类变量的初始化时机

类变量属于Java本身,只有当程序初始化该Java类时才会为该类的类变量分配内存空间,并执行初始化。
从程序运行角度来看,每个JVM对一个Java类只初始化一次,因此Java程序每运行一次,系统只为类变量分配一次空间,执行一次初始化
从语法角度看,程序可以在2个地方对类变量执行初始化:
1.定义类变量时指定初始值
2.静态初始化块中对类变量指定初始值
两种方式的执行顺序与它们在源程序中的排列顺序相同

2.2 父类构造器

当创建任何Java对象时,程序总会先依次调用每个父类非静态初始化块,父类构造器(总是从Object开始)执行初始化,最后才调用本类的非静态初始化块,构造器执行初始化

2.2.1 隐式调用和显式调用

当调用某个类的构造器来创建Java对象时,系统总会先调用父类的非静态初始化块进行初始化。这个调用是隐式进行的,而且父类的静态初始化块总是会被执行。接着会调用父类的一个或多个构造器执行初始化,这个调用既可以是通过super进行显式调用,也可以是隐式调用。
当所有父类的非静态初始化块构造器依次调用完成后,系统调用本类的非静态初始化块,构造器执行初始化,最后反回本类的实例

只要在程序创建Java对象,系统总是先调用最顶层的父类初始化操作,包括初始化块和构造器,然后依次向下调用所有父类的初始化操作,最终执行本类的初始化操作返回本类的实例。至于调用那个构造器执行初始化,则分为以下几种情况:
1.子类构造器执行体的第一行代码使用super显式调用父类构造器,系统将根据super调用里传入的实参列表来确定是那个构造器;
2.子类构造器执行体的第一行代码使用this显式调用本类中重载的构造器,系统将根据this调用里传入的实参参数来决定本类的另一个构造器
3.子类构造器执行中既没有super调用也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器

2.2.2 访问子类对象的实例变量

构造器只是负责对Java对象实例变量执行初始化(也就是赋初始值),在执行代码之前,该对象所占用的内存已经被分配下来,这些内存里的值都默认是空值——对于基本类型的变量,默认的空值就是0或false;对引用类型的变量默认就是null

当变量的编译时类型和运行时类型不同时通过该变量访问它引用的对象的实例变量时,该实例变量的值由声明该变量的类型决定。但通过该变量调用它引用的对象的实例方法时,该方法行为将由它实际所引用的对象决定

2.2.3 调用被子类重写的方法

如果父类构造器调用了被子类重写的方法,且通过子类构造器来创建子类对象,调用(不管是显示还是隐式)了这个父类构造器,就会导致子类重写方法在子类构造器的所有代码之前执行,从而导致子类的重写方法访问不到子类的实例变量值的情形

2.3 父子实例的内存控制

2.3.1 继承成员变量和继承方法的区别

编译器在处理方法和成员变量时存在区别。对于父类中定义的成员变量而言,系统依然将其保留在父类中,并不会将他转移到其子类中。这使得父类和子类可以同时拥有同名的实例变量
如果子类重写了父类的方法,就意味着子类里定义的方法覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中。对于实例变量则不存在这样的现象,即使子类中定义了与父类完全同名的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量
因为继承成员变量和继承方法之间存在这样的差别,所以对于一个引用类型的变量而言,当通过该变量访问它所引用的对象的实例变量时,该实例变量的值取决于声明该变量时的类型;当通过该变量来调用它所引用的对象方法时,该方法行为取决于它所实际引用的对象的类型

2.3.2 内存中的子类实例

子类对象中不仅保存了在子类中定义的所有实例变量,还保存了它的所有父类变量所定义的全部实例变量

super关键字本身并没有引用任何对象,它甚至不能被当成一个真正的引用变量使用。主要有如下两个原因:
1.子类方法不能直接使用return super ; 但使用return this ;返回调用该方法的对象是允许的
2.程序不允许直接把super当对象使用

至此,对父,子对象在内存中存储有了一个准确结论:当程序创建一个子类对象时,系统不仅会为为该类所定义的实例变量分配内存,也会为其父类中定义的所有实例变量分配内存,即使子类定义了与父类同名的实例变量。也就是说,当系统创建一个Java对象时候,如果该Java类有两个父类(一个直接父类A,一个间接父类B),假设A类中定义了2个实例变量,B类中定义了三个实例变量,当前类中定义了2个实例变量那这个Java对象将会保存2+3+2个实例变量
如果在子类里定义了已有变量同名的变量,那么子类中定义的变量会隐藏父类中定义的变量。注意不是完全覆盖,因此系统为创建子类对象时,依然会为父类中定义的,被隐藏的变量分配内存空间
为了在子类方法中访问父类中定义的,被隐藏的变量,或者为了在子类方法中调用父类中定义的,被覆盖的方法,可以通过super,作为限定来修饰这些实例变量和实例方法

2.3.3 父类子类的类变量

类变量属于类本身,而实例变量则属于Java对象;类变量在类初始化阶段完成初始化,而实例变量在对象初始化时完成初始化
如果需要访问父类中定义的类变量,程序有两种方式:
1.直接使用父类名作主调访问类变量
2.使用super作为限定来访问类变量

2.4 final 修饰符

1.final 可以修饰变量,被final修饰的变量被赋初始值后,不能对它重新赋值
2.final 可以修饰方法,被final修饰的方法不能重写
3.final可以修饰类,被final修饰的类不能派生子类

2.4.1 final修饰的变量

被final修饰的实例变量必须显式指定初始值,而且只能在如下三种情况中指定初始值
1.定义final实例变量时指定初始值;
2.在非静态初始化块中为final变量指定初始值;
3.在构造器中为final变量指定初始值
对于普通实例变量Java程序可以对其进行默认初始化,但对于final变量,则必须由程序员显式指定初始值
对于final类变量而言,同样必须显式指定初始值,而且只能在两个地方指定初始值:
1.定义final变量时指定初始值
2.在静态初始化块中为final类变量
当使用final修饰类变量时,如果定义该final类变量时指定了初始值,而且该初始值可以在编译时就被确定下来,系统将不会在静态初始化块中对该类变量赋初始值,而将是在类定义中直接使用该初始值代替该final变量

2.4.2 执行“宏替换”的变量

对一个final变量,不管它是类变量,实例变量,还是局部变量,只要定义该变量时使用了final修饰符,并在定义该final类变量时指定了初始值,而且该初始值可以在编译时就被确定下来,那么这个final变量本质上已经不再是变量,而是相当于一个直接量

注:Java会缓存所用曾经用过的字符串直接量。例如执行String a =“Java”;语句之后,系统的字符串池中就会缓存一个字符串“Java” ;如果程序再次执行String b = “Java” ;系统将会让b 直接指向字符串池中的“Java”字符串,因此a==b将会返回true。

2.4.3 final方法不能被重写

如果父类中某个方法使用了final修饰,那么这个方法将不可能被它的子类访问到,因此这个方法也不可能被它的子类重写

注:Java编程中有一个比较有用的工具注释——@Override,被该注释修饰的方法必须重写父类方法。为了避免在编程过程中出现手误,每当希望某个方法重写父类方法时,总应该为该方法添加@Override注释,如果被@Override修饰的方法没有重写父类的方法,编译器会在编译该程序时提示编译错误

2.4.4 内部类中的局部变量

Java要求所有被内部类访问的局部变量都使用final修饰:对于普通局部变量而言,它的作用域就是停留在该方法内,当方法执行结束,该局部变量也就随之消失;但内部类则可能产生隐式的闭包,闭包将使得局部变量脱离它所在的方法继续存在
由于内部类可能扩大局部变量的作用域,如果再加上这个被内部类访问的局部变量没有使用final修饰,也就是说该变量的值可以随意改变,那将引起极大的混乱,因此Java编译器要求所有内部类访问的局部变量必须使用final修饰符修饰

你可能感兴趣的:((二)对象与内存控制)