第八讲 面向对象基础——构造函数、this关键字、static关键字和对象的初始化流程

构造函数

构造函数的作用

可用于给对象进行初始化,对象一建立就会调用与之对应的构造函数。

构造函数的特点

  1. 函数名和类名相同;
  2. 没有返回值类型;
  3. 没有具体的返回值。

构造函数和一般函数的区别

构造函数和一般函数在写法上有不同,在运行上也有不同。构造函数在对象创建时执行,给对象初始化,而且只执行一次。而一般函数是对象调用时才执行,是给对象添加对象具备的功能。一个对象建立,构造函数只运行一次,而一般函数可以被该对象调用多次。你不仅就要问当初new对象时,也没有构造函数啊?那这个对象是怎么初始化的呢?

  • 注意1:定义的每一个类中,都有一个默认的空参数构造函数;
  • 注意2:一旦在类中自定义了构造函数后,默认的构造函数就没有了。

例,

class Car 
{
    // Car() {} // 类中默认的空参构造函数,专门用于创建对象初始化用的
}
class CarDemo 
{
    public static void main(String[] args)
    {
        Car c = new Car();
    }

}

构造函数的细节

  • 细节一:构造函数中有return语句吗?可以有,用于结束初始化的。如:

    Person(String n)
    {
        name = n;
        return;
    }
  • 细节二:一般函数的名字是可以和类名一样的,但是不符合书写规范。如以下代码是可以通过编译的。

    class Person
    {
        void Person() // 可以编译通过,一般函数的名字是可以和类名一样的,但是不符合书写规范
        {
    
        }
    
        void showPerson() {}
    }
  • 细节三:构造函数里面是可以调用一般函数的。
  • 细节四:构造函数是可以私有化的,只在本类中使用,而且构造函数一旦私有化,其他程序就无法创建该类的对象。原因:无法对创建的对象进行初始化。

小结

关于构造函数,总结如下:

  1. 当一个类中没有定义构造函数时,那么系统会默认给该类加入一个空参数的构造函数。当在类中自定义了构造函数后,默认的构造函数就没有了;
  2. 一个类中默认会有一个空参数的构造函数,这个默认的构造函数的权限和所属类一致。如果类被public修饰,那么默认的构造函数也带public修饰符;如果类没有被public修饰,那么默认的构造函数也没有public修饰。简而言之:默认构造函数的权限是随着类的变化而变化的
  3. 什么时候定义构造函数呢?
    答:当分析事物时,该事物存在具备一些特性或者行为,那么将这些内容定义在构造函数中。

根据如下代码,你能画出构造函数的内存图吗?

class Person
{ 
    private String name;
    private int age;

    Person(String n)
    {
        name = n;
    }
    Person(String n, int a)
    {
        name = n;
        age = a;
    }
    public void setName(String n) 
    {
        name = n;
    }
    public String getName()
    {
        return name;
    }
    public void setAge(int a) 
    {
        age = a;
    }
    public int getAge()
    {
        return age;
    }
    public void show() 
    {
        System.out.println("name = " + name + ",age = " + age);
    }

}

class PersonDemo
{
    public static void main(String[] args)
    {
        Person p1 = new Person("xiaoqiang", 30);
        p1.show();
    }
}

这里写图片描述

this关键字

下面我们通过构造函数之间的调用来引出this关键字的讲解。试着思考一下,若构造函数私有,则就表示只在本类中有效,那么该如何访问呢?这里有个注意点,即构造函数只能被构造函数调用,不能直接被一般方法调用。还是回到我们的问题中来,构造函数之间该如何访问呢?答案是通过关键字this来解决。
this看上去,是用于区分局部变量和成员变量同名情况(当成员变量和局部变量同名时,可以通过this关键字区分)的,那么this到底代表的是什么呢?答案是:this代表本类的对象,到底代表哪一个呢?this代表它所在函数所属对象的引用,简单说:哪个对象在调用this所在的函数,this就代表哪个对象。
其实在Java里面有一个机制:只要用对象调用了方法,这个方法里面就持有一个引用,该引用指向哪个对象呢?哪个对象调用我,我就指向谁。总结一点就是:只要是直接被对象调用的方法都持有this引用(或者亦可说凡是访问了对象中的数据的方法都持有this引用)。
根据如下代码,你能画出this关键字在内存中的体现吗?如果你能真能画出,相信你一定对this关键字有了较深的理解。

