疯狂Java笔记之对象及其内存管理

(复习疯狂Java的笔记)

1.实例变量和类变量

Java程序的变量大体可分为成员变量和局部变量。其中局部变量可分为如下二类。

  • 形参:在方法签名中定义的局部变量,由方法调用者负责为其赋值,随方法的结束而消亡。
  • 方法内的局部变量:在方法内定义的局部变量,必须在方法内对其进行显式初始化口这种类型的局部变量从初始化完成后开始生效,随方法的结束而消亡。
  • 代码块内的局部变量:在代码块内定义的局部变量,必须在代码块内对其进行显式初始化。这种类型的局部变量从初始化完成后开始生效,随代码块的结束而消亡。

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

类体内定义的变量被称为成员变量〔英文是Field)。如果定义该成员变量时没有使用static
修饰,该成员变量又被称为非静态变量或实例变量;如果使用了static修饰,则该成员变量又可被称为静态变量或类变量

(坑:表面上看定义成员变量是没有先后顺序的,实际上还是要采用合法的前向引用)如:

int num=num2+1;
int num2=2;

是会报错的,出得num2位静态比变量的时候。

2.实例变量和类变量的属性

使用static修饰的成员变量是类变量,属于该类本身:没有使用属于该类的实例。在同一个JVM内,侮个类只对应一个
Java对象口static修饰的成员变量是Class对象,但侮个类可以创建多个

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

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

对于实例变量,它是Java对象本身。每创建Java对象时都需要为实例变量分配内存空间,并对实例进行初始化。
程序可以在三个地方进行初始化:

  • 定义实例变量时指定初始值。
  • 非静态初始化块中对实例变量指定初始值。
  • 构造器中对实例变量指定初始值。
    其中第1,2种方式都比在构造器初始化更早执行,当第1,2种的执行顺序与他们在源程序中的排列顺序相同。

4.类变量的初始化时机

类变量是属于Java类本身。从程序运行的角度来看,每个jvm对一个Java类只初始化一次,因此只有每次运行Java程序时,才会初始化该Java类,才会为该类的类变量分配内存空间,并执行初始化。

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

  • 定义类变量时指定初始值。
  • 静态初始化块中对类变量指定初始值。

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

父类构造器

1.隐式调用和显式调用

当创建Java对象时,系统会先调用父类的非静态初始化块进行初始化。而这种调用是隐式调用。而第一次初始化时最优先初始化的是静态初始化块。接着会调用父类的一个或多个构造器进行初始化,这个调用是用过super()的方法来显式调用或者隐式调用。当所有父类初始化完之后才初始化子类。实例代码如下:

class Animal{
    
    static{
        System.out.println("Animal静态初始化块");
    }
    
    {
       System.out.println("Animal初始化块");
    }
        
    public Animal(){
        System.out.println("Animal构造器");
    }
    
    
}

class Cat extends Animal{
    public Cat(String name,int age){
        super();
        System.out.println("Cat构造器");
    }
    
    static{
        System.out.println("Cat静态初始化块");
    }
    
    {
        System.out.println("Cat初始化块");
        weight=2.0;
    }
    
    double weight=2.3;

    public String toString(){
        return "weight="+weight;
    }
    
}

public class JavaTest {

    public static void main(String[] args) {
        
        Cat cat=new Cat("kitty",2);
        System.out.println(cat);
//      Cat cat2=new Cat("Garfied",3);
//      System.out.println(cat2);
    }
    
}

输出的结果是:

疯狂Java笔记之对象及其内存管理_第1张图片
java.PNG

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

子类因为继承父类所以可以访问父类的成员方法和变量,当一般情况下父类是访问不了子类的,因为父类不知道哪个子类继承。但是在特殊情况下是可以的,如下代码:

class BaseClass{
    private int i=2;
    public BaseClass(){
        this.display();
    }
    public void display(){
        System.out.println("BaseClass");
        System.out.println(i);
    }
    
}

class Derived extends BaseClass{
    private int i=22;
    public Derived(){
        i=222;
    }
    public void display(){
        System.out.println("Derived");
        System.out.println(i);
    }
    public void sub(){
        System.out.println("sub");
    }
}

public class JavaTest {
    public static void main(String[] args) {
        Derived derived=new Derived();
    }
}

结果如下:

java2.PNG

仔细看代码,好像怎么也不会输出0吧,为什么呢。

