码迷,mamicode.com
首页 > 编程语言 > 详细

java 对象与内存

时间:2016-04-10 15:02:17      阅读:314      评论:0      收藏:0      [点我收藏+]

标签:

java内存管理分为两个方面:内存分配和内存回收

不能随意挥霍java的内存分配,会造成java程序的运行效率低下:


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

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


一、. 实例变量和类变量的内存分配

  

  java程序的变量大致分为成员变量和局部变量。局部变量分为3类:


形参:在方法中定义的局部变量,由方法调用者负责为其赋值,随方法的结束而消失

方法内的局部变量: 在方法内定义的局部变量,必须在方法内对其进行显示初始化。这种变量从初始化完成后开始生效,随方法的结束而消失。

代码块内的局部变量:在代码块中定义的局部变量,必须在代码块内对其进行显示初始化。这种类型的局部变量从初始化完成后开始生效,随代码块的结束而消失。


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


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

static只能修饰类里定义的成员部分,包括成员变量、方法、内部类、初始化快、内部枚举类。

int    num1=num2-1;

int     num2=3;

这会出现非法前向引用,因为第一行中需要用到num2的值来对num1进行赋值,但num2在第二行才进行赋值。

int    num1=num2-1;

static int    num2=3;

这样就不会出错,因为num1是实例变量,而num2是类变量,类变量的初始化时机总是处于实例变量的初始化时机之前,所以正确。


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


class person

{

   string name; 

   int age;

  static int eyenum;//类变量

}    

 public class fieldtest

{

    public static void main (string [ ] args)

      {

      person.eyenum=2; //对类变量初始化,通过person

     person p=new person();

      p.name="猪八戒“;

     p.age=300;

      person  p2=new person();

      p2.name="孙悟空”;

   p2.age=500;

p2.eyenum=3;  }

}

java分配内存的情况如下:

技术分享技术分享

当程序创建person的对象p时,系统不再为eyenum类变量分配内存空间,而只为person对象的实例变量执行初始化,因为实例变量才是属于person实例,而类变量是属于person类本身的。

技术分享


当person类的eyenum类变量被修改之后,程序通过p,p2,person类访问eyenum类变量都将输出3.


2.实例变量的初始化时机


程序可以在3个地方对实例变量执行初始化:

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

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

构造器中对实例变量指定初始化;


前两种比第三种更早执行。但第一、二种的执行顺序与它们在源程序中的排列顺序相同。


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;

}

 public class fieldtest

{

    public static void main (string [ ] args)

       {      

             cat  cat1=new cat("kitty",2);

          cat cat2=new cat("niko",3);

              }

}

程序执行的时候会先执行cat中的非静态初始化块(因为该类中没有类变量),再调用该cat类的构造器来初始化该cat的实例,weight的值是2.3而不是2。这是因为,初始化中指定初始值,定义weight时指定初始值,都属于对该实例变量执行的初始化操作,它们的执行顺序与它们在源代码中的排列顺序相同。所以,初始化块中对weight所指定的初始化的值每次都将被2.3所覆盖。


总结:定义实例变量时定义的初始值、初始化块中为实例变量指定的初始值、构造器中为实例变量指定的初始值,三者的作用完全类似,都用于对实例变量指定初始值。经过编译器处理之后,它们对应的赋值语句都被合并到构造器中,在合并过程中,构造器中的语句都在前面那些语句的后面,而前两种情况的执行顺序,和它们源代码中的顺序一样。


3. 类变量的初始化时机

  

  程序可以在2个地方对类变量进行初始化:

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

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


程序执行的时候,首先为所有类变量分配内存空间,再按源代码中的排列顺序执行静态初始化块中所指定的初始值和定义类变量时所指定的初始值。


二、父类构造器


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

