[转帖]java对象和内存的控制深入浅出_Android, Python及开发编程讨论区_Weblogic技术|Tuxedo技术|中间件技术|Oracle论坛|JAVA论坛|Linux/Unix技术|hadoop论坛_联动北方技术论坛  
网站首页 | 关于我们 | 服务中心 | 经验交流 | 公司荣誉 | 成功案例 | 合作伙伴 | 联系我们 |
联动北方-国内领先的云技术服务提供商
»  游客             当前位置:  论坛首页 »  自由讨论区 »  Android, Python及开发编程讨论区 »
总帖数
1
每页帖数
101/1页1
返回列表
0
发起投票  发起投票 发新帖子
查看: 3108 | 回复: 0   主题: [转帖]java对象和内存的控制深入浅出        下一篇 
只是很无聊
注册用户
等级:中尉
经验:440
发帖:33
精华:0
注册:2013-6-18
状态:离线
发送短消息息给只是很无聊 加好友    发送短消息息给只是很无聊 发消息
发表于: IP:您无权察看 2013-6-19 10:42:39 | [全部帖] [楼主帖] 楼主

一、对象与内存控制的知识点

1.java变量的初始化过程,包括局部变量,成员变量(实例变量和类变量)。
2.继承关系中,当使用的对象引用变量编译时类型和运行时类型不同时,访问该对象的属性和方法是有区别的。
3.final修饰符特性。

二、java变量的划分与初始化过程

     java程序的变量大体可以分为成员变量和局部变量,成员变量可以分为实例变量(非静态变量)和类变量(静态变量),一般我们遇到的局部变量会在下列几种情况中出现:
     (1)形参:在方法签名中定义的局部变量,由调用方为其赋值,随着方法结束消亡。
     (2)方法内的局部变量:在方法内定义的局部变量必须在方法内显示的初始化(赋初始值),随着变量初始化完成开始,到方法结束而消亡。
     (3)代码块内的局部变量:在代码块内定义的局部变量必须在代码块内显示的初始化(赋初始值),随着初始化完成开始生效,随着代码块的结束而消亡。

01packagecom.zlc.array;


02


03publicclassTestField {


04{


05String b ;


06//如果不初始化,编译器就会报The local variable b may not have been initialized


07System.out.println(b);


08}


09publicstaticvoidmain(String[] args) {


10inta ;


11//如果不初始化,编译器就会报The local variable b may not have been initialized


12System.out.println(a);


13}


14}


    使用static修饰的成员变量是类变量,属于类本身,没有用static修饰的成员变量是实例变量,属于该类的实例,在同一个JVM里面,每个 类只能对应一个Class对象,但每个类可以创建多个java对象。(也就是说一个类变量只需一块内存空间,而该类每创建一次实例,就需要为实例变量分配 一块空间)
   
     实例变量的初始化过程:从语法角度来说,程序可以在三个地方对实例变量执行初始化:
   (1)定义实例变量时指定初始值。
   (2)非静态块中对实例变量指定初始值。
   (3)构造器中对实例变量指定初始值。
    其中(1)和(2)这两种方式初始化时间都比(3)在构造器中要早,(1)和(2)两种初始化顺序是按照他们在源码中的排列顺序决定的。

01packagecom.zlc.array;


02


03publicclassTestField {


04publicTestField(intage){


05System.out.println("构造函数中初始化 this.age = "+this.age);


06this.age = age;


07}


08{


09System.out.println("非静态块中初始化");


10age = 22;


11}


12//定义的时候初始化


13intage = 15;


14publicstaticvoidmain(String[] args) {


15TestField field = newTestField(24);


16System.out.println("最终 age = "+field.age);


17}


18}


    运行结果为:非静态块中初始化
                     构造函数中初始化 this.age = 15
                     最终 age = 24
    如果会使用javap的话,可以通过javap -c XXXX(class文件)看下改java类是如何编译的。
    定义实例变量时指定初始值,初始化块中为实例变量指定初始值语句地位是平等的,当经过编译器编译处理之后,他们都被提到构造器中,上面所说的 int age = 15;会划分下面两个步骤执行:
    1)int age;创建java对象时系统根据该语句为该对象分配内存。
    2)age = 15;这条语句会被提取到java类的构造器中执行。
   
    类变量的初始化过程
:从语法角度来说,程序可以从两个地方对类变量进行初始化赋值。
   (1)定义类变量时指定初始值。
   (2)静态块中对类变量指定初始值。
    两种执行顺序和他们在源码中的排列顺序相同,我们举个小变态的例子:

01packagecom.zlc.array;


02


03classTestStatic {


04//类成员 DEMO TestStatic实例


05finalstaticTestStatic DEMO = newTestStatic(15);


06//类成员 age


07staticintage = 20;


08//实例变量 curAge


09intcurAge;


10


11publicTestStatic(intyears) {


12// TODO Auto-generated constructor stub


13curAge = age - years;


14}


15}