class Person
{ 
    private String name;
    private int age;

    private Person(String n)
    {
        name = n;
    }
    Person(String n, int a)
    {
        this(n);
        age = a;
    }
    public void show() 
    {
        System.out.println("name = " + name + ",age = " + age);
    }

}

class ThisDemo
{
    public static void main(String[] args)
    {
        Person p = new Person("小明", 21);
        p.show();
    }
}

this关键字的内存图解:
这里写图片描述
this带上参数列表的方式就可以访问本类中的其他构造函数。比如:this("lisi")访问的就是本类中带一个字符串参数的构造函数。还有一点要注意,即用于调用构造函数的this语句必须定义在构造函数的第一行,因为初始化动作要先执行。

this关键字的应用

当定义类中功能时,该函数内部要用到调用该函数的对象时,这时用this来表示这个对象。但凡本类功能内部使用到了本类对象都用this表示。例,判断两人是否为同龄人。

class Person {
    private String name;
    private int age;

    Person(int age) {
        this.age = age;
    }

    Person(String name) {
        this.name = name;
    }

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void speak() {
        System.out.println("name="+this.name+"...age="+this.age);
        this.show();
    }

    public void show() {
        System.out.println(this.name);
    }

    /*
    需求:给人用于比较年龄是否相同的功能,也即是否是同龄人。
    */
    public boolean compare(Person per) {
        return this.age == per.age;
    }
}
class PersonDemo3 {
    public static void main(String[] args) {
        Person p = new Person(20);
        Person p1 = new Person(25);
        boolean b = p1.compare(p2);
        System.out.println(b);
    }
}

static关键字

static关键字是一个修饰符,用于修饰成员(成员变量、成员函数)。当成员被静态修饰后,就多了一个调用方式,除了可以被对象调用外,还可以直接被类名调用:类名.静态成员

static关键字的特点

被static关键字修饰后的成员具备以下特点:

  1. 随着类的加载而加载,类的消失而消失,说明它的生命周期最长;
  2. 优先于对象存在,明确一点:静态是先存在的,对象是后存在的;
  3. 被所有对象所共享;
  4. 可以直接被类名所调用。

static关键字使用注意事项

  1. 静态方法只能访问静态成员,不能访问非静态成员,这就是静态方法的访问局限性。而非静态方法既可以访问静态也可以访问非静态;
  2. 静态方法中不可以定义this、super关键字。因为静态优先于对象存在,所以静态方法中不可以出现this;
  3. 主函数是静态的。

我们知道其使用注意事项之后,那么static关键字在程序开发中,什么时候使用呢?

  1. 成员变量:如果数据在所有对象中的都是一样的,直接静态修饰;
  2. 成员函数:如果函数没有访问过对象中的属性数据,那么该函数就是静态的。

成员变量和静态变量的区别

下面我们着重看一下成员变量和静态变量的区别:

  • 名称上的区别
    成员变量也叫实例变量;
    静态变量也叫类变量。
  • 内存存储上的区别
    成员变量存储在堆内存的对象中;
    静态变量存储在方法区的静态区中。
  • 生命周期不同
    成员变量随着对象的出现而出现,随着对象的消失而消失;
    静态变量随着类的出现而出现,随着类的消失而消失。

下面我通过一个例子的讲解来加深对static关键字的理解。例,有如下代码:

class Person
{
    private String name;
    private int age;

    // public static String country = "CN"; // 全局变量
    static String country = "CN";

    Person(String name, int age)
    {
        this.name = name;
        this.age = age;
    }

    public void show()
    {
        System.out.println("name = " + name + ",age = " + age);
    }
    public static void sleep()
    {
        System.out.println("呼呼");
        // System.out.println("呼呼..." + name); // 无法从静态上下文中引用非静态变量name
    }

}