class creature 
{
{
System.out.println("cresature的非静态初始化块");
}
public creature()
{
System.out.println("cresature的无参构造器");
}
public creature(String name)
{
this();
System.out.println("cresature的带有name参数的构造器,name参数:"+name);
}
}
class animal extends creature
{
{
System.out.println("animal的非静态初始化块");
}
public animal(String name)
{
super(name);
System.out.println("animal的带参数nmae的构造器");
}
public animal(String name, int age)
{
this(name);
System.out.println("animale的带两个参数的构造器");
}
}
class wolf extends animal
{
{
System.out.println("wolf的非静态初始化块");
}
public wolf()
{
super("灰太狼",3);
System.out.println("wolf的无参的构造器");
}
public wolf(double weight)
{
this();
System.out.println("wolf的带weight参数的构造器"+weight);
}
}
public class inittest {
public static void main(String[] args)
{
new wolf(5.6);
}
}

上面程序定义了三个类,都包含非静态初始化、构造器成分。其执行结果如下

cresature的非静态初始化块
cresature的无参构造器
cresature的带有name参数的构造器,name参数:灰太狼
animal的非静态初始化块
animal的带参数nmae的构造器
animale的带两个参数的构造器
wolf的非静态初始化块
wolf的无参的构造器
wolf的带weight参数的构造器5.6


只要在程序创建java对象的时候,系统总是先调用最顶层父类的初始化操作,包括初始化块和构造器,然后依次向下调用所以父类的初始化操作,最后执行本类的初始化操作返回本类的实例。至于调用父类的那儿一个构造器初始化,分为几种情况:

子类构造器执行体的第一行代码使用super()显示调用父类构造器

子类构造器执行体的第一行代码使用this()显示调用本类的重载构造器,系统根据this调用里传入的实参列表来确定本类的另一个构造器

子类中既没有super也没有this,则隐式调用父类中无参数构造器

super和this都只能在构造器中使用,且都必须在构造器中的第一行,一个构造器中只能使用其中一个,最多使用一次。


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


子类的方法可以访问父类的实例变量,这是因为子类继承了父类。父类的方法不能访问子类的实例变量,因为父类无法知道会被哪儿个子类继承。但是在一些极端的情况下,可能出现父类访问子类变量的情况。


构造器只是负责对java对象实例变量执行初始化,即赋值,在执行构造器代码之前,该对象所占的内存已经被分配下来,这些内存里值都默认是空值。当this在构造器中时,this代表正在初始化的java对象,


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


2.3调用被子类重新的方法


一般情况下,父类不能调用子类的方法,但是有一种特殊情况,但子类方法重新父类的方法之后,父类表面上只是调用自己的方法,但实际是调用了子类的方法。

class animal 
{
private String desc;

public animal()
{
this.desc=getDesc();
}
public String getDesc()
{
return"animal";       //2
}
public String toString()
{
return desc;
}
}
class wolf extends animal
{
private String name;
private double weight;
public wolf(String name, double weight)
{
this.name=name;             //3
this.weight=weight;
}
@Override
public String getDesc()
{
return "wolf[name="+name+",weight="+weight+"]";
}
}
public class inittest {
public static void main(String[] args)
{
System.out.println(new wolf("灰太狼",32.3));//1
}
}

上面的程序输出的结果是wolf[name=null,weight=0.0]  


子类重写了父类的getDesc()函数,程序执行的时候,首先是执行1部分程序,也就是调用构造器来初始化wolf对象,但是会先调用父类的无参数构造器先初始化,也就是第二部分的程序,先于3被执行,父类的无参构造器中的函数被子类重新,于是调用子类的方法,于是输出这样的结果。执行完2部分的程序之后,会执行3部分的程序,wolf对象会对name, weight进行赋值,但是desc实例变量的值是wolf[name=null,weight=0.0]


为了避免这种不希望看到的结果,应该避免在animal类 的构造器中调用被子类重写的方法,因此改为

class animal 
{
private String desc;

public String getDesc()
{
return"animal";       //2
}
public String toString()
{
return  getDesc();
}
}

结果就会是你期待的wolf[name=灰太狼,weight=32.3]



