第二课:对象与内存控制

第二课:对象与内存控制

Java向程序员许下了美好的承诺:无需关心内存回收,java提供了优秀的垃圾回收机制来回收已经分配的内存。在这样的承诺下,大部分java开发者肆无忌惮地挥霍着java程序的内存分配,从而造成java程序的运行效率低下。

Java内存管理分为两个方面:内存分配和垃圾回收。内存分配特指创建java对象时JVM为该对象在堆内存中所分配的内存空间。内存回收是指当java对象失去引用时,变成垃圾的时候,JVM的垃圾回收机制自动清理该对象,并回收该对象所占有的内存。

Java同样存在内存泄漏和资源泄漏的问题。

Java的垃圾回收机制本身就是由一个后台线程完成,本身也是非常消耗性能的,若肆无忌惮地创建对象,让系统分配内存和利用垃圾回收机制进行回收:

不断分配内存使得系统中可用内存减少,从而降低程序运行性能;

大量已分配内存的回收使得垃圾回收的负担加重,降低程序的运行性能。

本课先介绍内存分配方面。

2.1实例变量和类变量

局部变量:形参、方法内的局部变量、代码块内的局部变量。

局部变量的作用时间很短暂,它们都被存储在方法的栈内存中。

成员变量、类变量(static)的定义。

类变量的初始化时机总是处于实例变量的初始化时机之前,与定义位置无关。

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

一个类对应一个class,一个类可以创建多个java对象;类只有一个,而对象有多个。

由于同一个JVM内每个类只对应一个class对象,因此同一个jvm内的一个类变量只需要一块内存空间;但对于实例变量而言,该类每创建一次实例,就需要为实例变量分配一块内存空间。也就是说,程序中有几个实例,实例变量就需要几块内存空间。

实例变量调用类变量,底层仍然会将其转变为类去调用类变量。

2.1.2实例变量的初始化时机

每次创建java对象都会为实例变量分配内存空间,并对实例变量执行初始化。

①定义实例变量时指定初始值;

②非静态初始化块中对实例变量指定初始值;

③构造器中对实例变量指定初始值。

第1、2种方式比第3种方式更早执行。

定义实例变量时指定的初始值、初始化块中为实例变量指定的初始值、构造器中为实例变量指定的初始值,三者的作用完全类似,都用于对实例变量指定初始值。

经过编译器处理后,它们对应的赋值语句都被合并到构造器中。在合并过程中,定义变量语句转换得到的赋值语句、初始化块里的语句转换得到的赋值语句,总是位于构造器的所有语句之前;合并后,两种赋值语句的顺序保持它们在源代码中的顺序。

2.1.3类变量的初始化时机

只有当程序初始化该java类时才会为该类的类变量分配内存空间,并执行初始化。

①定义类变量时指定初始值;

②静态初始化块中对类变量指定初始值。

这两种方式的执行顺序与它们在源代码中的排列顺序相同。

对于类变量,系统先进行内存分配,分配完系统进行初始化,初始化之后按定于时的赋值语句或者静态初始化块的赋值语句进行赋值。

2.2父类构造器

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

2.2.1隐式调用和显式调用

隐式调用:当调用某个类的构造器创建java对象,系统总会先调用父类的费静态初始化块进行初始化。而父类的静态初始化块总是会被执行。

而父类的构造器,可以使隐式调用也可以通过super关键字进行显式调用。

super调用用于显式调用父类的构造器,this调用用于显式调用本类中另一个重载的构造器。Super调用和this调用都只能出现在构造其中使用,而且super调用和this调用都必须作为构造器的第一行代码,因此构造器中的super调用和this调用最多只能使用其中之一,而且最多只能调用一次。

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

注意:带着问题继续看下去。一个java对象怎样拥有多个同名的实例变量?子类定义的成员变量并不能完全覆盖父类中成员变量。

很多书籍会说java对象是由构造器创建的,但实际情况是,构造器只是负责对java对象实例变量进行执行初始化。执行构造器代码之前,该对象所占的内存已经被分配下来,这些内存里都默认是空值。

this在构造器中,this表示的是当前的java对象。

故访问子类对象的实例变量的时候,也注意构造器的实际运行机制和this关键字的含义。

是比较容易混乱,po一个书里的例子:

 

2.2.3调用被子类重写的方法

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

回头要做个实验:

 

2.3父子实例的内存控制

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

创建一个对象:

Fruit apple=new Apple();

Fruit为编译时类型,而Apple是运行时类型。也就是编译时实例变量的值是Fruit的值,而运行方法用的是Apple中的方法。

会出现一种情况:指向同一对象的引用类型,其实例变量的值却不一样,实际上,这关乎到java继承中在处理成员变量和方法时存在区别的问题。

子类定义了与父类完全同名的实例变量,这个实例变量不可能覆盖父类中定义的实例变量;但子类定义的与父类同名的方法,却是可以彻底覆盖掉父类的方法。(重写)

2.2.3 内存中子类实例

当程序创建一个子类对象时,系统不仅会为该类中定义的实例变量分配内存,也会为其父类中定义的所有实例变量分配内存,即使子类定义了与父类中同名实例变量。

如果在子类里定义了与父类中已有变量同名的变量,那么子类中定义的变量会隐藏父类中定义的变量。但不是完全覆盖,系统为创建子类对象时,依然会为父类中定义的、被隐藏的变量分配内存空间。

为了在子类方法中访问父类中定义的、被隐藏的实例变量,或者为了在子类方法中调用父类中定义的、被覆盖的方法,可以通过super作为限定来修饰这些实例变量和实例方法。

2.3.3父、子类的类变量

子类访问父类的类变量:

①使用父类的类名来访问其类变量;

②使用super来访问父类的类变量。

2.4 final修饰符

final可用来修饰变量、方法和类,一经定义就不可再修改了。

2.4.1 final修饰的变量

final修饰实例变量:

①定义final实例变量时指定初始值;

②在非静态初始化块中为final实例变量指定初始值;

③在构造器中为final实例变量指定初始值。

以上三种方式其实都一样,到最后实例变量还是会在构造器中被赋值。

对于普通的实例变量,java程序可以对它执行默认的初始化;但是对于final实例变量,必须由程序员显式指定初始值。

final修饰类变量:

①定义final类变量时指定初始值;

②在静态初始化块中为final类变量指定初始值。

经过编译器处理,这两种方式都会被抽取到静态初始化块中赋初始值。

final定义局部变量。

以上,被final修饰的变量一旦被赋初始值,final变量的值就将不会再改变。

2.4.2 执行“宏替换”的变量

final修饰符的一个重要用途就是定义“宏变量”,当定义final变量时就为该变量指定了初始值,而且该初始值可以在编译时确定下来,那这个final变量本质就是一个“宏变量”,编译器会把程序中所有用到该变量的地方直接替换成该变量的值。

2.4.3final方法不能被重写

2.4.4内部类中的局部变量

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值