16publicclassTest{


17publicstaticvoidmain(String[] args) {


18System.out.println(TestStatic.DEMO.curAge);


19TestStatic staticDemo = newTestStatic(15);


20System.out.println(static1.curAge);


21}


22}



    输出结果有两行打印,一个是打印TestStatic类属性DEMO的实例变量,第二个通过java对象staticDemo输出TestStatic的实例属性,根据我们上面分析的实例变量和类变量的初始化流程可以进行推���:
    1)初始化第一阶段,加载类的时候为类变量DEMO、age分配内存空间,此时DEMO和age的默认值分别是null和0。

   2)初始化第二阶段,程序按顺序依次给 DEMO、age赋初始值,TestStatic(15)需要调用TestStatic的构造器,此时age = 0 所以打印结果为 -15,而当staticDemo被初始化的时候,age已经被赋值等于20了,所以输出结果为5。

三、在继承关系中继承成员变量和继承成员方法的区别。

    当创建任何java对象时,程序总会先调用父类的非静态块、父类构造器,最后才调用本类的非静态块和构造器。通过子类的构造器调用父类的构造器一般分为两种情况,一个是隐式调用,一个通过super显示调用父类的构造器。
    子类的方法可以调用父类的实例变量,这是因为子类继承了父类就会获取父类的成员变量和方法,但父类的方法不能访问子类的实例变量,因为父类不知道 它将被哪个类继承,它的子类将会增加什么样的成员变量,当然在一些极端的例子里面还是可以实现父类调用子类变量的,比如:子类重写了父类的方法,一般都会 打印出默认值,因为这个时候子类的实例变量还没有初始化。

01packagecom.zlc.array;


02classFather{


03intage = 50;


04publicFather() {


05// TODO Auto-generated constructor stub


06System.out.println(this.getClass());


07//this.sonMethod();无法调用


08info();


09}


10publicvoidinfo(){


11System.out.println(age);


12}


13}


14publicclassSon extendsFather{


15intage = 24;


16publicSon(intage) {


17// TODO Auto-generated constructor stub


18this.age = age;


19}


20@Override


21publicvoidinfo() {


22// TODO Auto-generated method stub


23System.err.println(age);


24}


25publicstaticvoidmain(String[] args) {


26newSon(28);


27}


28//子类特有的方法


29publicvoidsonMethod(){


30System.out.println("Son method");


31}


32}



    按照我们正常推断,通过子类隐式的调用父类的构造器,而在父类的构造器中调用了info()方法(注意:我这里没有说调用父类的),按道理来说是输出了父类的age实例变量,打印结果预计是50,但实际输出的结果为0,分析原因:
   1)java对象的内存分配不是在构造器中完成的,构造器只是完成了初始化赋值的过程,也就是在调用父类的构造器之前,jvm已经给这个Son对象分类好了内存空间,这个空间存放了两个age属性,一个是子类的age,一个是父类的age。
   2)在调用new Son(28)的时候,当前的this对象代表着是子类Son的对象,我们可以通过把对象.getClass()打印出来就会得到class com.zlc.array.Son的结果,但是当前初始化过程又是在父类的构造器中进行的,通过this.sonMethod()又无法被调用,这是因 为this的编译类型是Father的缘故。

   3)在变量的编译时类型和运行时类型不同时,通过该变量访问它的引用对象的实例变量时,该实例变量的值由声明该变量的类型决定,但通过该变量调用它引用 的对象的实例方法时,该方法的行为由它实际引用的对象决定,所以这里调用的是子类的info方法,所以打印的是子类的age,由于age还没来得急初始化 所以打印默认值0。
    通俗的来说也就是,当声明的类型和真正new的类型不一致的时候,使用的属性是父类的,调用的方法是子类的。
    通过javap -c我们更能直接的体会为什么继承属性和方法会有很大的区别,如果我们把上面例子里面,子类Son的info重写方法去掉,这个时候调用的会是父类的 info方法,是因为在进行编译的时候会把父类的info方法编译转移到子类里面去,而声名的成员变量会留在父类中不进行转移,这样子类和父类拥有了同名 的实例变量,而如果子类重写了父类的同名方法,则子类的方法会完全覆盖掉父类的方法(至于为什么java要这么设计,个人也不太清楚)。同名变量能同时存 在不覆盖,同名方法子类会彻底覆盖父类同名方法。
    总的来说对于一个引用变量而言,当通过该变量访问它所引用的对象的实例变量时,该实例变量的值取决于声明该变量时类型,当通过该变量访问它所引用的对象的方法时,该方法行为取决于它所实际引用的对象的类型。
最后拿个小case复习下:

01packagecom.zlc.array;


02classAnimal{


03intage ;


04publicAnimal(){


05


06}


07publicAnimal(intage) {


08// TODO Auto-generated constructor stub


09this.age = age;


10}


11voidrun(){


12System.out.println("animal run "+age);


13}


14}


