1.对象和类的简单解析

1.对象和类的简单解析

1.1.对象的简单内存

堆(Heap)

此内存区域的功能是存放对象的实例,存放由new创建的对象或者内存数组,在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。几乎所有的对象都在这里分配,注意:对象的属性跟局部变量可是不一个概念,局部变量存储在栈中,对象的属性存在堆中

在堆中产生一个数组或对象之后,还可以在栈中定义一个特殊的变量p1,让栈中的这个变量赋值等于数组或对象在堆内存中的首地址。

例如:Person p1 = new Person();

栈中的这个变量就是数组或对象的引用变量也就是引用类型的数据,保存的是对象在堆中的首地址,以后就可以在程序中使用栈中的变量p1来访问堆中的数组或者对象,引用类型的变量就相当于为在堆内存中的数组或者对象所在的地址起的一个别名(可以理解下C语言的指针),便于我们程序的理解,总不能直接用内存地址作为变量使用,可读性太差。引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其他作用域之外后边释放。而数组和对象本省在堆中分配,即使程序运行到使用new产生的数组或者对象的语句所在的代码块之外,数组和对象本省占据的内存不会被释放。数组和对象在没有引用变量指向它的时候,才变为垃圾,不能再被使用,在随后的一个不确定时间被垃圾回收器收走(释放掉)。这也是Java比较占内存的原因,实际上,栈中的变量指向堆内存中的变量,这就是Java中的指针。

  • 栈(Stack),虚拟机栈

    一般是指虚拟机栈,此处内存用于存储局部变量(方法中的变量都是局部变量,所以栈中保存的可以是基本数据类型的变量,也可以是对象的首地址这种引用类型的变量)等,局部变量表存放了编译期可知长度的各种基本数据类型(),对象引用(reference类型,它不是对象本身,是对象在堆内存中的首地址)

    在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配,当在一段代码块定义一个变量时,Java 就在栈中为这个变量分配内存空间,当超过变量的作用域后(比如,在函数A中调用函数B,在函数B中定义变量a,变量a的作用域只是函数B,在函数B运行以后,变量a会自动被销毁。分配给它的内存会被回收),Java会自动释放掉为该变量分配的内存空间,该内存空间可以立即另做他用。

  • 方法区(Method Area)

    用于存储已经被类加载器加载的类信息,常量,静态变量等数据;

  • 本地方法栈

  • 程序计数器

2.1.程序分析

  1. 实例1:
package com.ethan.objandthis1;

public class Demo2 {
    public static void main(String[] args) {
        Person per1 = new Person() ;
        Person per2 = new Person() ;
        per1.name="张三" ;
        per1.age=30 ;
        per2.age=33 ;
        per1.tell();
        per2.tell();
    }
}


class Person {
    String name;
    int age;
    boolean isMale;
    public void tell() {
        System.out.println("姓名:"+name+",年龄:"+age);
    }
}

对上面的程序进行内存分析如下图:

  1. 实例2:
package com.ethan.objandthis1;

public class Demo2 {
    public static void main(String[] args) {
        Person per1 = new Person() ;
        Person per2 = new Person() ;
        per1.name="张三" ; 
        per2.name="李四" ;
        Person per3 = per1;//注意
        per1.age=30 ;
        per2.age=33 ;
        per3.age=20;//注意
        System.out.println(per1.age);//注意
    }
}


class Person {
    String name;
    int age;
    boolean isMale;
    public void tell() {
        System.out.println("姓名:"+name+",年龄:"+age);
    }
}

对上面的程序进行内存分析如下图:

总结:

  1. 实际上所谓的引用传递,就是将一个堆内存空间的使用权交给多个栈内存空间,每个栈内存空间都可以修改堆内存空间的内容;
  2. 引用类型就是指同一个堆内存可以被多个栈内存指向就如同上面的per3和per1,两个引用变量都可以操作堆内存中的同一个对象per1;
  3. 通过new操作,才能实例化对象,也就是在堆中为对象开辟内存空间,而上述的per3操作只是声明了一个引用变量;
  4. 在Java中主要存在4块内存空间,这些内存的名称及作用如下:

  1) 栈内存空间:保存所有对象的名称。

  2)堆内存空间:保存每个对象的具体属性内容。

  3)全局数据区:保存static类型的属性值。

  4)全局代码区:保存所有的方法定义。

3.1.属性(成员变量)和局部变量的对比

相同点:

  1. 都是变量,变量的先声明后使用,定义变量的格式,都有其对应的作用域;