2.3 父子实例的内存控制




class base
{
int count=2;
public void display()
{
System.out.println(this.count);
}
}
class derived extends base
{
int count=20;
@Override
public void display()
{
System.out.println(this.count);
}

}
public class inittest {
public static void main(String[] args)
{
base b=new base(); //1
System.out.println(b.count);
b.display();
derived d= new derived();
System.out.println(d.count);
d.display();
base bd=new derived();
System.out.println(bd.count);
bd.display();
base db2=d;
System.out.println(db2.count);
}
}

输出的结果为

2
2 // 输出结果毫无疑问,声明一个base变量b
20
20//  也是没有问题的
2
20//  声明了一个base变量,但是却将derived对象赋给变量。此时系统会自动进行向上转型来保证程序正确。直接通过db访问count实例变量,输出的将是base(声明时的类型)对象的count实例变量的值;如果通过db来调用display()方法,该方法将表现出derived(运行时的类型)对象的行为方法。
2// 此时db2和d都指向同一个对象,但是db2是base变量,所以通过它访问实例变量时,显示的是声明类型的行为,所以是2而不是20


但不管是d 变量,还是db,db2,只要它们实际指向一个derived对象,不管声明它们时用什么类型,当通过这些变量调用方法时,方法总是表现出derived对象的行为,而访问实例变量时,表现的是声明变量类型的行为


如果在子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中。对于实例变量则不存在这样的现象,即使子类中有与父类相同的实例变量,这个实例变量也不会覆盖父类中的。因此继承成员变量和继承方法之间存在这样的差别。


2.3 父子类的类变量

由于类变量属于类本身,而实例变量属于对象,所以类变量不会那么的复杂。java允许通过对象来访问类变量,如果在子类中直接访问父类中定义的count类变量,可以直接使用父类名.count或者super.count,建议用前一种。


2.4 final修饰符

final可以修饰变量,被final修饰的变量被赋值初始值之后,不能对它重新赋值

final可以修饰方法,被final修饰的方法不能被重写

final可以修饰类,被修饰的类不能派生子类


对于普通实例变量,java程序可以对它执行默认的初始化,也就是将实例 变量的值指定为默认的初始值0或null;但对于final实例变量,则必须由程序员显示指定初始值。


只能在3个地方对final变量指定初始值;

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

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

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


对于final类变量而言,同样必须显示指定初始值,而且只能在2处初始化:

定义final类变量时

在静态初始化块中为final类变量赋值


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


final修饰符的一个重要用途就是定义“宏变量"。如果被赋的表达式只是基本的算术表达式或字符串连接运算,没有访问普通变量,调用方法,会将其看成宏变量处理。


但对于final实例变量而言只有在定义该变量时指定初始值才会有”宏变量“的效果。


public class inittest {
public static void main(String[] args)
{
String s1="赖yuppies";
String s2="赖"+"yuppies";
System.out.println(s1==s2);
String str1="赖";
String str2="yuppies";
String s3=str1+str2;
System.out.println(s1==s3);

}
}

输出的结果为true  false 

s1是一个直接量,s2的值是两个字符串直接量进行连接运算,由于编译器可以在编译阶段就确定s2的值,所以系统会让s2直接指向字符串池中缓存中的”赖yuppies"字符串。str1,str2是两个变量,在编译的时候,s3的值不能确定,所以是false.


可以将str1,str2修饰为final类型。


如果程序需要在匿名内部类中使用局部变量,那么这个局部变量必须使用final修饰符修饰。


java要求所有被内部类访问的局部变量都使用final修饰的原因:对于局部变量而言,它的作用域停留在该方法中,当执行方法结束时,该局部变量也随之消失;但内部类则可能产生隐式的“闭包”,闭包将使得变量脱离它所在的方法继续存在。



java 对象与内存

标签:

原文地址:http://blog.csdn.net/yuppies_coldplay/article/details/51097438

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!