15classDog extendsAnimal{


16intage;


17String name;


18publicDog(intage,String name) {


19// TODO Auto-generated constructor stub


20this.age = age;


21this.name = name;


22}


23@Override


24voidrun(){


25System.out.println("dog run "+age);


26}


27}


28publicclassTestExtends {


29publicstaticvoidmain(String[] args) {


30Animal animal = newAnimal(5);


31System.out.println(animal.age);


32animal.run();


33


34Dog dog = newDog(1, "xiaobai");


35System.out.println(dog.age);


36dog.run();


37


38Animal animal2 = newDog(11, "wangcai");


39System.out.println(animal2.age);


40animal2.run();


41


42Animal animal3;


43animal3 = dog;


44System.out.println(animal3.age);


45animal3.run();


46}


47}



     想要调用父类的方法:可以通过super来调用,但super关键字没有引用任何对象,它不能当做真正的引用变量来使用,有兴趣的朋友可以自己研究下。
     上面介绍的都是实例变量和方法,类变量和类方法要简单多了,直接使用类名.方法就方便了很多,也不会遇到那么多麻烦。

四、final修饰符的使用(特别是宏替换)

   final可以修饰变量,被final修饰的变量被赋初始值之后,不能对他重新赋值。
   final可以修饰方法,被final修饰的方法不能被重写。
   final可以修饰类,被final修饰的类不能派生子类。

被final修饰的变量必须显示的指定初始值:
对于是final修饰的是实例变量,则只能在下列三个指定位置赋初始值。
    (1)定义final实例变量时指定初始值。
    (2)在非静态块中为final实例变量指定初始值。
    (3)在构造器中为final实例变量指定初始值。
     最终都会被提到构造器中进行初始化。
对于用final指定的类变量:只能在指定的两个地方进行赋初始值。
    (1)定义final类变量的时候指定初始值。
    (2)在静态块中为final类变量指定初始值。
     同样经过编译器处理,不同于实例变量的是,类变量都是提到静态块中进行赋初始值,而实例变量是提到构造器中完成。
    被final修饰的类变量还有一种特性,就是“宏替换”, 当被修饰的类变量满足在定义该变量的时候就指定初始值,而且这个初始值在编译的时候就能确定下来(比如:18、"aaaa"、16.78等一些直接量), 那么该final修饰的类变量不在是一个变量,系统就会当成“宏变量”处理(就是我们常说的常量),如果在编译的时候就能确定初始值,则就不会被提到静态 块中进行初始化了,直接在类定义中直接使该初始值代替掉final变量。我们还是举那个年龄减去year的例子:

01packagecom.zlc.array;


02


03classTestStatic {


04//类成员 DEMO TestStatic实例


05finalstaticTestStatic DEMO = newTestStatic(15);


06//类成员 age


07finalstaticintage = 20;


08//实例变量 curAge


09intcurAge;


10


11publicTestStatic(intyears) {


12// TODO Auto-generated constructor stub


13curAge = age - years;


14}


15}


16publicclassTest{


17publicstaticvoidmain(String[] args) {


18System.out.println(TestStatic.DEMO.curAge);


19TestStatic static1 = newTestStatic(15);


20System.out.println(static1.curAge);


21}


22}



    这个时候的age 被final修饰了,所以在编译的时候,父类中所有的age都变成了20,而不是一个变量,这样输出的结果就能达到我们的预期。
    特别是在对字符串进行比较的时候更能显示出

01packagecom.zlc.array;


02


03publicclassTestString {


04staticString static_name1 = "java";


05staticString static_name2 = "me";


06staticString statci_name3 = static_name1+static_name2;


07


08finalstaticString final_static_name1 = "java";


09finalstaticString final_static_name2 = "me";


10//加final 或者不加都行 前面兩個能被宏替換就行了


11finalstaticString final_statci_name3 = final_static_name1+final_static_name2;


12


13publicstaticvoidmain(String[] args) {


14String name1 = "java";


15String name2 = "me";


16String name3 = name1+name2;


17//(1)


18System.out.println(name3 == "javame");


19//(2)


20System.out.println(TestString.statci_name3 == "javame");


21//(3)


22System.out.println(TestString.final_statci_name3 == "javame");


23}


24}



    用final修饰方法和类没有什么好说的,只是一个不能被子类重写(和private一样),一个不能派生子类。
    用final修饰局部变量的时候,Java要求被内部类访问的局部变量都是用final修饰,这个是有原因的,对于普通局部变量而言,它的作用域 就停留在该方法内,当方法结束时,该局部变量也就消失了,但内部类可能产生隐式的“闭包”,闭包使得局部变量脱离他所在的方法继续存在。
     有时候在会在一个方法里面new 一个线程,然后调用该方法的局部变量,这个时候需要把改变量声明为final修饰的。




赞(0)    操作        顶端 
总帖数
1
每页帖数
101/1页1
返回列表
发新帖子
请输入验证码: 点击刷新验证码
您需要登录后才可以回帖 登录 | 注册
技术讨论