首先我们要知道Java构造器只是起到对变量进行初始化的作用,而在执行构造器之前我们的对象已经初始化了,在内存中已经被分配起来了,而这些值默认是空值。

其次this在代表正在初始化的对象,一般看会以为就是BaseClass对象,不过在上面代码里,this是放在BaseClass的构造器里,当时我们是在Derived()构造器执行的,是Derived()构造器隐式调用了BaseClass()构造器的代码,所以在这个情况下是this是Derived对象。所以当我们改为this.sub()时是报错的。

此外这个this的编译类型是BaseClass,所以我们改为this.i的时候输出是2.

所以应该避免在父类构造器中调用被子类重写的方法。

父子实例的内存控制

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

class Animal{
    public String name="Animal";
    
    public void sub(){
        System.out.println("AnimalSub");
    }
    
}
class Wolf extends Animal{
    public String name="Wolf";

    public void sub(){
        System.out.println("WolfSub");
    }
}

public class JavaTest {
    public static void main(String[] args) {
        Animal animal=new Animal();
        System.out.println(animal.name);
        animal.sub();
        Wolf wolf=new Wolf();
        System.out.println(wolf.name);
        wolf.sub();
        Animal sub=new Wolf();
        System.out.println(sub.name);
        sub.sub();
    }
}

结果如下:

疯狂Java笔记之对象及其内存管理_第2张图片
image.png

所以当声明类型为父类,运行类型为子类是,成员变量表现出父类,而方法表现出子类,这就是多态。

2.内存中的子类实例

class Fruit{
    String color="未确定颜色";
    
    public Fruit getThis(){
        return this;
    }
    
    public void info(){
        System.out.println("Fruit方法");
    }
    
}

public class JavaTest extends Fruit{
    
    @Override
    public void info() {
        System.out.println("JavaTest方法");
    }
    
    public void AccessSuperInfo(){
        super.info();
    }
    
    public Fruit getSuper(){
        return super.getThis();
    }
    
    String color="红色";
    
    public static void main(String[] args) {
        JavaTest javaTest=new JavaTest();
        Fruit f=javaTest.getSuper();
        
        System.out.println("javaTest和f所引用的对象是否相同:"+(javaTest==f));
        System.out.println("所引用对象的color实例变量:"+javaTest.color);
        System.out.println("所引用对象的color实例变量:"+f.color);
        
        javaTest.info();
        f.info();
        javaTest.AccessSuperInfo();
    }
    
}

当创建一个对象时,系统不仅为该类的实例变量分配内存,同时也为其父类定义的所有实例变量分配内存,即是子类定义了与父类同名的实例变量。也就是说,当系统创建一个Java对象时,如果该Java类有两个父类(一个直接父类A,一个间接父类g ),假设A类中定义了2个实例变量,B类
中定义了3个实例变量,当前类中定义了2个实例变量,那么这个Java对象将会保存2+3十2个实例变量。

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

为了在子类中访问父类定义的,被隐藏的变量和方法,可以使用super来限定修饰这些变量和方法。

3.父,子类的类变量

如果在子类中要访问父类中被隐藏的静态变量和方法,程序有两种方式:

  • 直接使用父类的类名作为主调来访问类变量
  • 使用super.作为限定来访问类变量

一般情况下,都建议使用第一种方式访问类变量,因为类变量属于类本身,使用类名做主调来访问可以较好的可读性

final修饰符

1.final 修饰的变量

final修饰的实例变量必须显示指定初始值,只能在如下三个位置指定初始值。

  • 定义final实例变量时指定初始值
  • 在非静态初始化块中为final实例变量指定初始值
  • 在构造器中为final实例变量指定初始值

对于普通实例java可以指定默认初始化,而final实例变量只能显示指定初始化。

2.执行‘宏替换’的变量

在定义时final类变量指定了初始值,该初始值在编译时就被确定下来,这个final变量本质上已经不再是变量而是一个直接量,如果被赋的表达式只是基木的算术表达式或字符串连接运算,没有访问普通变量,调用方法,Java编译器同样会将这种final变量当成“宏变量”来处理。

3.final方法不能重写

如果父类中某个方法使用了final修饰符进行修饰,那么这个方法将不可能被他的子类访问到,因此这个方法也不可能被他的子类重写。从这个层面说,private和final同时修饰某个方法没有太大的意义,但是被java语法允许。

4.内部类中的局部变量

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

你可能感兴趣的:(疯狂Java笔记之对象及其内存管理)