Java提高——对象与内存控制
实例变量和类变量
Java内存管理分为两个方面:内存分配和内存回收。
内存分配是特指创建Java对象时,JVM为该对象在堆内存中所分配的内存空间
内存回收是指当Java对象失去引用,变成垃圾时,JVM的垃圾回收机制自动清理该对象,并回收该对象所占的内存。
成员变量和局部变量(作用时间短,存在方法的栈内存中)。
类体内定义的变量被成为成员变量。如果定义该成员变量时没有使用static修饰,那么该成员变量又被称为非静态变量或实例变量;如果使用了static修饰,则该成员变量被称为静态变量或类变量。
static只能修饰在类里定义的成员部分(变量方法,内部类,初始化块---),如果没有用static修饰这些类里的成员,这些成员属于该类的实例;如果用了,这些成员变量就属于类本身。
类变量的初始化时机总是处于实例变量的初始化时机之前。如:
int i = n1 +1; int n1 = 2;
非法
int i = n1 +1; static int n1 = 2;
合法
实例变量和类变量的属性
使用static修饰的成员变量是类变量,属于类本身;没有用static修饰的成员变量是实例变量,属于该类的实例
在同一个JVM中,每个类对应一个Class对象,但每个类可以创建多个java对象
由于同一个JVM内每个类只对应一个Class对象,因此同一个JVM内的类的类变量只需一块内存空间;但对于实例变量而言,每创建一次实例,就需要为实例变量分配一块内存空间。也就是说程序中有几个实例,实例变量就需要几块内存空间。
class Perso{ String name; int age; static int eyeNum; @Override public String toString() { return "Perso{" + "name='" + name + '\'' + ", age=" + age + '}'; } } public class FieldTest { public static void main(String[] args) { //类变量属于类本身,只要类初始化完成,程序就可以使用类变量 Perso.eyeNum = 2; //通过Perso类访问类变量 System.out.println(Perso.eyeNum); //创建第一个Perso对象 Perso p = new Perso(); p.age=12; p.name="lura"; //通过p访问Perso类的类变量——不能实现通过实例变量访问静态变量 System.out.println(p.eyeNum); } }
实例变量的初始化
每次创建Java对象都需要为实例变量其分配内存空间,并执行初始化。
初始化的地方:1)定义实例变量时指定初始值;2)非静态化块中指定初始值;3)构造器中指定初始值
(Java对象的3中初始化方式)
public class Cat { //定义时指定初始值 String name = "cc"; int age; //构造函数指定初始值 public Cat(int age) { this.age = age; } //非静态初始化块中指定初始值 { age = 2; } }
三种初始化方式作用完全类似,但是经过编译处理后,对应的赋值语句都会被合并到构造器中。且过程中,前两者总是在构造器之前。
类变量的初始化
只有当Java类被初始化时才会为类变量分配内存空间执行初始化。
1)定义类变量时执行初始化;2)静态初始化块中指定初始值
public class StaticInitTest { //定义类变量,定义时指定初始值 static int num = 3; static int mu ; //通过静态块为类变量指定初始值 static { mu = 5; } }
先为所有类变量分配内存空间,再按照源码中的顺序执行定义类变量时所指定的初始值和静态初始化块中所指定初始值。
父类构造器
当创建任何Java对象的时候,程序总会先依此调用每个父类非静态初始化块、父类构造器(总是从Object开始)执行初始化,最后才调用本类的非静态初始化块、构造器执行初始化。
显示调用和隐式调用
当调用某个类的构造器来创建Java对象的时候,系统总会先调用父类的非静态初始化块进行初始化。这个调用时隐式的,而且父类的静态初始化块总是会被执行。接着会调用父类的一个或多个构造器进行初始化,这个调用既可以通过super进行显示调用,也可以是隐式调用。
class Creature{ { System.out.println("Creature非静态初始化块"); } //下面定义两个构造器 public Creature(){ System.out.println("Creature无参构造器"); } public Creature(String name){ //通过this调用另一个重载、无参构造器 this(); System.out.println("Creature带有name参数的构造器,name参数:"+name); } } class Annimal extends Creature{ { System.out.println("Annimal的非静态初始化块"); } public Annimal(){ System.out.println("Annimal的无参构造器"); } public Annimal(String name){ //通过super调用父类的有参构造器 super(name); System.out.println("Annimal带一个参数的有参构造器,name:"+name); } public Annimal(String name,int age){ //使用this调用另一个重载的构造器 this(name); System.out.println("Annimal带两个参数的构造器,name:"+name+",age:"+age); } } class Wolf extends Annimal{ { System.out.println("Wolf的非静态初始化块"); } public Wolf(){ //显示调用父类有两个参数的构造器 super("灰太狼",3); System.out.println("Wolf无参构造器"); } public Wolf(double weight){ //用this调用另一个重载的构造器 this(); System.out.println("Wolf带有参的构造器,weight:"+weight); } } public class InitTest { public static void main(String[] args) { new Wolf(5.6); } }
子构造器使用super显示调用父类的构造器,系统根据传入参数确定调用哪一个;子构造器使用this显示调用本类中的重载构造器,参数确定调用哪一个;如果子构造器中没有super和this,系统将会在执行子类构造器之前,隐式的调用父类无参构造器。(super、this都只能在构造器中使用,而且必须作为构造器中的第一行代码,因此构造器中只能二选一使用)
Java对象不是由构造器创建的,构造器只负责初始化(就是赋值)。在执行构造器代码之前,该对象所站的内存已经被分配下来了。
当变量的编译时类型和运行时类型不同时,通过该变量访问它引用的对象的实例变量时,该实例变量的值由声明该变量的类型决定。但通过该变量调用它引用的对象的实例方法时,该方法行为将由它实际所引用的对象决定。
在访问权限允许的情况下,子类可以调用父类的方法,这是因为子类继承父类,会获得父类定义的成员变量和方法;但子类不能调用子类的方法,因为它无从知晓那个子类继承了它。——但是有一种特殊情况,当子类重写了父类的方法之后,父类表面上只是调用属于自己的、被子类重写的方法,但随着执行context的改变,将会变成父类实际调用子类的方法。如:
class Annimal{ //desc实例变量保存对象toString方法的返回值 private String desc; public Annimal(){ //调用getDesc方法实例化desc变量 this.desc = getDesc(); } public String getDesc() { return "Annimal"; } @Override public String toString() { return desc; } } public class Wolf extends Annimal { //定义name、weight两个变量 private String name; private double weight; public Wolf(String name, double weight) { //为两个实例变量赋值 this.name = name; this.weight = weight; } @Override public String getDesc(){ return "name: "+name+",weight: "+weight; } public static void main(String[] args) { System.out.println(new Wolf("灰太狼",23.3)); } }
name: null,weight: 0.0 ——得到的结果就是因为父类调用了子类的构造器。为避免此种情况,应避免在父类的构造器中调用被子类重写过的方法。
父子实例的内存控制
继承成员变量和继承方法的区别:
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 FieldAndMethodTest { public static void main(String[] args) { //声明并创建一个Base对象 Base b = new Base(); //直接访问实例变量和通过display来访问实例变量 System.out.println(b.count); b.display(); //声明并创建一个Derived对象 Derived d = new Derived(); //直接访问count实例变量和通过display来访问count变量 System.out.println(d.count); d.display(); //声明一个Base变量,将Derived对象赋给变量 Base bd = new Derived(); //直接访问count实例变量和通过display访问实例变量 System.out.println(bd.count); bd.display(); //让d2b变量指向原d所指向的Derived对象 Base d2b = d; //访问d2b指向对象的count实例变量 System.out.println(d2b.count); } }2
2
20
20
2
20
2
对于b,d没什么好说的。对于bd程序会自动向上转型保证程序的正确性,直接通过bd访问count实例变量,输出的将是Base(声明时的类型)对象的count实例变量的值;如果通过bd来调用display方法,该方法将表现出Derived(运行时类型)对象的行为。d2b的结果表明表明所指向的对象中包含了两块内存。
如果子类重写了父类的方法,就意味着子类里定义的方法彻底覆盖了父类里同名的方法,系统将不可能把父类里的方法转移到子类中。对于实例变量则不存在这种现象,即使子类中定义了与父类完全同名的实例变量,这个实例变量依然不可能覆盖父类的实例变量。——因为继承实例变量和方法间的差别,所以对于一个引用类型的变量而言,当通过该变量访问它所引用的对象的实例变量时,该实例变量的值取决于声明该变量时类型;当通过该变量来调用它所引用的对象的方法时,该方法行为取决于它所实际引用的对象的类型。
对于父、子类对象在内存中存储:当程序创建一个子类对象时,系统不仅会为该类中定义得实例变量分配内存,也会为其父类中定义的所有实例变量分配内存,即使子类中定义了与父类中同名的实例变量。也就是说,当系统创建一个Java对象时,如果该Java类有两个父类(直接父A,间接父B),实例变量A有两个,B有3个,当前类有2个,那么Java对象将会保存2+3+2个对象。
如果在子类中定义了与父类已有变量同名的变量,那么子类中定义的变量会隐藏父类中定义的变量。注意不是完全覆盖,因此系统为子类创建对象时,仍然会为父类中定义的、被隐藏的实例分配内存空间。
对于类变量可以通过super和类名来访问父类定义的类变量。
final修饰符
final修饰的实例变量:被final修饰的实例变量必须显示的指定初始值。而且只能在如下3个地方指定初始值:
1)定义final实例变量时指定初始值;2)在非静态初始化块中为final实例变量指定初始值;3)在构造器中为final实例变量指定初始值
public class FinalTest { //定义final变量时赋初值 final int var1 = "java".length(); //在初始化块中为final变量赋初值 final int var2; { var2 = "轻量级Java实战应用".length(); } //在构造器中为final变量赋初值 final int var3; public FinalTest(){ this.var3 = "疯狂Java讲义".length(); } }
final变量必须被显示的赋初值,而且本质上final实例变量只能在构造器中被赋初值(因为所有方法本质是一样的),除此之外,final实例变量将不能被再次赋值。
final修饰的类变量:同样必须显示的指定初始值,而且final类变量只能在两个地方指定初始值:
1)定义final变量指定初始值;2)在静态初始化块中为final类变量指定初始化值
//定义final类变量时指定初始化值 final static int va4 = "你牛逼".length(); //在静态初始化块中为final类变量指定初始化值 final static int var5; static { var5 = "哈哈".length(); }
本质上final类变量只能在静态初始化块中被赋初值。
被final修饰的变量一旦被赋初值,final变量的值以后将不会被改变。
对于一个使用final修饰的变量而言,如果定义该final变量时就指定初始值,而且这个初始值可以在编译时就确定下来,那么这个final变量将不在是一个变量,系统会将其当成“宏变量”处理,也就是说所有出现该变量的地方,系统将直接把它变成对应的值处理。
final方法不能被重写
当final修饰某个方法时,用于限制该方法不可被它的子类重写。
class B{ final void info(){} } public class A extends B{ //试图重写父类的final方法出现错误 void info(){} }
内部类中的局部变量:如果程序需要在匿名内部类中使用局部变量,那么这个局部变量必须使用final修饰。