class StaticDemo
{
    public static void main(String[] args) 
    {
        Person p = new Person("lisi", 21);
        p.show();
        /*
        如果创建对象调用sleep方法,发现,并没有使用对象中的数据。
        该对象的建立是没有意义的。

        该方法所属于Person.class,可以通过类名的方式来访问。
        注意:用类名直接调用的方法必须通过指定修饰符来修饰,就是关键字static
        */
        Person.sleep();
    }
}

static关键字的内存图解:
这里写图片描述

静态代码块

其格式为:

static {
    静态代码块中的执行语句
}

特点是随着类的加载而执行,且只执行一次,并优先于主函数。用于给类进行初始化。其应用场景为:类不需要创建对象,但是需要初始化,这时将部分代码存储到静态代码块中。
运行如下代码,你觉得会输出什么?

class StaticCode {
    static {
        System.out.println("a");
    }
}

class StaticCodeDemo {
    static {
        System.out.println("b");
    }
    public static void main(String[] args) {
        new StaticCode(); // 将StaticCode.class这个类加载进内存 
        new StaticCode(); // 注意StaticCode.class已经加载进内存,不会再次加载
        System.out.println("over");
    }
    static {
        System.out.println("c");
    }
}

输出结果为:

b
c
a
over

注意,若StaticCode s = null;,StaticCode类并没有被加载进内存。只有用到了类中的内容,才涉及类的加载问题,光建立引用,是不会加载的。

构造代码块

其格式为:

{
    构造代码块中的执行语句
}

构造代码块定义的是不同对象共性的初始化内容,其作用是给对象进行初始化,对象一建立就运行,而且优先于构造函数执行。它和构造函数的区别是构造代码块是给所有对象进行统一初始化,而构造函数是给对应的对象初始化。
下面我用一个面试题来加深对静态代码块和构造代码块的理解。运行如下代码,你觉得会输出什么?

class StaticCode {
    int num = 9;
    StaticCode() {
        System.out.println("b");
    }

    static {
        System.out.println("a");
    }

    {
        System.out.println("c"+this.num);
    }

    StaticCode(int x) {
        System.out.println("d");
    }

    public static void show() {
        System.out.println("show run");
    }
}

class StaticCodeDemo {
    public static void main(String[] args) {
        new StaticCode(4);
    }
}

输出结果为:

a
c9
d

静态的应用

什么时候使用静态呢?我们要从两方面下手,因为静态修饰的内容有成员变量和函数。

  • 什么时候定义静态变量(类变量)呢?
    当对象中出现共享数据时,该数据被静态所修饰,对象中的特有数据要定义成非静态,存在堆内存中。
  • 什么时候定义静态函数呢?
    当功能内部没有访问到非静态数据(对象的特有数据),那么该功能就定义成静态的。

这里以一个例子来说明静态的应用。每一个应用程序中都有共性的功能,可以将这些功能进行抽取,独立封装,以便复用。我们试图建立一个可以对数组进行操作的工具类,该类中提供了获取最值、排序等功能。
分析:虽然可以通过建立ArrayTool的对象使用这些工具方法,对数组进行操作,发现了问题:

  1. 对象是用于封装数据的,可是ArrayTool对象并未封装特有数据;
  2. 操作数组的每一个方法都没有用到ArrayTool对象中的特有数据。

这时就考虑,让程序更严谨,是不需要对象的。所以可以将ArrayTool中的方法都定义成static的,直接通过类名调用即可。将方法都静态后,可以方便于使用,但是该类还是可以被其他程序建立对象的,为了更为严谨,强制让该类不能建立对象,可以通过将构造函数私有化完成。

/**
这是一个可以对数组进行操作的工具类,该类中提供了获取最值、排序等功能。
@author 李阿昀
@version V1.1
*/
public class ArrayTool {

    // ArrayTool() {}

    /**
    空参数构造函数
    */
    private ArrayTool() {

    }
    /**
    获取一个整型数组中的最大值
    @param arr 接受一个int类型的数组
    @return 会返回一个该数组中的最大值
    */
    public static int getMax(int[] arr) {
        int max = 0;
        for (int x = 0; x < arr.length; x++) {
            if(arr[x] > arr[max])
                max = x;
        }
        return arr[max];
    }