不同点:

  1. 在类中定义和声明的位置不一样

    属性:直接定义在类中;

    局部变量:声明在方法内,形参,代码块,构造器参数,构造器内部变量

  2. 属性需要加权限修饰符,指明其权限范围,private,protected,默认,public

  3. 属性有默认初始化值,局部变量没有默认赋值,意味着我们调用时候需要显式赋值,声明局部变量时必须初始化值

  4. 在内存中加载的位置不一样

    1. 属性(非static类型的属性)加载到堆空间;
    2. 局部变量加载到栈空间;

以下学习来自bravo1988 - 知乎 (zhihu.com)的java小册!!!

4.1. 关于java中方法的引用

我们知道,在Java中对象是在堆空间中生成的,属性产生赋值的数据会在堆空间占据一定内存开销。而方法只有一份。

那么,方法为什么被设计成只有一份呢?

因为多个个体,属性可能不同,比如我身高180,你身高150,我18岁,你30了。但我们都能跑、能跳、能吃饭,这些技能(method)都是共通的,没必要和属性数据一样单独在堆空间各存一份,所以被抽取出来存放,可以减少内存的开支(猜测)

此时,方法相当于一套指令模板,谁都可以传入数据交给它执行,然后得到执行后的结果返回。

但此时会存在一个问题:张三这个对象调用了eat()方法,你应该把饭送到他嘴里,而不是送到李四嘴里。那么方法如何知道把饭送到哪里呢?

换句话说:共性的方法如何处理不同对象的特定的数据?

Java的this其实就是解决这个问题的。可以理解为一个对象内部一直持有一个引用,这个引用就是this,当你调用某个方法时,必须传递这个对象引用,然后方法根据这个引用就知道当前这套指令是对哪个对象的数据进行操作了。

2. static与this

我们都知道,static修饰的属性或方法其实都是属于类的,是所有对象共享的。但接触Python后我多了一层理解。之所以一个变量或者方法要声明为static,是因为

  • static变量:大家共有的,大家都一样,不是特定的差异化数据
  • static方法:这个方法不处理差异化数据

也就是说,static注定与差异化数据无关,即与具体对象的数据无关。

静态方法为例,当你确定一个方法只提供通用的操作流程,而不会在内部引用具体对象的数据时,你就可以把它定为静态方法。

这个其实和我们之前听到的解释不一样。网络上一贯的解释都是上来就告诉你静态方法不能访问实例变量,再解释为什么,是倒着解释的。而上面这段话的出发点是,当你满足什么条件时,你就可以把一个方法定为静态方法。

反过来解释,为什么Java中静态方法无法访问非静态数据(实例字段)和非静态方法(实例方法)。因为Java不会在调用静态方法时传递this,静态方法内没有this当然无法处理实例相关的一切。

3. 关于子类和父类

首先看一个demo关于子类和父类生成对象时候:

public class Demo {

    public static void main(String[] args) {
        /**
         * new一个子类对象
         * 我们知道,子类对象实例化时,会隐式调用父类的无参构造
         * 所以Father里的System.out.println()会执行
         * 猜猜打印的内容是什么?
         */
        Son son = new Son();

        Daughter daughter = new Daughter();
    }

}

class Father{
    /**
     * 父类构造器
     */
    public Father(){
        // 打印当前对象所属Class的名字
        System.out.println(this.getClass().getName());
    }
}

class Son extends Father {
}

class Daughter extends Father {
}

首先我们需要明确的知道,子类在生成对象调用构造器时候,是肯定会默认调用父类的无参构造器,隐式的调用super(),但是我们一定要知道:并没有生成父类对象。

下面的demo是便于理解this以及父类和子类的关系,无法正常编译。

class Father{
    /** this参数名接收参数值, 
    this默认指向父类自身, 
    如果this是通过子类对象super(this)传入的, 那么this当然指向该子类对象
    当然这种写法不合语法, 只是为了方便表达, Java做了相应的隐式处理
    */
    public Father(this){
        System.out.println(this.getClass().getName());
    }
}
class Son extends Father {
    public Son(this) {
        //把指向自身的this传给父类构造方法
        super(this);
    }
}

重点的理解:!!!

生成子类对象时候,虽然是调用了父类的无参构造器,但是并不意味着生成了父类对象,调用了父类构造器的目的是初始化了一些必要的属性, 并没有创建父类对象,有自己独立空间的才是一个对象, 此时父类初始化的属性都在子类对象所属的空间里面, 所以并没有创建出父类对象。 this只指向子类对象。

一个子类对象的实例会包含其所有基类所声明的字段,外加自己新声明的字段。那些父类声明的字段并不构成一个完整的父类的实例。super()是让父类封装对自己所声明的字段做初始化的手段。"

posted @ 2021-07-02 12:46  ethanSung  阅读(218)  评论(0编辑  收藏  举报