    /**
    获取一个整型数组中的最小值
    @param arr 接受一个int类型的数组
    @return 会返回一个该数组中的最小值
    */
    public static int getMin(int[] arr) {
        int min = 0;
        for (int x = 0; x < arr.length; x++) {
            if(arr[x] < arr[min])
                min = x;
        }
        return arr[min];
    }

    /**
    给int数组进行选择排序
    @param arr 接受一个int类型的数组
    */
    public static void selectSort(int[] arr) {
        for (int x = 0; x < arr.length - 1; x++) {
            for (int y = x + 1; y < arr.length; y++) {
                if(arr[x] > arr[y]) {
                    swap(arr, x, y);    
                }
            }
        }
    }

    /**
    给int数组进行冒泡排序
    @param arr 接受一个int类型的数组
    */
    public static void bubbleSort(int[] arr) {
        for (int x = 0; x < arr.length - 1; x++) {
            for (int y = 0; y < arr.length - x - 1; y++) {
                if(arr[y] > arr[y+1]) {
                    swap(arr, y, y+1);      
                }
            }
        }
    }

    /**
    给数组中的元素进行位置的置换
    @param arr 接受一个int类型的数组
    @param a 要置换的位置
    @param b 要置换的位置
    */
    private static void swap(int[] arr, int a, int b) {
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }

    /**
    用于打印数组中的元素。打印形式是:[element1, element2, ...]
    @param arr 接受一个int类型的数组
    */
    public static void printArray(int[] arr) {
        System.out.print("[");
        for (int x = 0; x < arr.length; x++) {
            if(x != arr.length - 1) 
                System.out.print(arr[x]+", ");
            else
                System.out.println(arr[x]+"]");
        }
    }
}

接下来,将ArrayTool.class文件发送给其他人,其他人只要将该文件设置到classpath路径下,就可以使用该工具类。但是,很遗憾,该类中定义了多少个方法,对方不清楚,因为该类并没有使用说明书,所以开始制作程序的说明书,java的说明书通过文档注释来完成。即:

javac -d myhelp -author -version ArrayTool.java。  
  • myhelp:为文档生成的目录,默认为当前目录,也可以指定为其他目录;
  • -author:文档作者的名字;
  • -version:文档版本。

对象的初始化流程

到了最后一个知识点了,即对象的初始化流程。我还是用一个例子来说明,例有如下代码:

class Person
{
    private int age = 8; // 显示初始化

    { // 构造代码块。给所有对象进行初始化的。构造函数只给对应的对象初始化。
        System.out.println("constructor code run!!!......." + age);
    }

    Person()
    {
        // 隐藏了以下三句代码:
        // 1. super()调用父类构造函数
        // 2. 显示初始化
        // 3. 构造代码块初始化。
        System.out.println("person run!!!");
    }

    Person(int age)
    {
        this.age = age;
        System.out.println("person(age) run!!!");
    }
}
/*
创建一个对象的流程:
1,加载指定的字节码文件进内存
2,通过new在堆内存中开辟空间,并分配首地址值
3,对对象中的属性进行默认初始化
4,调用与之对应的构造函数,构造函数压栈
5,构造函数中执行隐式的语句super()访问父类中的构造函数
6,对属性进行显示初始化
7,调用类中的构造代码块
8,执行构造函数中自定义的初始化代码
9,初始化完毕,将地址赋值给指定的引用
*/
class ConsCodeDemo 
{
    public static void main(String[] args) 
    {
        Person p = new Person();
    }
}

你能试着说明Person p = new Person();该句话都做了哪些事情?——创建一个对象的流程为:

  1. 加载指定的字节码文件进内存;
  2. 通过new在堆内存中开辟空间,并分配首地址值;
  3. 对对象中的属性进行默认初始化;
  4. 调用与之对应的构造函数,构造函数压栈;
  5. 构造函数中执行隐式的语句super()访问父类中的构造函数;
  6. 对属性进行显示初始化;
  7. 调用类中的构造代码块;
  8. 执行构造函数中自定义的初始化代码;
  9. 初始化完毕,将地址赋值给指定的引用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李阿昀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值